mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-17 05:07:37 +08:00
fix(resolution): type parameters, def-local types and Scala scoping (#2123)
Found by the README-wide sweep: cats' most-depended-on symbols were an `implicit def A` (4,916) and a `case class B()` declared inside a test method (3,573) — every type parameter `A`/`B`/`F` in the library bound to them; getx's `T` to a constant `T`; serde's `E`/`T`/`D`/`S`/`F` to test structs. New seam gate (type-parameters.ts, applied in resolveOne so every strategy obeys it): - A resolved TYPE reference whose name an enclosing declaration declares as a type parameter is that parameter: read from the heads (the index records none) — after the declared name (`class Foo<T>`, `fn f<T>`, `func F[T any]`, `def f[F[_]: Monad, A]`), before it detached from a type (`<T> T max(`, `fun <T> f`), a C++ `template <…>` above; annotations (`@sp(Int) A`), variance and keywords skipped; a `[…]` list counts only [] and () so Scala's `B >: A` bound does not close it; lists up to 40 lines. - Scala: a bare name a `def` takes as a value parameter (`(f: A => B)`, `(implicit G: …)`) is that parameter — cats bound 846 `f(a)` calls to a case class's `f` field. Class parameters are fields and are left alone. - Scala: a BARE (in the source) type reference reaches a member of another type only from inside it, a subtype, or a file importing its members; otherwise the in-scope type of that name is taken when one fits (cats' `trait FlatMap`, not `Eval`'s nested `FlatMap`; sttp's `trait Backend`). Name matcher: - A type (class/struct/enum/interface/trait/alias) declared inside a function is only reachable from inside it, like nested functions (#1230). - A capitalized Scala type position never names a method. A/B vs main (edge diffs, every change classified; each arm's init exit code checked — an earlier cut crashed resolution on a Scala method named `*`, now pinned by the test): cats −25,281/+179 (the gains retargets to the in-scope FlatMap/State; the real types Shown and IsEq kept 5/5 and 315/315), sttp −1,237/+23, getx −430, serde −446/+9, typeorm −632/+152 (`EntityTarget<Entity>`'s type parameter; codemod fixtures' function-local classes), gson −9; okio/fmt ±2, Newtonsoft.Json and gin byte-identical. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
### Fixes
|
||||
|
||||
- A generic type parameter, like `A` in `def zipWith[A, B](…)`, `T` in `<T> T max(…)` or `class Foo<T>`, no longer links to a project symbol that happens to share its name. On cats, one `implicit def A` had thousands of made-up dependents from every `A` in the library. The same goes for Rust, Dart, TypeScript, Java, Kotlin, Swift, Go, C# and C++. A class or other type declared inside a function is only linked from inside that function. In Scala, a `def`'s own parameter, like `f` in `(f: A => B)`, no longer links to a same-named field elsewhere. A bare name no longer reaches a type nested inside another type unless the code is inside that type, extends it or imports its members; the type in scope is linked instead, like cats' own `FlatMap` in place of `Eval`'s nested `FlatMap`. Re-index projects in these languages after upgrading.
|
||||
- A minified or bundled JavaScript file is now recognized as generated even when it isn't named `.min.js`, for example a docs site's `bundle.js`, a readable webpack build or a vendored `underscore-min.js`. Its one-letter functions now rank below your own code in search and `codegraph_explore` answers.
|
||||
- In C# and VB.NET, a type written in code, like `Type sourceType`, `List<int>`, `new TypeMap()` or `Dictionary<string, Type>`, now links to the type and no longer to a property, method or field that shares its name. `List<…>` used to link to some test class's `List` property, `Type` to an attribute's `Type` property, and a parameter of type `TypeMap` to a `TypeMap` property instead of the `TypeMap` class. Those members showed up with hundreds of made-up dependents. Reading a property or passing a method, like `Builder.Services` or `MapGet("/x", GetItems)`, still links as before. Re-index C# and VB.NET projects after upgrading.
|
||||
- JavaScript and TypeScript calls are no longer linked to a class member that merely shares their name. A call on a namespace import, like zod's `z.string()` (`import * as z from 'zod'`), or on something imported from an npm package, used to link to whichever class in the project had a `string` member, and a bare call like mocha's `it(…)` or `describe(…)` to an interface's `it` property or a class's `describe` field. On zod one test helper's getter had thousands of made-up callers and topped the most-depended-on list. A React hook or component imported from a package, like `useQuery` from `@tanstack/react-query`, no longer links to a same-named one in the project, and a hook nested inside another function is no longer a target at all. Re-index JavaScript and TypeScript projects after upgrading.
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* A type parameter is not a project symbol that shares its name.
|
||||
*
|
||||
* cats' `def zipWith[A, B, C](fa: ISeq[A], …)` sent `A` to an `implicit def A`
|
||||
* (4,916 dependents) and `B` to a `case class B()` declared inside a test;
|
||||
* getx's `T` landed on a constant named `T`. A type declared inside a function
|
||||
* is only in scope in there.
|
||||
*/
|
||||
import { describe, it, expect, afterAll } from 'vitest';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { CodeGraph } from '../src';
|
||||
|
||||
const roots: string[] = [];
|
||||
afterAll(() => {
|
||||
for (const r of roots.splice(0)) fs.rmSync(r, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
async function project(files: Record<string, string>): Promise<CodeGraph> {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-type-params-'));
|
||||
roots.push(root);
|
||||
for (const [rel, content] of Object.entries(files)) {
|
||||
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
|
||||
fs.writeFileSync(path.join(root, rel), content);
|
||||
}
|
||||
return CodeGraph.init(root, { index: true });
|
||||
}
|
||||
|
||||
function typeTargets(cg: CodeGraph, name: string): string[] {
|
||||
const from = cg.getNodesByName(name).filter((n) => n.kind === 'function' || n.kind === 'method');
|
||||
return cg
|
||||
.getOutgoingEdgesFrom(from.map((n) => n.id), ['references', 'type_of', 'returns', 'instantiates'])
|
||||
.map((e) => cg.getNode(e.target))
|
||||
.map((n) => `${n!.kind}:${n!.name}`)
|
||||
.sort();
|
||||
}
|
||||
|
||||
describe('type parameters', () => {
|
||||
it('Scala: a def’s type parameters, and a case class local to another test', async () => {
|
||||
const cg = await project({
|
||||
'src/Inst.scala': `package app
|
||||
object Inst {
|
||||
implicit def A: Int = 1
|
||||
}
|
||||
`,
|
||||
'src/Seqs.scala': `package app
|
||||
object Seqs {
|
||||
def zipWith[A, B, C](fa: List[A], fb: List[B])(f: (A, B) => C): List[C] = ???
|
||||
}
|
||||
`,
|
||||
// Operator names are declared names too — and must not break the reading.
|
||||
'src/Ops.scala': `package app
|
||||
object Ops {
|
||||
def |+|[A](x: A, y: A): A = x
|
||||
def *[B](b: B): B = b
|
||||
}
|
||||
trait Heyting[@sp(Int, Long) A] {
|
||||
def and(a: A, b: A): A
|
||||
}
|
||||
final case class FlatMapped(f: Int => Int) {
|
||||
type A = Int
|
||||
}
|
||||
trait Laws {
|
||||
type L = Int
|
||||
}
|
||||
class MyLaws extends Laws {
|
||||
def check(x: L): Boolean = true
|
||||
}
|
||||
object UsesImport {
|
||||
import FlatMapped._
|
||||
def viaImport(x: A): Int = 1
|
||||
}
|
||||
object Folds {
|
||||
def foldMap[A](fa: List[A])(f: A => Int)(implicit G: Ordering[Int]): Int = { f(fa.head); G.max(1, 2) }
|
||||
}
|
||||
object TraverseLaws {
|
||||
def updatedRef[A, B >: A](fa: List[A], b: B): Option[B] = None
|
||||
}
|
||||
trait UnorderedTraverseTests {
|
||||
def unorderedTraverse[
|
||||
A,
|
||||
B,
|
||||
C,
|
||||
X,
|
||||
Y
|
||||
](implicit
|
||||
ArbXB: Option[X],
|
||||
ArbYB: List[B]
|
||||
): Unit = ()
|
||||
}
|
||||
`,
|
||||
'src/Elsewhere.scala': `package app
|
||||
object Elsewhere {
|
||||
def combineAll(xs: List[A]): Int = 1
|
||||
def lift(implicit F: FlatMap): Int = 1
|
||||
}
|
||||
`,
|
||||
'src/FlatMap.scala': `package app
|
||||
trait FlatMap
|
||||
`,
|
||||
'src/Eval.scala': `package app
|
||||
object Eval {
|
||||
final case class FlatMap(x: Int)
|
||||
}
|
||||
`,
|
||||
'src/SyntaxSuite.scala': `package app
|
||||
class SyntaxSuite {
|
||||
def testOptionEmpty(): Unit = {
|
||||
case class B()
|
||||
val b: B = B()
|
||||
}
|
||||
}
|
||||
`,
|
||||
});
|
||||
try {
|
||||
expect(typeTargets(cg, 'zipWith')).toEqual([]);
|
||||
expect(typeTargets(cg, '|+|')).toEqual([]);
|
||||
expect(typeTargets(cg, '*')).toEqual([]);
|
||||
// A specialized parameter of the enclosing trait.
|
||||
expect(typeTargets(cg, 'and')).toEqual([]);
|
||||
// A def's own value parameters: `f(…)` calls the argument, `G` is the implicit.
|
||||
const folds = cg.getNodesByName('foldMap')[0]!;
|
||||
const fromFolds = cg.getOutgoingEdgesFrom([folds.id], ['calls', 'references']).map((e) => cg.getNode(e.target)!.name);
|
||||
expect(fromFolds).not.toContain('f');
|
||||
expect(fromFolds).not.toContain('G');
|
||||
// Another type's member is only in scope inside it, a subtype, or after an import.
|
||||
expect(typeTargets(cg, 'combineAll')).toEqual([]);
|
||||
// …and the one in scope is found instead.
|
||||
const lift = cg.getNodesByName('lift')[0]!;
|
||||
const liftTargets = cg.getOutgoingEdgesFrom([lift.id], ['references']).map((e) => cg.getNode(e.target)!);
|
||||
expect(liftTargets.map((n) => `${n.kind}:${n.qualifiedName}`)).toContain('trait:FlatMap');
|
||||
expect(liftTargets.map((n) => n.qualifiedName)).not.toContain('Eval::FlatMap');
|
||||
expect(typeTargets(cg, 'check')).toContain('type_alias:L');
|
||||
expect(typeTargets(cg, 'viaImport')).toContain('type_alias:A');
|
||||
// A lower bound's `>:` is not a closing bracket.
|
||||
expect(typeTargets(cg, 'updatedRef')).toEqual([]);
|
||||
// A parameter list written one per line.
|
||||
expect(typeTargets(cg, 'unorderedTraverse')).toEqual([]);
|
||||
// Inside its own method, the local class is still the one meant.
|
||||
expect(typeTargets(cg, 'testOptionEmpty')).toContain('class:B');
|
||||
} finally {
|
||||
cg.close();
|
||||
}
|
||||
});
|
||||
|
||||
it('Java, TypeScript and Dart: declared before or after the name', async () => {
|
||||
const cg = await project({
|
||||
'src/T.java': `package app;
|
||||
public class T {}
|
||||
`,
|
||||
'src/Foo.java': `package app;
|
||||
public class Foo {}
|
||||
`,
|
||||
'src/Util.java': `package app;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
public class Util {
|
||||
public static <T extends Comparable<T>> T max(List<T> xs) { return null; }
|
||||
public static Map<String, Foo> index(List<Foo> xs) { return null; }
|
||||
}
|
||||
`,
|
||||
'src/wrap.ts': `export class T {}
|
||||
export function wrap<T>(x: T): T { return x; }
|
||||
`,
|
||||
'lib/first.dart': `const T = 1;
|
||||
T first<T>(List<T> xs) => xs.first;
|
||||
`,
|
||||
});
|
||||
try {
|
||||
expect(typeTargets(cg, 'max')).toEqual([]);
|
||||
// A generic type's ARGUMENTS are not parameters: `Map<String, Foo>` still names Foo.
|
||||
expect(typeTargets(cg, 'index')).toContain('class:Foo');
|
||||
expect(typeTargets(cg, 'wrap')).toEqual([]);
|
||||
expect(typeTargets(cg, 'first')).toEqual([]);
|
||||
} finally {
|
||||
cg.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -25,6 +25,7 @@ import { matchJsStoreBindingCall, isUnresolvedJsMemberCall, isVisibleAcrossFiles
|
||||
import { isVisibleCppMacro, clearCppMacroVisibility } from './cpp-macro-visibility';
|
||||
import { isCppConstructorRef, matchCppConstructor } from './cpp-constructor';
|
||||
import { gateSwiftTypeTarget, clearSwiftTypeVisibility, swiftExtendedConformances } from './swift-type-visibility';
|
||||
import { gateTypeParameter, clearTypeParameterMemos } from './type-parameters';
|
||||
import { resolveViaImport, resolvePhpImportedStaticCall, resolveJvmImport, extractImportMappings, extractReExports, loadCppIncludeDirs, isPhpIncludePathRef, isCobolCopybookRef, isNixPathImportRef, isBoundToOutOfRepoImport, clearImportResolverMemos, resolveImportPath, isExternalImport } from './import-resolver';
|
||||
import { ResolverPool, minRefsForPool } from './resolver-pool';
|
||||
import { resolveAliasBinding } from './alias-binding';
|
||||
@@ -448,6 +449,7 @@ export class ReferenceResolver {
|
||||
clearNameMatcherMemos(this.context);
|
||||
clearCppMacroVisibility(this.context);
|
||||
clearSwiftTypeVisibility(this.context);
|
||||
clearTypeParameterMemos(this.context);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -985,7 +987,13 @@ export class ReferenceResolver {
|
||||
if (isVisibleCppMacro(ref, this.context)) return null;
|
||||
// A Swift type reference never lands on an `extension X {}` node, nor on a
|
||||
// nested type it cannot name bare (see ./swift-type-visibility).
|
||||
const candidate = gateSwiftTypeTarget(this.gateTargetKind(this.resolveOneInner(ref), ref), ref, this.context);
|
||||
// A name a declaration around the reference declares as a type parameter
|
||||
// (`def f[A]`, `class Foo<T>`) is that parameter (see ./type-parameters).
|
||||
const candidate = gateTypeParameter(
|
||||
gateSwiftTypeTarget(this.gateTargetKind(this.resolveOneInner(ref), ref), ref, this.context),
|
||||
ref,
|
||||
this.context,
|
||||
);
|
||||
const resolved = candidate?.resolvedBy === 'framework'
|
||||
? this.gateFrameworkLanguage(candidate, ref)
|
||||
: this.gateLanguage(candidate, ref);
|
||||
|
||||
@@ -597,6 +597,8 @@ export function matchFunctionRef(
|
||||
|
||||
/** Languages with no nested named functions: nesting in the graph is never a scope. */
|
||||
const NO_NESTED_FUNCTIONS = new Set<string>(['c', 'cpp']);
|
||||
/** Types a function body can declare for itself. */
|
||||
const LOCAL_TYPE_KINDS = new Set<string>(['class', 'struct', 'enum', 'interface', 'trait', 'type_alias']);
|
||||
|
||||
/**
|
||||
* A function nested inside another FUNCTION is only callable from within its
|
||||
@@ -614,7 +616,9 @@ function isLexicallyReachable(
|
||||
ref: UnresolvedRef,
|
||||
context: ResolutionContext
|
||||
): boolean {
|
||||
if (candidate.kind !== 'function') return true;
|
||||
// A function — or a type (`case class B()` in a test method) — declared
|
||||
// inside a function is only in scope in there.
|
||||
if (candidate.kind !== 'function' && !LOCAL_TYPE_KINDS.has(candidate.kind)) return true;
|
||||
// C and C++ have no nested named functions, so a function the graph shows
|
||||
// inside another is an extraction artifact, not a scope: tree-sitter-c
|
||||
// cannot parse a macro call whose arguments are designated initializers
|
||||
@@ -1247,6 +1251,11 @@ export function matchByExactName(
|
||||
const candidates = sameName.filter((n) =>
|
||||
!(cMacroCall && n.kind !== 'function' && n.kind !== 'method') &&
|
||||
!(typeRef && !canNameInTypePosition(n)) &&
|
||||
// A Scala type position (`Arbitrary[B]`) never names a method: an
|
||||
// `implicit def A: Order[A]` shares its name with half of cats' type
|
||||
// parameters. Scala's value references only read a file's own vals.
|
||||
!(ref.language === 'scala' && ref.referenceKind === 'references' && /^[A-Z]/.test(ref.referenceName) &&
|
||||
(n.kind === 'method' || n.kind === 'function')) &&
|
||||
// Type/value references retain same-family eligibility: a native namesake
|
||||
// must not hide the actual web type. Calls still gate only the winner.
|
||||
(!valueRef || sameLanguageFamily(n.language, ref.language)) &&
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
/**
|
||||
* A name an enclosing declaration declares as a TYPE PARAMETER means that
|
||||
* parameter, never a project symbol that happens to share it.
|
||||
*
|
||||
* `def zipWith[A, B, C](fa: ISeq[A], …)` in cats sent its `A` to an
|
||||
* `implicit def A` elsewhere (4,916 dependents) and its `B` to a
|
||||
* `case class B()` declared inside some test; getx's `T` landed on a constant
|
||||
* named `T`. The index does not record type parameters, so they are read
|
||||
* from the declaration heads around the reference: after the declared name
|
||||
* (`class Foo<T>`, `fn f<T: Display>`, `func F[T any]`, `def f[F[_]: Monad, A]`,
|
||||
* `struct Stack<Element>`), before it where the language puts them
|
||||
* (Java's `<T extends X> T max(…)`, Kotlin's `fun <T> f()`), and a C++
|
||||
* `template <typename T>` line above it. Applied at resolveOne's seam, so every
|
||||
* strategy's result obeys it.
|
||||
*/
|
||||
import type { Node } from '../types';
|
||||
import type { ResolutionContext, ResolvedRef, UnresolvedRef } from './types';
|
||||
|
||||
const GENERIC_LANGUAGES: ReadonlySet<string> = new Set([
|
||||
'scala', 'java', 'kotlin', 'csharp', 'typescript', 'tsx', 'swift', 'rust', 'dart', 'go', 'cpp', 'vbnet', 'arkts',
|
||||
]);
|
||||
|
||||
/** Where a reference names a type. */
|
||||
const TYPE_REF_KINDS: ReadonlySet<string> = new Set(['references', 'type_of', 'returns', 'instantiates']);
|
||||
|
||||
/** Declarations that can carry type parameters. */
|
||||
const DECLARATION_KINDS: ReadonlySet<string> = new Set([
|
||||
'class', 'struct', 'interface', 'trait', 'protocol', 'enum', 'type_alias', 'function', 'method', 'union', 'module',
|
||||
]);
|
||||
|
||||
/** How far past a declaration's first line its head can run: a parameter list may take a line per parameter. */
|
||||
const HEAD_LINES = 40;
|
||||
|
||||
const memos = new WeakMap<ResolutionContext, Map<string, ReadonlySet<string>>>();
|
||||
|
||||
/** Drop the memos (see ReferenceResolver.clearCaches). */
|
||||
export function clearTypeParameterMemos(context: ResolutionContext): void {
|
||||
memos.delete(context);
|
||||
valueMemos.delete(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* The resolved reference, or null when a declaration around it declares its
|
||||
* name: as a type parameter (a type reference), or — Scala — as a value
|
||||
* parameter of the enclosing `def` (`f(a)` where `f: A => B` is the def's own
|
||||
* argument; cats bound 846 such calls to a case class's `f` field).
|
||||
*/
|
||||
export function gateTypeParameter(resolved: ResolvedRef | null, ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
|
||||
if (!resolved) return resolved;
|
||||
let result = resolved;
|
||||
if (ref.language === 'scala' && (ref.referenceKind === 'calls' || ref.referenceKind === 'references') &&
|
||||
/^[A-Za-z_]\w*$/.test(ref.referenceName)) {
|
||||
for (const declaration of enclosingDeclarations(ref, context)) {
|
||||
if ((declaration.kind === 'function' || declaration.kind === 'method') &&
|
||||
declaredValueParameters(declaration, context).has(ref.referenceName)) return null;
|
||||
}
|
||||
}
|
||||
if (ref.language === 'scala' && !scalaMemberInScope(result, ref, context)) {
|
||||
// Out of scope: the type of that name that IS in scope — cats' own
|
||||
// `trait FlatMap`, not `Eval`'s nested `FlatMap` case class — when one fits.
|
||||
const target = context.getNodeById?.(result.targetNodeId);
|
||||
if (!target || !SCALA_TYPE_KINDS.has(target.kind)) return null;
|
||||
const inScope = context
|
||||
.getNodesByName(ref.referenceName)
|
||||
.filter((n) => n.id !== target.id && n.language === 'scala' && SCALA_TYPE_KINDS.has(n.kind))
|
||||
.filter((n) => scalaMemberInScope({ ...result, targetNodeId: n.id }, ref, context));
|
||||
const chosen = inScope.length === 1 ? inScope[0]! : inScope.find((n) => n.filePath === ref.filePath);
|
||||
if (!chosen) return null;
|
||||
result = { ...result, targetNodeId: chosen.id };
|
||||
}
|
||||
if (!GENERIC_LANGUAGES.has(ref.language) || !TYPE_REF_KINDS.has(ref.referenceKind)) return result;
|
||||
const name = ref.referenceName;
|
||||
if (!/^[A-Z][A-Za-z0-9_]*$/.test(name)) return result;
|
||||
for (const declaration of enclosingDeclarations(ref, context)) {
|
||||
if (declaredTypeParameters(declaration, context).has(name)) return null;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const SCALA_OWNER_KINDS: ReadonlySet<string> = new Set(['class', 'trait', 'module', 'interface', 'struct', 'enum']);
|
||||
const SCALA_TYPE_KINDS: ReadonlySet<string> = new Set(['class', 'trait', 'interface', 'struct', 'enum', 'type_alias']);
|
||||
|
||||
/**
|
||||
* A bare Scala name reaches a member of another type — `FreeT.FlatMapped`'s
|
||||
* `type A`, a test suite's `val P` — only from inside that type, a type that
|
||||
* extends it, or a file importing its members (`import Owner._`,
|
||||
* `Owner.{A}`, `Owner.*`). cats' bare type parameters landed on such members
|
||||
* across the library once the test-local classes stopped catching them.
|
||||
*/
|
||||
function scalaMemberInScope(result: ResolvedRef, ref: UnresolvedRef, context: ResolutionContext): boolean {
|
||||
if (!/^[A-Za-z_]\w*$/.test(ref.referenceName) || ref.referenceKind !== 'references') return true;
|
||||
// Bare in the SOURCE: the index keeps `SttpClient.Service` by its last
|
||||
// segment, and a qualified name reaches any member.
|
||||
const line = context.getFileLines?.(ref.filePath)?.[ref.line - 1] ?? context.readFile(ref.filePath)?.split(/\r?\n/)[ref.line - 1];
|
||||
if (line === undefined || !line.startsWith(ref.referenceName, ref.column) || /\.\s*$/.test(line.slice(0, ref.column))) return true;
|
||||
const target = context.getNodeById?.(result.targetNodeId);
|
||||
if (!target || target.language !== 'scala') return true;
|
||||
const cut = target.qualifiedName.lastIndexOf('::');
|
||||
if (cut < 0) return true;
|
||||
const ownerQn = target.qualifiedName.slice(0, cut);
|
||||
const containers = context
|
||||
.getNodesByQualifiedName(ownerQn)
|
||||
.filter((o) => o.filePath === target.filePath && o.startLine <= target.startLine && o.endLine >= target.endLine);
|
||||
const inside = (o: Node): boolean => ref.filePath === o.filePath && ref.line >= o.startLine && ref.line <= o.endLine;
|
||||
// A def's local (`case class B()` in a test method): only in there.
|
||||
const def = containers.find((o) => o.kind === 'function' || o.kind === 'method');
|
||||
if (def) return inside(def);
|
||||
const owner = containers.find((o) => SCALA_OWNER_KINDS.has(o.kind));
|
||||
if (!owner) return true; // a package member
|
||||
if (inside(owner)) return true;
|
||||
// Inherited: the reference sits in a type that extends the owner.
|
||||
for (const around of enclosingDeclarations(ref, context)) {
|
||||
if (!SCALA_OWNER_KINDS.has(around.kind)) continue;
|
||||
if (around.name === owner.name) return true;
|
||||
if (context.getSupertypes?.(around.name, 'scala').includes(owner.name)) return true;
|
||||
}
|
||||
// Imported: `import cats.data.FreeT.FlatMapped._`, `Owner.{A, B}`, `Owner.*`.
|
||||
const text = context.readFile(ref.filePath) ?? '';
|
||||
const imports = new RegExp(
|
||||
String.raw`\bimport\s+[\w.]*\b${owner.name}\.(?:_|\*|given\b|\{[^}]*\b(?:_|\*|${target.name})\b[^}]*\}|${target.name}\b)`,
|
||||
);
|
||||
return /^[A-Za-z_]\w*$/.test(owner.name) && /^[A-Za-z_]\w*$/.test(target.name) ? imports.test(text) : true;
|
||||
}
|
||||
|
||||
function enclosingDeclarations(ref: UnresolvedRef, context: ResolutionContext): Node[] {
|
||||
return context
|
||||
.getNodesInFile(ref.filePath)
|
||||
.filter((n) => DECLARATION_KINDS.has(n.kind) && n.startLine <= ref.line && n.endLine >= ref.line);
|
||||
}
|
||||
|
||||
function declaredTypeParameters(node: Node, context: ResolutionContext): ReadonlySet<string> {
|
||||
let memo = memos.get(context);
|
||||
if (!memo) {
|
||||
memo = new Map();
|
||||
memos.set(context, memo);
|
||||
}
|
||||
const hit = memo.get(node.id);
|
||||
if (hit) return hit;
|
||||
const names = new Set<string>();
|
||||
const lines = context.getFileLines?.(node.filePath) ?? context.readFile(node.filePath)?.split(/\r?\n/) ?? null;
|
||||
if (lines) {
|
||||
const head = lines.slice(node.startLine - 1, Math.min(node.endLine, node.startLine - 1 + HEAD_LINES)).join('\n');
|
||||
const at = nameIndex(head, node.name);
|
||||
if (at >= 0) {
|
||||
// After the declared name: `Foo<T>`, `f[A]`.
|
||||
let open = at + node.name.length;
|
||||
while (open < head.length && /[ \t]/.test(head[open]!)) open++;
|
||||
if (head[open] === '<' || head[open] === '[') collect(head, open, names);
|
||||
// Before it, detached from any type name: `static <T> T max(`, `fun <T> f(`.
|
||||
const before = head.slice(0, at);
|
||||
for (let i = 0; i < before.length; i++) {
|
||||
if (before[i] === '<' && (i === 0 || /[\s(,]/.test(before[i - 1]!))) collect(before, i, names);
|
||||
}
|
||||
}
|
||||
// C++: `template <typename T, class U>` on the lines just above.
|
||||
if (node.language === 'cpp') {
|
||||
const above = lines.slice(Math.max(0, node.startLine - 3), node.startLine).join('\n');
|
||||
const template = /\btemplate\s*</g;
|
||||
for (let m = template.exec(above); m; m = template.exec(above)) collect(above, m.index + m[0].length - 1, names);
|
||||
}
|
||||
}
|
||||
memo.set(node.id, names);
|
||||
return names;
|
||||
}
|
||||
|
||||
const valueMemos = new WeakMap<ResolutionContext, Map<string, ReadonlySet<string>>>();
|
||||
|
||||
/**
|
||||
* A Scala def's value parameters, every curried list: `def f[A](fa: F[A])(f:
|
||||
* A => B)(implicit G: Applicative[G])` → fa, f, G.
|
||||
*/
|
||||
function declaredValueParameters(node: Node, context: ResolutionContext): ReadonlySet<string> {
|
||||
let memo = valueMemos.get(context);
|
||||
if (!memo) {
|
||||
memo = new Map();
|
||||
valueMemos.set(context, memo);
|
||||
}
|
||||
const hit = memo.get(node.id);
|
||||
if (hit) return hit;
|
||||
const names = new Set<string>();
|
||||
const lines = context.getFileLines?.(node.filePath) ?? context.readFile(node.filePath)?.split(/\r?\n/) ?? null;
|
||||
if (lines) {
|
||||
const head = lines.slice(node.startLine - 1, Math.min(node.endLine, node.startLine - 1 + HEAD_LINES)).join('\n');
|
||||
const at = nameIndex(head, node.name);
|
||||
if (at >= 0) {
|
||||
let i = at + node.name.length;
|
||||
const skipSpace = (): void => {
|
||||
while (i < head.length && /\s/.test(head[i]!)) i++;
|
||||
};
|
||||
skipSpace();
|
||||
if (head[i] === '[') {
|
||||
const close = matchingBracket(head, i);
|
||||
i = close < 0 ? head.length : close + 1;
|
||||
}
|
||||
for (skipSpace(); head[i] === '('; skipSpace()) {
|
||||
const close = matchingBracket(head, i);
|
||||
if (close < 0) break;
|
||||
for (const item of splitTopLevel(head.slice(i + 1, close), false)) {
|
||||
const bare = item.replace(/^\s*(?:@[\w.]+(?:\([^()]*\))?\s*)+/, '');
|
||||
const param = /^\s*(?:(?:implicit|using|val|var|override|private|protected|final|inline|erased)\s+)*([A-Za-z_]\w*)\s*:/.exec(bare)?.[1];
|
||||
if (param) names.add(param);
|
||||
}
|
||||
i = close + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
memo.set(node.id, names);
|
||||
return names;
|
||||
}
|
||||
|
||||
/**
|
||||
* The declared name's position in its head: the whole word, past attributes
|
||||
* and modifiers. A plain search — a declared name can be an operator (Scala's
|
||||
* `*`, `|+|`), which no pattern built from it could match safely.
|
||||
*/
|
||||
function nameIndex(head: string, name: string): number {
|
||||
if (!name) return -1;
|
||||
const word = /[\w$]/;
|
||||
for (let at = head.indexOf(name); at >= 0; at = head.indexOf(name, at + 1)) {
|
||||
const before = at > 0 ? head[at - 1]! : '';
|
||||
const after = head[at + name.length] ?? '';
|
||||
if (!word.test(before) && !word.test(after)) return at;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/** Add the parameter names of the bracket group opening at `open`. */
|
||||
function collect(text: string, open: number, names: Set<string>): void {
|
||||
const close = matchingBracket(text, open);
|
||||
if (close < 0) return;
|
||||
for (const item of splitTopLevel(text.slice(open + 1, close), text[open] === '<')) {
|
||||
// Past annotations (`@sp(Int, Long) A`, `@NonNull T`), variance and keywords.
|
||||
const bare = item.replace(/^\s*(?:@[\w.]+(?:\([^()]*\))?\s*)+/, '');
|
||||
const param = /^\s*(?:(?:in|out|reified|const|typename|class|struct)\s+|[+\-']\s*)*([A-Za-z_]\w*)/.exec(bare)?.[1];
|
||||
if (param && param !== 'typename' && param !== 'class') names.add(param);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The bracket closing the one at `open`. A `[…]` list counts only square
|
||||
* brackets and parentheses — Scala's bounds `B >: A` and `A <: AnyRef` are not
|
||||
* angle brackets — and a `<…>` list skips the `->` / `=>` inside its bounds.
|
||||
*/
|
||||
function matchingBracket(text: string, open: number): number {
|
||||
const angle = text[open] === '<';
|
||||
const closer = angle ? '>' : text[open] === '[' ? ']' : ')';
|
||||
let depth = 0;
|
||||
for (let i = open; i < text.length; i++) {
|
||||
const c = text[i]!;
|
||||
if (c === '{' || c === ';') return -1;
|
||||
if (isOpener(c, angle)) depth++;
|
||||
else if (isCloser(c, angle, text[i - 1])) {
|
||||
depth--;
|
||||
if (depth === 0) return c === closer ? i : -1;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function splitTopLevel(inner: string, angle: boolean): string[] {
|
||||
const out: string[] = [];
|
||||
let depth = 0;
|
||||
let start = 0;
|
||||
for (let i = 0; i < inner.length; i++) {
|
||||
const c = inner[i]!;
|
||||
if (isOpener(c, angle)) depth++;
|
||||
else if (isCloser(c, angle, inner[i - 1])) depth--;
|
||||
else if (c === ',' && depth === 0) {
|
||||
out.push(inner.slice(start, i));
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
out.push(inner.slice(start));
|
||||
return out;
|
||||
}
|
||||
|
||||
function isOpener(c: string, angle: boolean): boolean {
|
||||
return c === '[' || c === '(' || (angle && c === '<');
|
||||
}
|
||||
|
||||
function isCloser(c: string, angle: boolean, previous: string | undefined): boolean {
|
||||
if (c === ']' || c === ')') return true;
|
||||
return angle && c === '>' && previous !== '-' && previous !== '=';
|
||||
}
|
||||
Reference in New Issue
Block a user