mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-23 01:59:06 +08:00
fix(ts): an array method on a local annotated as an array of a class links to nothing, not the class's method (#2639)
`items: Item[]` was read as an `Item`: the annotation's capture stopped at `[`, so `items.find(…)` linked to `Item.find` whenever the class declares a method of that name. On vscode that was 16 wrong edges, like `distinctElements.find(…)` on an `ExplorerItem[]` reaching `ExplorerItem.find` and `ranges.map(…)` on an `OffsetRange[]` reaching `OffsetRange.map`. The annotation pattern now captures a bracket suffix with the type. On an array of a type, a method arrays have links to nothing and isn't matched by its name either. Any other method can't be called on the array, so it is called on an element that shares its name, as in `d.forEach(d => d.dispose())`, and keeps the element's type as before. An indexed type (`Element["groupIds"]`) is still read as the type it indexes. The sync parking that came with the previous change now reads arrays from the type inference gives instead of re-reading the declaration line: an array method on an array keeps its member's tail, and an element's or an indexed type's call waits for that type's module. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -23,7 +23,9 @@ import {
|
||||
normalizeInferredTypeName,
|
||||
goRefQualifier,
|
||||
inferLocalReceiverType,
|
||||
arrayElementType,
|
||||
} from './name-matcher';
|
||||
import { JS_ARRAY_METHODS } from './js-builtins';
|
||||
|
||||
/**
|
||||
* Extension resolution order by language
|
||||
@@ -3270,34 +3272,22 @@ export function pythonModulePath(source: string, fromFile: string): string | nul
|
||||
* way resolution reads it for the call (inferLocalReceiverType, memoized per
|
||||
* file, scope and receiver, so a call the matcher just tried costs a lookup):
|
||||
* the binding the type is written through, `LRUCache` of `new LRUCache<K,
|
||||
* V>(…)` or `lib` of `new lib.LRUCache()`. Undefined for a type the file
|
||||
* doesn't import, in a file that imports nothing, and for a local declared as
|
||||
* an array of the type (`results: URI[]`). Resolution reads the element type
|
||||
* there too, but the call is the array's (`results.push(…)`), which no edit
|
||||
* of the type's module links: such calls were most of what this parked on
|
||||
* vscode, and retrying them nearly doubled what an edit of `uri.ts` retried.
|
||||
* V>(…)` or `lib` of `new lib.LRUCache()`, of the element type of an array
|
||||
* (`results: URI[]`), or of the type whose property it is (`Emitter<E>['event']`).
|
||||
* Undefined for a type the file doesn't import, in a file that imports
|
||||
* nothing, and for a method arrays have called on an array (`results.push(…)`):
|
||||
* resolution links that to no project class, so no edit of the type's module
|
||||
* does. Such calls were most of what this parked on vscode, and retrying them
|
||||
* nearly doubled what an edit of `uri.ts` retried.
|
||||
*/
|
||||
function localTypeBinding(receiver: string, ref: UnresolvedRef, context: ResolutionContext, mappings: ImportMapping[]): ImportMapping | undefined {
|
||||
if (mappings.length === 0) return undefined;
|
||||
const decl: { raw?: string; line?: number } = {};
|
||||
if (!inferLocalReceiverType(receiver, ref, context, decl) || !decl.raw) return undefined;
|
||||
const head = decl.raw.split('.')[0];
|
||||
const binding = mappings.find((m) => m.localName === head);
|
||||
if (!binding || decl.line === undefined) return binding;
|
||||
const lines = context.getFileLines?.(ref.filePath) ?? context.readFile(ref.filePath)?.split(/\r?\n/);
|
||||
const declaration = lines?.[decl.line];
|
||||
return declaration !== undefined && declaresArrayOf(declaration, receiver, decl.raw) ? undefined : binding;
|
||||
}
|
||||
|
||||
/** A `name: Type` annotation, the type's arguments, and the `[` of an array of it: `items: Item<K, V>[]`. */
|
||||
const ANNOTATION = /([A-Za-z_$][\w$]*)\s*:\s*([A-Za-z_$][\w.$]*)\s*(?:<[^;=(){}<>]*(?:<[^;=(){}<>]*>[^;=(){}<>]*)*>)?\s*(\[)?/g;
|
||||
|
||||
/** Whether `line` annotates `name` as an array of `type`: `items: Item[]`, `items: Item<K>[] = []`. */
|
||||
function declaresArrayOf(line: string, name: string, type: string): boolean {
|
||||
for (const m of line.matchAll(ANNOTATION)) {
|
||||
if (m[1] === name && m[2] === type) return m[3] !== undefined;
|
||||
}
|
||||
return false;
|
||||
const decl: { raw?: string } = {};
|
||||
const type = inferLocalReceiverType(receiver, ref, context, decl);
|
||||
if (!type || !decl.raw) return undefined;
|
||||
if (arrayElementType(type) !== undefined && JS_ARRAY_METHODS.has(ref.referenceName.slice(receiver.length + 1))) return undefined;
|
||||
const head = decl.raw.split(/[.<[\s]/)[0];
|
||||
return mappings.find((m) => m.localName === head);
|
||||
}
|
||||
|
||||
const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
|
||||
|
||||
@@ -33,6 +33,21 @@ export const JS_BUILTIN_METHODS = new Set([
|
||||
'text', 'json', 'arrayBuffer', 'blob', 'formData',
|
||||
]);
|
||||
|
||||
/**
|
||||
* The methods an array has: `Array.prototype`'s and those every object
|
||||
* inherits. On a local annotated as an array (`items: Item[]`) a call of one
|
||||
* is the array's, never a method of the element's class.
|
||||
*/
|
||||
export const JS_ARRAY_METHODS = new Set([
|
||||
'at', 'concat', 'copyWithin', 'entries', 'every', 'fill', 'filter', 'find',
|
||||
'findIndex', 'findLast', 'findLastIndex', 'flat', 'flatMap', 'forEach',
|
||||
'includes', 'indexOf', 'join', 'keys', 'lastIndexOf', 'map', 'pop', 'push',
|
||||
'reduce', 'reduceRight', 'reverse', 'shift', 'slice', 'some', 'sort', 'splice',
|
||||
'toReversed', 'toSorted', 'toSpliced', 'unshift', 'values', 'with',
|
||||
'toString', 'toLocaleString', 'valueOf', 'hasOwnProperty', 'isPrototypeOf',
|
||||
'propertyIsEnumerable',
|
||||
]);
|
||||
|
||||
/**
|
||||
* TypeScript primitive type names. Distinct from JS_BUILT_INS on purpose: those
|
||||
* are runtime globals a receiver can be constructed from, these only ever come
|
||||
|
||||
@@ -9,7 +9,7 @@ import * as path from 'path';
|
||||
import { Language, Node } from '../types';
|
||||
import { UnresolvedRef, ResolvedRef, ResolutionContext, ImportMapping, isSupertypeTarget, CPP_DEFINE_SIGNATURE, isInheritanceRef, isImportableKind } from './types';
|
||||
import { blankStringContents, stripCommentsForRegex } from './strip-comments';
|
||||
import { JS_BUILT_INS, JS_BUILTIN_METHODS, TS_PRIMITIVE_TYPES } from './js-builtins';
|
||||
import { JS_ARRAY_METHODS, JS_BUILT_INS, JS_BUILTIN_METHODS, TS_PRIMITIVE_TYPES } from './js-builtins';
|
||||
import { SWIFT_TYPE_PATH_CALL, resolveSwiftTypePathCall } from './swift-type-visibility';
|
||||
import { dartImportPrefixes, dartLibrarySees, dartPrefixSees, inSameDartLibrary } from './dart-libraries';
|
||||
import { isDartLocallyBound } from './dart-local-scope';
|
||||
@@ -12847,6 +12847,11 @@ export function normalizeInferredTypeName(raw: string): string | null {
|
||||
return seg;
|
||||
}
|
||||
|
||||
/** The element type of an array type a TS annotation gave (`Item[]`, `Item []` → `Item`), or undefined. */
|
||||
export function arrayElementType(type: string): string | undefined {
|
||||
return /^(.+?)\s*\[\s*\]$/.exec(type)?.[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* A Java / C# type's optional type arguments (one level of nesting) and array
|
||||
* ranks, as a regex source: `<L, R>`, `<string, List<int>>`, `[]`, `[,]`.
|
||||
@@ -13061,7 +13066,11 @@ function buildLocalReceiverTypePatterns(language: Language, r: string): RegExp[]
|
||||
// at `<` so a generic-typed param (`repo: Repository<User>`) still yields
|
||||
// `Repository`. resolveMethodOnType validates the type actually declares
|
||||
// the method, so the looser match produces no edge on a mis-inference.
|
||||
new RegExp(`\\b${r}\\b\\s*:\\s*([A-Z][\\w.$]*)`), // lg: Logger (annotation or typed param)
|
||||
// A type followed by brackets is captured whole, so matchMethodCall
|
||||
// can tell an array of it (`items: Item[]`, `items: Item<K>[]`), whose
|
||||
// own methods aren't an element's (arrayElementType), from one of its
|
||||
// properties (`groupIds: Element["groupIds"]`).
|
||||
new RegExp(`\\b${r}\\b\\s*:\\s*([A-Z][\\w.$]*(?:(?:\\s*<[^;=(){}<>]*(?:<[^;=(){}<>]*>[^;=(){}<>]*)*>)?\\s*\\[[^\\]]*\\])?)`), // lg: Logger (annotation or typed param)
|
||||
];
|
||||
case 'python':
|
||||
return [
|
||||
@@ -13713,10 +13722,7 @@ export function inferLocalReceiverType(
|
||||
const type = matchLine(i);
|
||||
if (type) {
|
||||
states.set(key, { hi: callIdx, ansIdx: i, ansType: type, ansRaw: lastRaw });
|
||||
if (decl) {
|
||||
decl.raw = lastRaw;
|
||||
decl.line = i;
|
||||
}
|
||||
if (decl) decl.raw = lastRaw;
|
||||
return type;
|
||||
}
|
||||
}
|
||||
@@ -13735,19 +13741,13 @@ export function inferLocalReceiverType(
|
||||
}
|
||||
state.hi = callIdx;
|
||||
if (state.ansIdx < startIdx) return null;
|
||||
if (decl) {
|
||||
decl.raw = state.ansRaw;
|
||||
decl.line = state.ansIdx;
|
||||
}
|
||||
if (decl) decl.raw = state.ansRaw;
|
||||
return state.ansType;
|
||||
}
|
||||
for (let i = callIdx; i >= startIdx; i--) {
|
||||
const type = matchLine(i);
|
||||
if (type) {
|
||||
if (decl) {
|
||||
decl.raw = lastRaw;
|
||||
decl.line = i;
|
||||
}
|
||||
if (decl) decl.raw = lastRaw;
|
||||
return type;
|
||||
}
|
||||
}
|
||||
@@ -14211,6 +14211,18 @@ export function matchMethodCall(
|
||||
if (!awaited.name || TS_PRIMITIVE_TYPES.has(awaited.name)) return null;
|
||||
inferredType = awaited.name;
|
||||
}
|
||||
// A TS annotation with brackets after the type. An array of it (`items:
|
||||
// Item[]`): a method arrays have is the array's, which no project class
|
||||
// declares, where `distinctElements.find(…)` on vscode's `ExplorerItem[]`
|
||||
// reached `ExplorerItem.find`; any other is called on one of its elements
|
||||
// under the same name, as in `d.forEach(d => d.dispose())`, so it takes
|
||||
// the element's type. A type's property (`Emitter<E>['event']`,
|
||||
// `Parameters<typeof f>[0]`) is still read as the type.
|
||||
if (inferredType && ESM_FAMILY.has(ref.language) && inferredType.endsWith(']')) {
|
||||
const element = arrayElementType(inferredType);
|
||||
if (element !== undefined && JS_ARRAY_METHODS.has(methodName!)) return null;
|
||||
inferredType = element ?? inferredType.replace(/\s*\[[^\]]*\]$/, '');
|
||||
}
|
||||
if (inferredType) {
|
||||
// A Go local bound from a type assertion calls a method of the type Go
|
||||
// finds for it, or nothing the project declares: `flusher.Flush()` after
|
||||
@@ -14965,16 +14977,13 @@ interface GoProjectType {
|
||||
|
||||
/**
|
||||
* What inferLocalReceiverType tells its caller besides the type: the type as
|
||||
* the declaration spells it (`raw`), the 0-based index of the line it was
|
||||
* read from (`line`, set with `raw` by the line scan), and for a Go local
|
||||
* bound from a type assertion, the type goWrittenType finds for it
|
||||
* (`asserted`; null for one with no method the project declares). `asserted`
|
||||
* is unset when the local is declared some other way, or goWrittenType can't
|
||||
* tell its type.
|
||||
* the declaration spells it (`raw`), and for a Go local bound from a type
|
||||
* assertion, the type goWrittenType finds for it (`asserted`; null for one
|
||||
* with no method the project declares). `asserted` is unset when the local
|
||||
* is declared some other way, or goWrittenType can't tell its type.
|
||||
*/
|
||||
interface GoReceiverDecl {
|
||||
raw?: string;
|
||||
line?: number;
|
||||
asserted?: GoProjectType | null;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user