Ports the #645 (C++) / #608 (PHP) chained-receiver mechanism to Scala. A call whose receiver is itself a call — `Foo.create().bar()` (companion factory), `Builder(cfg).bar()` (case-class apply), or a fluent chain — used to drop the receiver to a bare `bar`, which name-matched a same-named method on an unrelated type. The most common wrong edge was a stdlib `Option`/`Iterator` `.map`/`.flatMap`/ `.foreach` mis-attributed onto the project's own same-named class. - scala.ts: `getReturnType` reads the `return_type` field — generic `List[Foo]` → container `List`, qualified `pkg.Foo` → `Foo`, `this.type` left undefined. - tree-sitter.ts: re-encode `Foo.create().bar` when the inner call's receiver chain starts with a capital (companion factory / case-class apply); instance chains (`list.map().filter()`) stay bare. - name-matcher.ts: `scala` joins the dotted-chain gate + CONSTRUCTS_VIA_BARE_CALL (case-class `apply` constructs the class); resolveMethodOnType validates, so a non-conventional `apply` returning another type yields no edge, not a wrong one. - index.ts: `scala` joins CHAIN_LANGUAGES so trait-inherited methods resolve via the conformance second pass. Validation: 4 synthetic tests (factory+decoy, case-class apply, trait conformance, absent-method safety). Real-repo A/B on gatling (750 Scala files): +14 / -59 unique edges — all corrections. The +14 are retargets (e.g. `HttpProtocolBuilder(cfg).baseUrl` now resolves to HttpProtocolBuilder::baseUrl, not the same-named private BaseUrlSupport helper); the -59 are wrong edges removed (stdlib Option/Iterator monad calls mis-tied to the project's Validation::*, self-loops, decoy collisions) — zero genuine factory chains dropped (verified: gatling has no real Validation.success().map() chains). db stable at 40 MB. EXTRACTION_VERSION 12→13. Full suite green. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
ccced9e358
commit
2f96f58cbb
@@ -21,4 +21,4 @@
|
||||
* turns the re-index hint into noise — keep it honest (see CLAUDE.md, "Honesty
|
||||
* in the product is load-bearing").
|
||||
*/
|
||||
export const EXTRACTION_VERSION = 12;
|
||||
export const EXTRACTION_VERSION = 13;
|
||||
|
||||
@@ -44,6 +44,28 @@ function emitScalaTypeRefs(typeNode: SyntaxNode, fromId: string, ctx: { addUnres
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Capture a Scala method's declared return type as a bare type name, for the
|
||||
* chained static-factory / fluent call mechanism (#750). `def create(): Bar`
|
||||
* yields `Bar`; a generic `List[Bar]` yields its base `List` (the method is on
|
||||
* the container, not the element); a qualified `pkg.Bar` yields `Bar`. A
|
||||
* singleton self-type (`this.type`, the fluent-builder idiom) is left undefined
|
||||
* — its type can't be recovered here, so the chain falls through rather than
|
||||
* inferring a wrong receiver.
|
||||
*/
|
||||
function extractScalaReturnType(node: SyntaxNode, source: string): string | undefined {
|
||||
const rt = node.childForFieldName('return_type');
|
||||
if (!rt) return undefined;
|
||||
const raw = getNodeText(rt, source).trim();
|
||||
if (raw.startsWith('this.')) return undefined; // `this.type` singleton — unhandled
|
||||
const base = raw
|
||||
.replace(/\[[^\]]*\]/g, '') // strip generic args: List[Bar] → List
|
||||
.replace(/\s+/g, '');
|
||||
const last = base.split('.').pop(); // qualified pkg.Bar → Bar
|
||||
if (!last || !/^[A-Za-z_]\w*$/.test(last)) return undefined;
|
||||
return last;
|
||||
}
|
||||
|
||||
function extractVisibility(node: SyntaxNode): 'public' | 'private' | 'protected' {
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const child = node.namedChild(i);
|
||||
@@ -77,6 +99,7 @@ export const scalaExtractor: LanguageExtractor = {
|
||||
bodyField: 'body',
|
||||
paramsField: 'parameters',
|
||||
returnField: 'return_type',
|
||||
getReturnType: extractScalaReturnType,
|
||||
interfaceKind: 'trait',
|
||||
|
||||
classifyClassNode: (node: SyntaxNode) => {
|
||||
|
||||
@@ -2530,7 +2530,8 @@ export class TreeSitterExtractor {
|
||||
this.language === 'kotlin' ||
|
||||
this.language === 'swift' ||
|
||||
this.language === 'rust' ||
|
||||
this.language === 'go') &&
|
||||
this.language === 'go' ||
|
||||
this.language === 'scala') &&
|
||||
receiver &&
|
||||
receiver.type === 'call_expression'
|
||||
) {
|
||||
@@ -2572,6 +2573,11 @@ export class TreeSitterExtractor {
|
||||
// only drop the edge. C/C++ re-encode any inner.
|
||||
if (this.language === 'rust') reencode = innerFn?.type === 'scoped_identifier';
|
||||
else if (this.language === 'go') reencode = innerFn?.type === 'identifier';
|
||||
// Scala: only a companion-factory / case-class-apply chain whose
|
||||
// receiver chain starts with a capitalized type (`Foo.create().bar()`,
|
||||
// `Foo(args).bar()`). An instance chain (`list.map().filter()`) has a
|
||||
// lowercase receiver whose type we can't recover — leave it bare.
|
||||
else if (this.language === 'scala') reencode = /^[A-Z]/.test(innerCallee);
|
||||
else reencode = !!innerCallee;
|
||||
}
|
||||
calleeName = reencode ? `${innerCallee}().${methodName}` : methodName;
|
||||
|
||||
@@ -37,7 +37,7 @@ const SUPERTYPE_BEARING_KINDS = new Set<Node['kind']>([
|
||||
* second pass. Dotted-receiver languages resolve via matchDottedCallChain; the
|
||||
* `::`-receiver ones (Rust) via matchScopedCallChain.
|
||||
*/
|
||||
const CHAIN_LANGUAGES = new Set(['java', 'kotlin', 'csharp', 'swift', 'rust', 'go']);
|
||||
const CHAIN_LANGUAGES = new Set(['java', 'kotlin', 'csharp', 'swift', 'rust', 'go', 'scala']);
|
||||
const SCOPED_CHAIN_LANGUAGES = new Set(['rust']);
|
||||
|
||||
/** The extractor's chained-receiver encoding: `<inner>().<method>`. */
|
||||
|
||||
@@ -600,9 +600,12 @@ export function matchScopedCallChain(
|
||||
/**
|
||||
* Languages where an unprefixed capitalized call `Foo(args)` constructs the
|
||||
* class (so a `Foo(args).method()` receiver's type is `Foo`). Java/C# need `new`,
|
||||
* so a bare `Foo()` there is a method call, not construction — excluded.
|
||||
* so a bare `Foo()` there is a method call, not construction — excluded. Scala's
|
||||
* `Foo(args)` is a case-class / companion `apply`, which conventionally returns
|
||||
* `Foo` — and resolveMethodOnType validates, so a non-conventional `apply` that
|
||||
* returns another type simply yields no edge rather than a wrong one.
|
||||
*/
|
||||
const CONSTRUCTS_VIA_BARE_CALL = new Set(['kotlin', 'swift']);
|
||||
const CONSTRUCTS_VIA_BARE_CALL = new Set(['kotlin', 'swift', 'scala']);
|
||||
|
||||
/**
|
||||
* Resolve a dotted chained call whose receiver is a static factory / fluent call —
|
||||
@@ -1120,15 +1123,17 @@ export function matchReference(
|
||||
}
|
||||
|
||||
// 1d. Dotted chained static-factory / fluent call (Java / Kotlin / C# / Swift /
|
||||
// Go) — `Foo.getInstance().bar()` encoded as `Foo.getInstance().bar`, or Go's
|
||||
// bare-factory `New().Method()` as `New().Method` (#645/#608 mechanism). Resolve
|
||||
// the method's class from the inner call's declared return type, then validate it.
|
||||
// Go / Scala) — `Foo.getInstance().bar()` encoded as `Foo.getInstance().bar`,
|
||||
// Go's bare-factory `New().Method()` as `New().Method`, or Scala's companion
|
||||
// factory `Foo.create().bar()` (#645/#608 mechanism). Resolve the method's class
|
||||
// from the inner call's declared return type, then validate it.
|
||||
if (
|
||||
ref.language === 'java' ||
|
||||
ref.language === 'kotlin' ||
|
||||
ref.language === 'csharp' ||
|
||||
ref.language === 'swift' ||
|
||||
ref.language === 'go'
|
||||
ref.language === 'go' ||
|
||||
ref.language === 'scala'
|
||||
) {
|
||||
result = matchDottedCallChain(ref, context);
|
||||
if (result) return result;
|
||||
|
||||
Reference in New Issue
Block a user