Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- In CFML, `createObject("component", "contenscms.core.queue.batchcontext")`, and the one-argument `createObject("contenscms.core.queue.batchcontext")`, now link to the component they create, found by its path the way `extends` finds one, as `new` already did. A call chained onto the new object, like `createObject(…).init()` or `new contenscms.core.queue.batchcontext().init()`, now links to that component's `init`, or to the one it inherits. In a framework's own source, a path like `wheels.JobWorker`, whose first part is the framework itself, now also finds the component at the project root, for `extends` as well. Before, `createObject` showed up as a call to a function that doesn't exist, and the chained `init` was linked to whichever component's `init` came first, or to none. Thanks @HarryMuc for the report. (#2652)
- In VB.NET, a call on a variable, parameter, field or property now reaches a method of the type it is declared with — one that type inherits, or an extension method written for it — and nothing when that type comes from outside your project, instead of any project method that merely shares the name.
- In VB.NET, a call to one of .NET's own methods such as `Add`, `Contains`, `Clear` or `Dispose` on a value whose type isn't known is no longer linked to a project method of that name, unless the value is named after that method's class.
- A method that hands its call on to another object's method of the same name, like a wrapper's `def unbind(value) = wrapped.unbind(value)` or `form.validate()` inside its own `validate()`, no longer links that call back to itself, so the wrapper no longer looks recursive in callers and impact. Calls through `this` or `self`, bare calls, and calls in Go keep their link when they really are recursive. Thanks @danusha2345.
- In VB.NET, a type name is looked up the way VB.NET does it — through the namespaces around it, the file's and project's `Imports` (aliases included), and the caller's own project — so a class declared in several namespaces or projects no longer draws every call to whichever copy was indexed first, and two candidates nothing tells apart get no link at all.
- In VB.NET, a call with no receiver, or on `Me`, now reaches only a member of the class it is written in, of a class it inherits, or of a Module, and no longer a nearby class's member of the same name.
- VB.NET designer code such as `New System.Drawing.Point(…)` or `Me.Size = New System.Drawing.Size(…)` is no longer linked to a project type or member that only shares the name, and a class nested inside another is matched by its bare name only from inside that class or one that inherits it.
Expand Down
76 changes: 76 additions & 0 deletions __tests__/resolution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6470,4 +6470,80 @@ bracketed()
expect(reachedFrom('consumer.ts', 'HiddenFace')).toBe(false);
}, 30000);
});

describe('Name-only method match never binds a delegating call to its own caller', () => {
const selfLoops = (g: CodeGraph, file: string): string[] =>
g.getNodesInFile(file)
.filter((n) => n.kind === 'method' || n.kind === 'function')
.flatMap((n) => g.getOutgoingEdges(n.id)
.filter((e) => e.kind === 'calls' && e.target === n.id)
.map(() => n.name));

it('drops the self-loop for a Scala delegating wrapper', async () => {
fs.writeFileSync(path.join(tempDir, 'Form.scala'), `
package play.api.data
class WrappedMapping(wrapped: Mapping) {
def unbind(value: String): String = wrapped.unbind(value)
}
`);
cg = await CodeGraph.init(tempDir, { index: true });
cg.resolveReferences();
expect(selfLoops(cg, 'Form.scala')).toEqual([]);
}, 30000);

it('drops the self-loop for a TypeScript wrapper handing its call to a parameter', async () => {
fs.writeFileSync(path.join(tempDir, 'wrapped.ts'), `
export class WrappedMapping {
unbind(wrapped, value: string): string {
return wrapped.unbind(value);
}
}
`);
cg = await CodeGraph.init(tempDir, { index: true });
cg.resolveReferences();
expect(selfLoops(cg, 'wrapped.ts')).toEqual([]);
}, 30000);

it('keeps real recursion through this./bare calls and a Go named receiver', async () => {
fs.writeFileSync(path.join(tempDir, 'Tree.java'), `
public class Tree {
public int depth(int n) { return n == 0 ? 0 : this.depth(n - 1); }
public int size(int n) { return n == 0 ? 0 : size(n - 1); }
}
`);
fs.writeFileSync(path.join(tempDir, 'relay.go'), `
package relay
type relayClient struct{}
func (r *relayClient) sendBatchAttempt(retry bool) error {
if retry {
return r.sendBatchAttempt(false)
}
return nil
}
`);
cg = await CodeGraph.init(tempDir, { index: true });
cg.resolveReferences();
expect(selfLoops(cg, 'Tree.java').sort()).toEqual(['depth', 'size']);
expect(selfLoops(cg, 'relay.go')).toEqual(['sendBatchAttempt']);
}, 30000);

it('keeps Go recursion through a range variable over the type\'s own children', async () => {
// cobra's `sub.checkCommandGroups()` over `c.commands`, etcd's
// `child.Repr(…)`: the loop variable is untyped, and it is the caller's type.
fs.writeFileSync(path.join(tempDir, 'node.go'), `
package tree
type Node struct{ children []*Node }
func (n *Node) Size() int {
total := 1
for _, child := range n.children {
total += child.Size()
}
return total
}
`);
cg = await CodeGraph.init(tempDir, { index: true });
cg.resolveReferences();
expect(selfLoops(cg, 'node.go')).toEqual(['Size']);
}, 30000);
});
});
25 changes: 25 additions & 0 deletions src/resolution/name-matcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20604,6 +20604,7 @@ export function matchMethodCall(
!(ref.language === 'objc' && objcReceiverReaches(objectOrClass!, targetMethods[0]!, context)) &&
!(ref.language === 'php' && phpReceiverReaches(objectOrClass!, targetMethods[0]!, context)))) {
if (throughCppNamespace() && !isCppConstructorThrough(objectOrClass!, targetMethods[0]!, context)) return null;
if (isForeignReceiverSelfLoop(targetMethods[0]!, objectOrClass!, ref)) return null;
return {
original: ref,
targetNodeId: targetMethods[0]!.id,
Expand Down Expand Up @@ -20918,6 +20919,30 @@ function importBindsOther(candidate: Node, ref: UnresolvedRef, context: Resoluti
return !/\bimport\s*\(/.test(lines.slice(node.startLine - 1, Math.min(node.endLine, node.startLine + 15)).join('\n'));
}

/** Receivers that name the current object (or its own class/base) — a call
* through one of these may genuinely recurse into the enclosing method. */
const SELF_RECEIVERS = new Set(['this', 'self', 'Self', 'super', 'cls']);

/**
* Strategy 3's single-candidate branch matches by method NAME alone, so a
* delegating wrapper whose method is the only one of that name —
* `wrapped.unbind(v)` inside `unbind(v)` — name-matches the method it is
* written in and the resolver emitted a self-loop. (The word-overlap branch
* already refuses the caller itself.) A receiver that is an explicit, other
* object is by construction not the enclosing method's own instance, so that
* candidate is the one guess we know is wrong: no edge beats a wrong edge.
* Real recursion (`this.foo()`, `self.foo()`, bare `foo()`) is kept; Go's
* named method receiver (`r.foo()`) is typed and never gets here. Go is left
* out altogether: a method's receiver and a struct's fields are typed before
* this point, so a Go call that still arrives here is on a local the
* resolver could not type, mostly a range variable over the type's own
* children — cobra's `sub.checkCommandGroups()` over `c.commands`, etcd's
* `child.Repr(…)` — which is real recursion.
*/
function isForeignReceiverSelfLoop(candidate: Node, receiver: string, ref: UnresolvedRef): boolean {
return candidate.id === ref.fromNodeId && !SELF_RECEIVERS.has(receiver) && ref.language !== 'go';
}

/** The directory of a Go file, which is its package: Go keeps one package per directory. */
function goPackageDir(filePath: string): string {
return path.posix.dirname(filePath.replace(/\\/g, '/'));
Expand Down