mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-27 12:57:10 +08:00
fix(dart): an annotation links the constant or constructor it names, even behind a comment (#2390)
A Dart annotation is a constant expression: a const variable (`@riverpod`, `@meta.immutable`, a type's static constant or enum value) or a call of a const constructor (`@Riverpod(keepAlive: true)`, `@Foo.named(…)`). It was ranked like a Python decorator, which favours a function or method of that name over a class and scores a constant not at all, so riverpod's 506 `@riverpod` annotations went to riverpod_analyzer_utils' extension getter `riverpod` (0.4) instead of riverpod_annotation's `const riverpod = Riverpod();`. matchDartAnnotation (name-matcher.ts) now takes every Dart `decorates` ref before any other strategy, and nothing falls through. It reads the annotation as written from its `@` and links a constant or (called) a class the file's library can see, through the import prefix it is written with (isDartImportPrefix, dart-libraries.ts); a type's own static constant, enum value or (called) named constructor (isDartConstructor); or, on a member, a static constant of the type around it. Never a method, getter or function. The decorator scan also stopped at the first sibling that was not an annotation, so a comment between an annotation and its declaration (`@override` `// ignore: must_call_super` `void f()`, riverpod_lint's `@riverpod` `// expect_lint: …` fixtures) hid the annotation. Dart's decoratorStepOverTypes lists its two comment kinds, which the scan now steps over, and the kernel's extract_decorators_for steps over the same. Validated on rrousselGit/riverpod @4ba1be2 and felangel/bloc @b9be1e2 with the kernel loaded: riverpod's 506 `@riverpod` edges move to `const riverpod` and 16 annotations behind a comment add an edge each, with nothing removed; bloc gains 7 `@override` refs and no edge changes. Kernel and wasm full-index dumps are byte-identical, and the parity sweeps show 0 diffs. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
631df0138e
commit
d2fb323e51
@@ -539,6 +539,10 @@ export const dartExtractor: LanguageExtractor = {
|
||||
// this: its annotations open its own node, and the dartdoc above them is
|
||||
// that node's previous sibling.
|
||||
docstringStepOverTypes: ['annotation'],
|
||||
// An annotation belongs to the next declaration, whatever comments come
|
||||
// between them: `@override` `// ignore: must_call_super` `void f()`, or a
|
||||
// dartdoc written below the annotations. The decorator scan steps over them.
|
||||
decoratorStepOverTypes: ['comment', 'documentation_comment'],
|
||||
resolveBody: (node, bodyField) => {
|
||||
// Dart: function_body is a next sibling of function_signature/method_signature
|
||||
if (node.type === 'function_signature' || node.type === 'method_signature') {
|
||||
|
||||
@@ -181,6 +181,13 @@ export interface LanguageExtractor {
|
||||
* each one is stepped over, and comments on either side of it still join.
|
||||
*/
|
||||
docstringStepOverTypes?: string[];
|
||||
/**
|
||||
* Node types that may stand between a declaration and the decorators written
|
||||
* before it without ending the scan for them. Dart writes comments there
|
||||
* (`@override` `// ignore: must_call_super` `void f()`), and an annotation
|
||||
* always belongs to the declaration that follows it.
|
||||
*/
|
||||
decoratorStepOverTypes?: string[];
|
||||
/** Extract visibility from node */
|
||||
getVisibility?: (node: SyntaxNode) => 'public' | 'private' | 'protected' | 'internal' | undefined;
|
||||
/** Check if node is exported */
|
||||
|
||||
@@ -6213,8 +6213,10 @@ export class TreeSitterExtractor {
|
||||
//
|
||||
// A grammar that wraps the declaration (Dart's `declaration`
|
||||
// around a member with no body) puts the annotations before the
|
||||
// wrapper, so the scan starts there.
|
||||
// wrapper, so the scan starts there. One that lets comments sit
|
||||
// between the annotations and the declaration (Dart) steps over them.
|
||||
const anchor = this.extractor?.getDeclarationWrapper?.(declNode) ?? declNode;
|
||||
const stepOver = this.extractor?.decoratorStepOverTypes;
|
||||
const parent = anchor.parent;
|
||||
if (parent) {
|
||||
const declStart = anchor.startIndex;
|
||||
@@ -6230,6 +6232,7 @@ export class TreeSitterExtractor {
|
||||
for (let j = declIdx - 1; j >= 0; j--) {
|
||||
const sibling = parent.namedChild(j);
|
||||
if (!sibling) continue;
|
||||
if (stepOver?.includes(sibling.type)) continue;
|
||||
if (sibling.type !== 'decorator' && sibling.type !== 'annotation' && sibling.type !== 'marker_annotation') {
|
||||
break; // non-decorator separator → stop consuming
|
||||
}
|
||||
|
||||
@@ -769,3 +769,14 @@ export function inSameDartLibrary(fromFile: string, declFile: string, context: R
|
||||
const scope = scopeOf(fromFile, context, memoFor(context));
|
||||
return scope !== null && scope.own.has(declFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether `name` is an import prefix where `fromFile` is written: an import
|
||||
* of its library is `as name` (`p` in `p.Foo`, not the `Foo` of `Foo.bar`).
|
||||
* Where the library is unknown, the file's own imports say.
|
||||
*/
|
||||
export function isDartImportPrefix(fromFile: string, name: string, context: ResolutionContext): boolean {
|
||||
const memo = memoFor(context);
|
||||
const importers = scopeOf(fromFile, context, memo)?.importers ?? [fromFile];
|
||||
return importers.some((file) => (directivesOf(file, context, memo)?.imports ?? []).some((d) => d.prefix === name));
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
isImportableKind,
|
||||
CPP_DEFINE_SIGNATURE,
|
||||
} from './types';
|
||||
import { isPythonSelfCall, matchJsStoreBindingCall, isUnresolvedJsMemberCall, matchObjectPathCall, thisScopeCaller, isVisibleAcrossFiles, matchReference, matchFunctionRef, matchDottedCallChain, matchScopedCallChain, matchMethodCall, sameLanguageFamily, crossesCodeBoundary, gateLanguageMatch, dumpNameMatcherProfile, clearNameMatcherMemos, isRustNameInScope, CASE_INSENSITIVE_LANGUAGES, isDartMemberRead, matchDartMemberRead, isDartChainLink, matchDartChainLink, isStdMethodName } from './name-matcher';
|
||||
import { isPythonSelfCall, matchJsStoreBindingCall, isUnresolvedJsMemberCall, matchObjectPathCall, thisScopeCaller, isVisibleAcrossFiles, matchReference, matchFunctionRef, matchDottedCallChain, matchScopedCallChain, matchMethodCall, sameLanguageFamily, crossesCodeBoundary, gateLanguageMatch, dumpNameMatcherProfile, clearNameMatcherMemos, isRustNameInScope, CASE_INSENSITIVE_LANGUAGES, isDartMemberRead, matchDartMemberRead, isDartChainLink, matchDartChainLink, isDartAnnotation, matchDartAnnotation, isStdMethodName } from './name-matcher';
|
||||
import { isVisibleCppMacro, clearCppMacroVisibility } from './cpp-macro-visibility';
|
||||
import { isCppConstructorRef, matchCppConstructor } from './cpp-constructor';
|
||||
import { gateSwiftTypeTarget, clearSwiftTypeVisibility, swiftExtendedConformances } from './swift-type-visibility';
|
||||
@@ -1149,6 +1149,9 @@ export class ReferenceResolver {
|
||||
// `events.map(f).transform(…)`), which arrives by its bare name: a member
|
||||
// of what the chain's head and links are declared to be, or nothing (#750).
|
||||
if (isDartChainLink(ref, this.context)) return matchDartChainLink(ref, this.context);
|
||||
// A Dart annotation (`@riverpod`, `@Riverpod(…)`) is a constant or a
|
||||
// constructor call, as written — never a method or function by its name.
|
||||
if (isDartAnnotation(ref)) return matchDartAnnotation(ref, this.context);
|
||||
|
||||
// Skip built-in/external references
|
||||
if (this.isBuiltInOrExternal(ref)) {
|
||||
|
||||
@@ -11,7 +11,7 @@ import { UnresolvedRef, ResolvedRef, ResolutionContext, ImportMapping, isSuperty
|
||||
import { blankStringContents, stripCommentsForRegex } from './strip-comments';
|
||||
import { JS_BUILT_INS, JS_BUILTIN_METHODS, TS_PRIMITIVE_TYPES } from './js-builtins';
|
||||
import { SWIFT_TYPE_PATH_CALL, resolveSwiftTypePathCall } from './swift-type-visibility';
|
||||
import { dartLibrarySees, dartPrefixSees, inSameDartLibrary } from './dart-libraries';
|
||||
import { dartLibrarySees, dartPrefixSees, inSameDartLibrary, isDartImportPrefix } from './dart-libraries';
|
||||
import { breakVbTie, isVbMemberInScope, isVbNestedTypeInScope, isVbTypeQualifiedBy, matchVbTypedCall, preferVbProject, sameVbProject } from './vbnet-receivers';
|
||||
import { isTestPath } from '../search/query-utils';
|
||||
import { isMinifiedContent } from '../extraction/generated-detection';
|
||||
@@ -4049,6 +4049,130 @@ export function matchDartMemberRead(ref: UnresolvedRef, context: ResolutionConte
|
||||
return { original: ref, targetNodeId: found.node.id, confidence: 0.9, resolvedBy: 'instance-method', edgeKind: 'calls' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a ref is a Dart annotation — `@riverpod`, `@Riverpod(keepAlive:
|
||||
* true)`, `@meta.immutable` — which the extractor records by its last name, at
|
||||
* the `@`.
|
||||
*/
|
||||
export function isDartAnnotation(ref: UnresolvedRef): boolean {
|
||||
return ref.language === 'dart' && ref.referenceKind === 'decorates';
|
||||
}
|
||||
|
||||
/**
|
||||
* A Dart annotation is a constant expression: a `const` variable, or a call of
|
||||
* a `const` constructor. So it links, as it is written, a constant or (called)
|
||||
* a class its library can see, through the import prefix it is written with;
|
||||
* a static constant, an enum value or (called) a named constructor of the
|
||||
* type it is written through (`@Foo.value`, `@Foo.named(…)`); or, on a
|
||||
* member, a static constant of the type around it. Never a method, a
|
||||
* getter or a function: riverpod's 506 `@riverpod` annotations went to
|
||||
* riverpod_analyzer_utils' extension getter `riverpod`, not riverpod_annotation's
|
||||
* `const riverpod = Riverpod();`.
|
||||
*/
|
||||
export function matchDartAnnotation(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
|
||||
const name = ref.referenceName;
|
||||
if (!context.getNodesByName(name).some((n) => n.language === 'dart')) return null;
|
||||
const written = dartAnnotationAt(ref, context);
|
||||
if (!written) return null;
|
||||
const { names, call } = written;
|
||||
let pool: Node[];
|
||||
let resolvedBy: ResolvedRef['resolvedBy'] = 'exact-match';
|
||||
if (names.length === 1 || (names.length === 2 && isDartImportPrefix(ref.filePath, names[0]!, context))) {
|
||||
const prefix = names.length === 2 ? names[0]! : null;
|
||||
const own = prefix === null && !call ? dartEnclosingStatic(name, ref, context) : null;
|
||||
if (own) return { original: ref, targetNodeId: own.id, confidence: 0.9, resolvedBy };
|
||||
pool = context.getNodesByName(name).filter((n) => isDartLibraryDecl(n) &&
|
||||
(call ? isDartAnnotationClass(n, context) : n.kind === 'constant') && isDartTopLevelVisible(n, ref, context, prefix));
|
||||
if (prefix === null) pool = preferOwnDartLibrary(pool, ref, context);
|
||||
} else if (names.length === 2 || (names.length === 3 && isDartImportPrefix(ref.filePath, names[0]!, context))) {
|
||||
const prefix = names.length === 3 ? names[0]! : null;
|
||||
let owners = context.getNodesByName(names[names.length - 2]!).filter((n) =>
|
||||
isDartLibraryDecl(n) && DART_TYPE_KINDS.has(n.kind) && isDartTopLevelVisible(n, ref, context, prefix));
|
||||
if (prefix === null) owners = preferOwnDartLibrary(owners, ref, context);
|
||||
pool = context.getNodesByName(name).filter((m) => m.language === 'dart' &&
|
||||
owners.some((o) => o.filePath === m.filePath && m.qualifiedName === `${o.qualifiedName}::${name}`) &&
|
||||
(call ? isDartConstructor(m, context) : m.kind === 'constant' || m.kind === 'enum_member'));
|
||||
resolvedBy = 'qualified-name';
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
if (pool.length <= 1) return pool[0] ? { original: ref, targetNodeId: pool[0].id, confidence: 0.9, resolvedBy } : null;
|
||||
// Libraries this cannot establish see every file's declarations.
|
||||
const best = findBestMatch(ref, pool, context);
|
||||
return best
|
||||
? { original: ref, targetNodeId: best.id, confidence: computePathProximity(ref.filePath, best.filePath) >= 30 ? 0.7 : 0.4, resolvedBy }
|
||||
: null;
|
||||
}
|
||||
|
||||
/** A class a Dart annotation can call a constructor of: not an extension, which has none. */
|
||||
function isDartAnnotationClass(n: Node, context: ResolutionContext): boolean {
|
||||
return n.kind === 'class' && dartExtensionDecl(n, context) === null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The static constant or enum value `name` of the type a Dart annotation is
|
||||
* written in, which a member's annotation names bare. A class's own
|
||||
* annotations stand outside its body.
|
||||
*/
|
||||
function dartEnclosingStatic(name: string, ref: UnresolvedRef, context: ResolutionContext): Node | null {
|
||||
let own: Node | null = null;
|
||||
for (const n of context.getNodesInFile(ref.filePath)) {
|
||||
if (DART_TYPE_KINDS.has(n.kind) && n.id !== ref.fromNodeId && n.startLine <= ref.line && n.endLine >= ref.line &&
|
||||
(!own || n.startLine >= own.startLine)) own = n;
|
||||
}
|
||||
if (!own) return null;
|
||||
const qualifiedName = `${own.qualifiedName}::${name}`;
|
||||
return context.getNodesByName(name).find((m) => m.language === 'dart' && m.filePath === ref.filePath &&
|
||||
m.qualifiedName === qualifiedName && (m.kind === 'constant' || m.kind === 'enum_member')) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* How a Dart annotation is written — the names of `@p.Foo.named(…)` up to the
|
||||
* one its ref records, and whether it is called — read from its `@`. The
|
||||
* extractor records the last name, or the one a type argument list follows
|
||||
* (`@Foo<int>.named(…)` is `Foo`'s).
|
||||
*/
|
||||
function dartAnnotationAt(ref: UnresolvedRef, context: ResolutionContext): { names: string[]; call: boolean } | null {
|
||||
const code = dartCodeOf(ref.filePath, context);
|
||||
const from = code?.lineStarts[ref.line - 1];
|
||||
if (!code || from === undefined) return null;
|
||||
const to = code.lineStarts[ref.line] ?? code.text.length;
|
||||
// On a line with non-ASCII text before it, the `@` whose names fit stands in.
|
||||
const ats = [from + ref.column];
|
||||
for (let k = code.text.indexOf('@', from); k >= 0 && k < to; k = code.text.indexOf('@', k + 1)) {
|
||||
if (k !== from + ref.column) ats.push(k);
|
||||
}
|
||||
for (const at of ats) {
|
||||
const read = code.text[at] === '@' ? readDartAnnotation(code.text, at + 1) : null;
|
||||
const end = read ? (read.typeArgsAt >= 0 ? read.typeArgsAt : read.names.length - 1) : -1;
|
||||
if (read && read.names[end] === ref.referenceName) return { names: read.names.slice(0, end + 1), call: read.call };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** A Dart annotation's dotted names, which of them a type argument list follows, and whether `(` comes next on its line. */
|
||||
function readDartAnnotation(text: string, i: number): { names: string[]; typeArgsAt: number; call: boolean } | null {
|
||||
const names: string[] = [];
|
||||
let typeArgsAt = -1;
|
||||
const identifier = /[ \t]*([A-Za-z_$][\w$]*)[ \t]*/y;
|
||||
while (names.length < 8) {
|
||||
identifier.lastIndex = i;
|
||||
const m = identifier.exec(text);
|
||||
if (!m) return null;
|
||||
names.push(m[1]!);
|
||||
i = identifier.lastIndex;
|
||||
if (text[i] === '<' && typeArgsAt < 0) {
|
||||
const close = dartTypeArgumentsEnd(text, i);
|
||||
if (close < 0) return null;
|
||||
typeArgsAt = names.length - 1;
|
||||
for (i = close + 1; text[i] === ' ' || text[i] === '\t'; i++);
|
||||
}
|
||||
if (text[i] !== '.') break;
|
||||
i++;
|
||||
}
|
||||
return { names, typeArgsAt, call: text[i] === '(' };
|
||||
}
|
||||
|
||||
/**
|
||||
* A member an extension adds to `typeName` — `s.shout()` on an enum `Shape`
|
||||
* through `extension ShapeInfo on Shape` — when the type itself declares none
|
||||
@@ -11530,7 +11654,8 @@ function findBestMatch(
|
||||
|
||||
// For decorator references (`@Foo`), prefer functions. Class
|
||||
// decorators (Python `@SomeClass`, Java annotation interfaces)
|
||||
// also resolve here, hence the smaller class bonus.
|
||||
// also resolve here, hence the smaller class bonus. (A Dart annotation,
|
||||
// a constant or a constructor call, is matchDartAnnotation's.)
|
||||
if (ref.referenceKind === 'decorates') {
|
||||
if (candidate.kind === 'function' || candidate.kind === 'method') {
|
||||
score += 25;
|
||||
|
||||
Reference in New Issue
Block a user