fix(cpp): resolve explicit operator calls (a.operator+(b)) to the operator method (#1268)
* fix(cpp): resolve explicit operator calls (a.operator+(b)) to the operator method (#1247) tree-sitter-cpp can't parse an operator_name in field position: the call_expression carries `function: <receiver>` plus an ERROR child wrapping the operator_name instead of a field_expression callee, so the extractor emitted a calls ref named just the receiver (`a`) and the edge never resolved — while the operator method itself indexed fine. Two-part fix, scoped to the explicit call form (infix `a + b` / `a[i]` need receiver type inference and are tracked in #1258): - extraction: recover the operator_name from the ERROR child and emit `<receiver>.operator+` (`->` receivers normalized, `this->` emits the bare name), like any other member call - resolution: matchMethodCall's dot pattern now admits an operator method part (cpp-gated; symbol chars failed the \w match), so receiver-type inference + resolveMethodOnType validate the target — a same-named operator on an unrelated class can't capture the edge Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(cpp): harden explicit operator-call recovery against real-world shapes (#1247) Validated on nlohmann/json (dozens of explicit operator[] / operator* / operator< call sites). Two refinements the synthetic fixtures missed: - normalize spaced call-site operator names (`it.operator * ()`, `other.operator < (*this)`) to the compact form definitions index as - drop the ref for a complex receiver (`obj()->operator+`, member chains ending in a call) instead of emitting a bare operator name: exact-name fallback GUESSED among unrelated same-named operators (linked a std::map operator[] call to an in-repo operator[]) — silent miss, never a wrong edge Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
ecc8b307ac
commit
6103f5e228
@@ -9,6 +9,9 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- C++ explicit operator calls — `a.operator+(b)`, `p->operator+(b)`, `a.operator[](3)`, and the other symbolic forms — now produce a `calls` edge to the operator method, so an operator invoked only through the explicit syntax no longer looks uncalled in callers and impact analysis. tree-sitter parses these call sites with the operator name stranded in an error node (never as a normal member access), so the call's target was silently read as just the receiver variable; the operator name is now recovered from the error node and resolved through receiver-type inference like any other member call — a same-named operator on an unrelated class can never capture the edge. Infix uses (`a + b`, `a[i]`) need real type inference and are tracked separately. (#1247)
|
||||||
|
|
||||||
## [1.4.1] - 2026-07-10
|
## [1.4.1] - 2026-07-10
|
||||||
|
|
||||||
|
|||||||
@@ -3823,6 +3823,75 @@ class APXCharacter { // the one real definition
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('C++ explicit operator-call refs (#1247)', () => {
|
||||||
|
// tree-sitter-cpp can't parse an operator_name in field position:
|
||||||
|
// `a.operator+(b)` yields `call_expression(function: identifier «a»,
|
||||||
|
// ERROR(operator_name), argument_list)` instead of a field_expression
|
||||||
|
// callee, so the emitted ref was just the receiver (`a`) and the call never
|
||||||
|
// resolved. The extractor recovers the operator_name from the ERROR child
|
||||||
|
// and emits `<receiver>.operator+` like any other member call.
|
||||||
|
const HEADER = 'struct V {\n V operator+(const V& o) const;\n V operator[](int i) const;\n V operator()(int i) const;\n bool operator==(const V& o) const;\n int get() const;\n};\n';
|
||||||
|
const callRefsOf = (body: string) =>
|
||||||
|
extractFromSource('op.cpp', HEADER + body)
|
||||||
|
.unresolvedReferences.filter((r) => r.referenceKind === 'calls')
|
||||||
|
.map((r) => r.referenceName);
|
||||||
|
|
||||||
|
it('recovers receiver.operator+ from the explicit call form', () => {
|
||||||
|
expect(callRefsOf('V f(const V& a, const V& b) { return a.operator+(b); }\n')).toContain('a.operator+');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('recovers pointer receivers (p->operator+ → p.operator+)', () => {
|
||||||
|
expect(callRefsOf('V f(const V* p, const V& b) { return p->operator+(b); }\n')).toContain('p.operator+');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('recovers subscript, call, and comparison operator forms', () => {
|
||||||
|
const refs = callRefsOf(
|
||||||
|
'V f1(const V& a) { return a.operator[](3); }\n' +
|
||||||
|
'V f2(V& a) { return a.operator()(1); }\n' +
|
||||||
|
'bool f3(const V& a, const V& b) { return a.operator==(b); }\n'
|
||||||
|
);
|
||||||
|
expect(refs).toContain('a.operator[]');
|
||||||
|
expect(refs).toContain('a.operator()');
|
||||||
|
expect(refs).toContain('a.operator==');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('normalizes spaced call-site operator names to the compact definition form', () => {
|
||||||
|
// nlohmann/json calls `it.operator * ()` / `other.operator < (*this)`
|
||||||
|
// while defining `operator*` / `operator<` compact.
|
||||||
|
const refs = callRefsOf(
|
||||||
|
'bool f(const V& a, const V& b) { return a.operator == (b); }\n' +
|
||||||
|
'V g(const V& a) { return a.operator [] (3); }\n'
|
||||||
|
);
|
||||||
|
expect(refs).toContain('a.operator==');
|
||||||
|
expect(refs).toContain('a.operator[]');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('drops the ref for a complex receiver instead of guessing (no wrong edge)', () => {
|
||||||
|
// `object->operator[](val)` through a member chain ending in a call —
|
||||||
|
// the receiver type isn't inferable and a bare `operator[]` ref would
|
||||||
|
// let exact-name matching guess among unrelated operators.
|
||||||
|
const refs = callRefsOf(
|
||||||
|
'struct W { V* obj(); };\n' +
|
||||||
|
'V f(W& w, const V& b) { return w.obj()->operator+(b); }\n'
|
||||||
|
);
|
||||||
|
expect(refs.some((r) => r.includes('operator+'))).toBe(false);
|
||||||
|
expect(refs).toContain('w.obj'); // the inner call itself still refs normally
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits the bare operator name for a this-> receiver', () => {
|
||||||
|
const refs = extractFromSource(
|
||||||
|
'op.cpp',
|
||||||
|
'struct V {\n V operator+(const V& o) const;\n V twice() const { return this->operator+(*this); }\n};\n'
|
||||||
|
).unresolvedReferences.filter((r) => r.referenceKind === 'calls').map((r) => r.referenceName);
|
||||||
|
expect(refs).toContain('operator+');
|
||||||
|
expect(refs.some((r) => r.includes('this'))).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('leaves plain member calls unchanged (control)', () => {
|
||||||
|
expect(callRefsOf('int f(const V& a) { return a.get(); }\n')).toContain('a.get');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('C++ macro-prefixed function names (#1093 follow-up)', () => {
|
describe('C++ macro-prefixed function names (#1093 follow-up)', () => {
|
||||||
// An unknown inline-specifier macro before the return type
|
// An unknown inline-specifier macro before the return type
|
||||||
// (`FORCEINLINE FString GetName(…)`) threw tree-sitter into error recovery:
|
// (`FORCEINLINE FString GetName(…)`) threw tree-sitter into error recovery:
|
||||||
|
|||||||
@@ -3180,6 +3180,76 @@ void wrong() { WidgetFactory::create().onlyOther(); }
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('C++ explicit operator-call resolution (#1247)', () => {
|
||||||
|
// `a.operator+(b)` produced no calls edge: the operator_name lands in an
|
||||||
|
// ERROR node (never a field_expression callee), so the extractor emitted a
|
||||||
|
// ref named just `a`. With the ERROR-node recovery it emits `a.operator+`,
|
||||||
|
// and matchMethodCall (dot pattern extended to admit operator method parts)
|
||||||
|
// resolves it through receiver-type inference. Infix `a + b` / `a[i]` need
|
||||||
|
// real type inference and are out of scope here (#1258).
|
||||||
|
async function indexCpp(files: Record<string, string>): Promise<void> {
|
||||||
|
for (const [name, content] of Object.entries(files)) {
|
||||||
|
fs.writeFileSync(path.join(tempDir, name), content);
|
||||||
|
}
|
||||||
|
cg = await CodeGraph.init(tempDir, { index: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function callerNamesOf(qualifiedName: string): string[] {
|
||||||
|
const target = cg.getNodesByKind('method').find((n) => n.qualifiedName === qualifiedName);
|
||||||
|
if (!target) return [];
|
||||||
|
const names = cg
|
||||||
|
.getIncomingEdges(target.id)
|
||||||
|
.filter((e) => e.kind === 'calls')
|
||||||
|
.map((e) => cg.getNode(e.source)?.name)
|
||||||
|
.filter((n): n is string => !!n);
|
||||||
|
return [...new Set(names)].sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
it('resolves explicit operator calls to the receiver type, never a same-named decoy', async () => {
|
||||||
|
// Aaa sorts first and declares the same operators — only receiver-type
|
||||||
|
// inference (const V& a → V) can pick V, so a name-only tie can't win.
|
||||||
|
await indexCpp({
|
||||||
|
'optest.cpp': `struct Aaa {
|
||||||
|
Aaa operator+(const Aaa& o) const { return o; }
|
||||||
|
Aaa operator[](int i) const { return *this; }
|
||||||
|
};
|
||||||
|
struct V {
|
||||||
|
int x;
|
||||||
|
V operator+(const V& o) const { return V{x + o.x}; }
|
||||||
|
V operator[](int i) const { return V{x + i}; }
|
||||||
|
int get() const { return x; }
|
||||||
|
};
|
||||||
|
int plainCaller(const V& a) { return a.get(); }
|
||||||
|
V explicitCaller(const V& a, const V& b) { return a.operator+(b); }
|
||||||
|
V subscriptCaller(const V& a) { return a.operator[](3); }
|
||||||
|
V pointerCaller(const V* p, const V& b) { return p->operator+(b); }
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(callerNamesOf('V::operator+')).toEqual(['explicitCaller', 'pointerCaller']);
|
||||||
|
expect(callerNamesOf('V::operator[]')).toEqual(['subscriptCaller']);
|
||||||
|
expect(callerNamesOf('V::get')).toEqual(['plainCaller']); // control: plain calls unaffected
|
||||||
|
expect(callerNamesOf('Aaa::operator+')).toEqual([]);
|
||||||
|
expect(callerNamesOf('Aaa::operator[]')).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolves an out-of-line operator definition (declaration in header)', async () => {
|
||||||
|
await indexCpp({
|
||||||
|
'v.hpp': `#pragma once
|
||||||
|
struct V { int x; V operator+(const V& o) const; };
|
||||||
|
`,
|
||||||
|
'v.cpp': `#include "v.hpp"
|
||||||
|
V V::operator+(const V& o) const { return V{x + o.x}; }
|
||||||
|
`,
|
||||||
|
'app.cpp': `#include "v.hpp"
|
||||||
|
V add(const V& a, const V& b) { return a.operator+(b); }
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(callerNamesOf('V::operator+')).toEqual(['add']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('PHP chained static-factory call resolution (#608)', () => {
|
describe('PHP chained static-factory call resolution (#608)', () => {
|
||||||
function callerNamesOf(qualifiedName: string): string[] {
|
function callerNamesOf(qualifiedName: string): string[] {
|
||||||
const target = cg.getNodesByKind('method').find((n) => n.qualifiedName === qualifiedName);
|
const target = cg.getNodesByKind('method').find((n) => n.qualifiedName === qualifiedName);
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { describe, it, beforeAll } from 'vitest';
|
||||||
|
import { extractFromSource } from '../src/extraction';
|
||||||
|
import { initGrammars, loadAllGrammars, getParser } from '../src/extraction/grammars';
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await initGrammars();
|
||||||
|
await loadAllGrammars();
|
||||||
|
});
|
||||||
|
|
||||||
|
const CODE = `struct V {
|
||||||
|
int x;
|
||||||
|
V operator+(const V& o) const { return V{x + o.x}; }
|
||||||
|
V operator[](int i) const { return V{x + i}; }
|
||||||
|
int get() const { return x; }
|
||||||
|
};
|
||||||
|
|
||||||
|
int plainCaller(const V& a) { return a.get(); }
|
||||||
|
V explicitCaller(const V& a, const V& b) { return a.operator+(b); }
|
||||||
|
`;
|
||||||
|
|
||||||
|
describe('scratch', () => {
|
||||||
|
it('dumps', async () => {
|
||||||
|
const parser: any = getParser('cpp' as any);
|
||||||
|
const tree = parser.parse(CODE);
|
||||||
|
const dump = (n: any, d = 0) => {
|
||||||
|
let out = `${' '.repeat(d)}${n.type} [${JSON.stringify(n.text.slice(0, 40))}]\n`;
|
||||||
|
for (let i = 0; i < n.childCount; i++) {
|
||||||
|
const c = n.child(i);
|
||||||
|
const f = n.fieldNameForChild ? n.fieldNameForChild(i) : null;
|
||||||
|
out += `${' '.repeat(d + 1)}${f ? f + ': ' : ''}`.trimEnd() ? '' : '';
|
||||||
|
out += dump(c, d + 1);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
// just dump the explicitCaller subtree
|
||||||
|
console.log(dump(tree.rootNode));
|
||||||
|
|
||||||
|
const result = extractFromSource('optest.cpp', CODE);
|
||||||
|
console.log('NODES', result.nodes.map((n: any) => `${n.kind}:${n.name}`));
|
||||||
|
console.log('KEYS', Object.keys(result));
|
||||||
|
console.log('UREFS', JSON.stringify((result as any).unresolvedReferences, null, 1));
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { describe, it, beforeAll } from 'vitest';
|
||||||
|
import { extractFromSource } from '../src/extraction';
|
||||||
|
import { initGrammars, loadAllGrammars, getParser } from '../src/extraction/grammars';
|
||||||
|
|
||||||
|
beforeAll(async () => { await initGrammars(); await loadAllGrammars(); });
|
||||||
|
|
||||||
|
const CASES: Record<string,string> = {
|
||||||
|
dot_plus: 'V f(V a, V b) { return a.operator+(b); }',
|
||||||
|
arrow_plus: 'V f(V* a, V b) { return a->operator+(b); }',
|
||||||
|
dot_sub: 'V f(V a) { return a.operator[](3); }',
|
||||||
|
dot_call: 'V f(V a) { return a.operator()(3); }',
|
||||||
|
dot_eq: 'bool f(V a, V b) { return a.operator==(b); }',
|
||||||
|
dot_bool: 'bool f(V a) { return a.operator bool(); }',
|
||||||
|
qualified: 'V f(V a, V b) { return V::operator+(a, b); }',
|
||||||
|
free_op: 'V f(V a, V b) { return operator+(a, b); }',
|
||||||
|
this_op: 'struct V { V g(V b) { return this->operator+(b); } };',
|
||||||
|
member_op: 'struct V { V x; V g(V b) { return x.operator+(b); } };',
|
||||||
|
arrow_deref: 'V f(V a) { return a.operator->(); }',
|
||||||
|
dot_notop: 'bool f(V a) { return a.operator!(); }',
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('dump', () => {
|
||||||
|
it('all', () => {
|
||||||
|
const p: any = getParser('cpp' as any);
|
||||||
|
for (const [k, code] of Object.entries(CASES)) {
|
||||||
|
const tree = p.parse(code);
|
||||||
|
const dump = (n: any, d = 0): string => {
|
||||||
|
let out = `${' '.repeat(d)}${n.type}${n.childCount === 0 ? ' ' + JSON.stringify(n.text) : ''}\n`;
|
||||||
|
for (let i = 0; i < n.childCount; i++) out += dump(n.child(i), d + 1);
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
const call = (function find(n: any): any {
|
||||||
|
if (n.type === 'call_expression') return n;
|
||||||
|
for (let i = 0; i < n.childCount; i++) { const r = find(n.child(i)); if (r) return r; }
|
||||||
|
return null;
|
||||||
|
})(tree.rootNode);
|
||||||
|
const refs = extractFromSource('t.cpp', code).unresolvedReferences.filter((r: any) => r.referenceKind === 'calls');
|
||||||
|
console.log(`\n=== ${k}: ${code}\n${call ? dump(call) : '(no call_expression)'}refs: ${JSON.stringify(refs.map((r: any) => r.referenceName))} hasError=${tree.rootNode.hasError}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { describe, it, beforeAll } from 'vitest';
|
||||||
|
import { extractFromSource } from '../src/extraction';
|
||||||
|
import { initGrammars, loadAllGrammars } from '../src/extraction/grammars';
|
||||||
|
beforeAll(async () => { await initGrammars(); await loadAllGrammars(); });
|
||||||
|
describe('d', () => { it('n', () => {
|
||||||
|
const code = `struct V {
|
||||||
|
int x;
|
||||||
|
operator bool() const { return x != 0; }
|
||||||
|
V operator+(const V& o) const { return V{x+o.x}; }
|
||||||
|
V& operator=(const V& o) { x = o.x; return *this; }
|
||||||
|
};`;
|
||||||
|
const r = extractFromSource('t.cpp', code);
|
||||||
|
console.log('NODES', r.nodes.map((n: any) => `${n.kind}:${JSON.stringify(n.name)}`));
|
||||||
|
}); });
|
||||||
@@ -4250,6 +4250,54 @@ export class TreeSitterExtractor {
|
|||||||
} else {
|
} else {
|
||||||
const func = getChildByField(node, 'function') || node.namedChild(0);
|
const func = getChildByField(node, 'function') || node.namedChild(0);
|
||||||
|
|
||||||
|
// C++ explicit operator call `a.operator+(b)` / `p->operator+(b)` (#1247):
|
||||||
|
// tree-sitter-cpp can't parse an operator_name in field position, so the
|
||||||
|
// callee is NOT a field_expression — the call_expression carries
|
||||||
|
// `function: <receiver>` plus an ERROR child wrapping the operator_name.
|
||||||
|
// Reading the function field alone yields just the receiver (`a`), an
|
||||||
|
// unresolvable ref. Recover `<receiver>.operator+` so it resolves like any
|
||||||
|
// other member call (matchMethodCall admits the operator method part).
|
||||||
|
// The infix forms `a + b` / `a[i]` need receiver type inference and are
|
||||||
|
// tracked separately (#1258).
|
||||||
|
if (this.language === 'cpp' && func) {
|
||||||
|
let operatorName = '';
|
||||||
|
for (let i = 0; i < node.namedChildCount; i++) {
|
||||||
|
const child = node.namedChild(i);
|
||||||
|
if (child?.type !== 'ERROR') continue;
|
||||||
|
const op = child.namedChildren.find((c: SyntaxNode) => c.type === 'operator_name');
|
||||||
|
if (op) { operatorName = getNodeText(op, this.source); break; }
|
||||||
|
}
|
||||||
|
if (operatorName) {
|
||||||
|
// Call sites may space the symbolic name (nlohmann/json's
|
||||||
|
// `it.operator * ()`, `other.operator < (*this)`) while definitions
|
||||||
|
// index compact (`operator*`) — normalize so they match. The word
|
||||||
|
// forms (`operator new`) keep their space.
|
||||||
|
const sym = operatorName.slice('operator'.length).trim();
|
||||||
|
if (/^[^\w\s]/.test(sym)) operatorName = `operator${sym.replace(/\s+/g, '')}`;
|
||||||
|
// `->` receivers resolve identically to `.` ones. A receiver that
|
||||||
|
// isn't a simple identifier/member chain (`(*it)`, a call result, …)
|
||||||
|
// can't aid type inference, and a bare operator name would fall
|
||||||
|
// through to exact-name matching — which GUESSES among the many
|
||||||
|
// same-named operators (on nlohmann/json it linked a std::map
|
||||||
|
// `object->operator[]` call to an unrelated in-repo operator[]).
|
||||||
|
// Drop the ref: a silent miss, never a wrong edge. `this->` keeps
|
||||||
|
// the bare name, matching how `this.method()` calls are emitted —
|
||||||
|
// the target is on the enclosing class, where exact-name's same-file
|
||||||
|
// preference is reliable.
|
||||||
|
const receiver = getNodeText(func, this.source).replace(/->/g, '.').replace(/\s+/g, '');
|
||||||
|
if (receiver !== 'this' && !/^[A-Za-z_][\w.]*$/.test(receiver)) return;
|
||||||
|
const calleeName = receiver === 'this' ? operatorName : `${receiver}.${operatorName}`;
|
||||||
|
this.unresolvedReferences.push({
|
||||||
|
fromNodeId: callerId,
|
||||||
|
referenceName: calleeName,
|
||||||
|
referenceKind: 'calls',
|
||||||
|
line: node.startPosition.row + 1,
|
||||||
|
column: node.startPosition.column,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (func) {
|
if (func) {
|
||||||
if (func.type === 'member_expression' || func.type === 'attribute' || func.type === 'selector_expression' || func.type === 'navigation_expression' || func.type === 'field_expression') {
|
if (func.type === 'member_expression' || func.type === 'attribute' || func.type === 'selector_expression' || func.type === 'navigation_expression' || func.type === 'field_expression') {
|
||||||
// Method call: obj.method() or obj.field.method()
|
// Method call: obj.method() or obj.field.method()
|
||||||
|
|||||||
@@ -1459,7 +1459,18 @@ export function matchMethodCall(
|
|||||||
// (with its existing single-candidate / receiver-overlap guards). Without this
|
// (with its existing single-candidate / receiver-overlap guards). Without this
|
||||||
// a multi-dot extension-method call (C# DI `builder.Services.AddCoreServices()`,
|
// a multi-dot extension-method call (C# DI `builder.Services.AddCoreServices()`,
|
||||||
// `Guard.Against.X()`) matched no pattern and never resolved.
|
// `Guard.Against.X()`) matched no pattern and never resolved.
|
||||||
const dotMatch = ref.referenceName.match(/^([\w.]+)\.(\w+:?(?:\w+:)*)$/);
|
// C++ explicit operator call `a.operator+(b)` reaches the resolver as
|
||||||
|
// `a.operator+` (#1247) — the operator's symbol chars (`+`, `==`, `[]`, `()`)
|
||||||
|
// fail the \w method part of the plain pattern, so admit them explicitly.
|
||||||
|
// Names like `operatorTable` stay on the plain pattern (tried first); the
|
||||||
|
// operator form requires at least one non-word char after `operator`, and
|
||||||
|
// every downstream strategy compares the method part by exact string
|
||||||
|
// equality, so a stray match can't invent an edge.
|
||||||
|
const dotMatch =
|
||||||
|
ref.referenceName.match(/^([\w.]+)\.(\w+:?(?:\w+:)*)$/) ??
|
||||||
|
(ref.language === 'cpp'
|
||||||
|
? ref.referenceName.match(/^([\w.]+)\.(operator[^\w\s.]+)$/)
|
||||||
|
: null);
|
||||||
const colonMatch = ref.referenceName.match(/^(\w+)::(\w+)$/);
|
const colonMatch = ref.referenceName.match(/^(\w+)::(\w+)$/);
|
||||||
// Lua/Luau method calls use a single colon (`lg:log`); R uses `$` (`lg$log`).
|
// Lua/Luau method calls use a single colon (`lg:log`); R uses `$` (`lg$log`).
|
||||||
// Recognize these receiver/method separators so local-variable receiver-type
|
// Recognize these receiver/method separators so local-variable receiver-type
|
||||||
|
|||||||
Reference in New Issue
Block a user