fix(dart): anchor @name so a constructor initializer stops minting a … (#3224)
Some checks are pending
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Skill copy sync / shipped skills drift guard (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run

* fix(dart): anchor @name so a constructor initializer stops minting a phantom

    A Dart declaration whose value is a constructor call parses the callee as a
    SECOND (identifier) sibling of the declared name:

      final TextEditingController _title = TextEditingController();
      -> initialized_identifier[ identifier "_title",
                                 identifier "TextEditingController", selector ]

    The five graph-node rules that capture fields and top-level variables matched
    `(identifier) @name` without the first-child anchor, so @name bound to both
    siblings and the query minted a phantom Property/Variable named after the TYPE
    alongside the real declaration. On dart-flutter-conduit that produced a
    `Property TextEditingController` next to the genuine `_title` / `_body` in
    editor_screen.dart and login_screen.dart.

    static_final_declaration has the same shape, so class statics and top-level
    final/const were affected too, as were top-level `var`/`final` variables. All
    five rules now anchor @name with `.`, matching the mirror rules in
    languages/dart/query.ts which already anchored.

    Verified against the vendored grammar: the phantoms disappear and every real
    declaration is still captured (_title, _body, nullable field, static final,
    uninitialized field, top-level final, top-level var). End to end on
    dart-flutter-conduit: Property nodes 108 -> 106, type-shaped names 2 -> 0,
    real fields unchanged.

    The new test loads the grammar via createParserForLanguage rather than
    loadLanguage: loadLanguage resolves to void, so the surrounding
    `if (!(await loadDartOrSkip())) return;` idiom is always falsy and skips the
    body. Confirmed as a negative control -- reverting only the query change makes
    the new test fail on the exact phantom.

* chore(autofix): apply prettier + eslint fixes via /autofix command

* Address PR review feedback (#3224)

Correct the RHS_ONLY_TYPES comments so they state the capture invariant
instead of claiming those names appear only as constructor callees.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review feedback (#3224)

Build the Dart query with Parser.Query and parser.getLanguage() so the
test no longer casts the tree (or Query/captures) through any.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Sumiteshwark 2026-09-12 01:35:09 +05:30 committed by GitHub
parent af2d9aec15
commit 68c42009df
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 121 additions and 5 deletions

View file

@ -2243,18 +2243,26 @@ export const DART_QUERIES = `
(identifier) @name . (formal_parameter_list))) @definition.constructor
; Field declarations (String name = '', Address address = Address())
; @name is ANCHORED to the first named child. An initialized_identifier whose
; value is a constructor call parses the callee as a SECOND (identifier) sibling
; of the name \`final TextEditingController _t = TextEditingController();\` is
; initialized_identifier[identifier "_t", identifier "TextEditingController",
; selector]. Unanchored, \`(identifier) @name\` matched both and minted a phantom
; Property named after the TYPE alongside the real field. The same shape applies
; to static_final_declaration (class static and top-level final/const), so every
; rule below anchors. languages/dart/query.ts already anchors its mirror rules.
(declaration
(type_identifier)
(initialized_identifier_list
(initialized_identifier
(identifier) @name))) @definition.property
. (identifier) @name))) @definition.property
; Nullable field declarations (String? name)
(declaration
(nullable_type)
(initialized_identifier_list
(initialized_identifier
(identifier) @name))) @definition.property
. (identifier) @name))) @definition.property
; static const / static final / const class fields
; A "static const a = 1;" / "static final String b = ..., c = ...;" field parses
@ -2267,7 +2275,7 @@ export const DART_QUERIES = `
(declaration
(static_final_declaration_list
(static_final_declaration
(identifier) @name))) @definition.property
. (identifier) @name))) @definition.property
; Getters
(method_signature
@ -2290,7 +2298,7 @@ export const DART_QUERIES = `
(program
(initialized_identifier_list
(initialized_identifier
(identifier) @name)) @definition.variable)
. (identifier) @name)) @definition.variable)
; Closure bindings: \`var f = (x) => x;\` binds a CALLABLE, so it emits Function
; rather than Variable, matching TS/JS. Overlap with the pattern above is
; collapsed by the parse-worker dedup (#2687). Since #2693 this node is also
@ -2336,7 +2344,7 @@ export const DART_QUERIES = `
(program
(static_final_declaration_list
(static_final_declaration
(identifier) @name)) @definition.variable)
. (identifier) @name)) @definition.variable)
; Imports
(import_or_export

View file

@ -0,0 +1,108 @@
import { describe, it, expect, beforeAll } from 'vitest';
import Parser from 'tree-sitter';
import { SupportedLanguages } from '../../src/config/supported-languages.js';
import { createParserForLanguage } from '../../src/core/tree-sitter/parser-loader.js';
import { DART_QUERIES } from '../../src/core/ingestion/tree-sitter-queries.js';
/**
* A Dart field whose value is a constructor call parses the callee as a SECOND
* (identifier) sibling of the field name:
*
* final TextEditingController _title = TextEditingController();
* initialized_identifier[ identifier "_title",
* identifier "TextEditingController", selector ]
*
* Unanchored, `(identifier) @name` matched both siblings and minted a phantom
* Property/Variable named after the TYPE next to the real field. The same shape
* applies to static_final_declaration (class statics and top-level final/const),
* so all five graph-node rules anchor @name to the first named child.
*
* Regression guard: constructor-callee type names must never be captured as
* declarations, and every real declared name must still be captured.
*/
const CODE = `class S {
final TextEditingController _title = TextEditingController();
final TextEditingController _body = TextEditingController();
Foo? nullableField = Foo();
static final Bar staticField = Bar();
final ArticleApi _noInitializer;
}
final Baz topLevelFinal = Baz();
var topLevelVar = Qux();
`;
/** Constructor callee names that must never be captured as declaration names. */
const RHS_ONLY_TYPES = ['TextEditingController', 'Foo', 'Bar', 'Baz', 'Qux'];
/** Every name actually declared in CODE. */
const DECLARED = [
'_title',
'_body',
'nullableField',
'staticField',
'_noInitializer',
'topLevelFinal',
'topLevelVar',
];
describe('Dart field/variable declarations with constructor initializers', () => {
let parser: Parser | null = null;
let unavailable: string | null = null;
beforeAll(async () => {
// NB: loadLanguage() resolves to void, so the `if (!(await loadDartOrSkip()))`
// idiom used elsewhere in this suite is always truthy-false and skips the
// whole test body. createParserForLanguage returns the Parser, so a genuine
// load failure is distinguishable from a successful load.
try {
parser = await createParserForLanguage(SupportedLanguages.Dart);
} catch (error) {
unavailable = error instanceof Error ? error.message : String(error);
}
});
function capturedNames(): { property: string[]; variable: string[] } {
if (!parser) throw new Error('parser unavailable');
const tree = parser.parse(CODE);
const query = new Parser.Query(parser.getLanguage(), DART_QUERIES);
const property: string[] = [];
const variable: string[] = [];
for (const match of query.matches(tree.rootNode)) {
const name = match.captures.find((c) => c.name === 'name');
const def = match.captures.find((c) => c.name.startsWith('definition.'));
if (!name || !def) continue;
if (def.name === 'definition.property') property.push(name.node.text);
if (def.name === 'definition.variable') variable.push(name.node.text);
}
return { property: [...new Set(property)], variable: [...new Set(variable)] };
}
it('does not mint a phantom named after the initializer type', (ctx) => {
if (!parser) return ctx.skip(`dart grammar unavailable: ${unavailable}`);
const { property, variable } = capturedNames();
const all = [...property, ...variable];
expect(all.length, 'query produced no captures at all').toBeGreaterThan(0);
for (const type of RHS_ONLY_TYPES) {
expect(all, `phantom captured for constructor callee "${type}"`).not.toContain(type);
}
});
it('still captures every declared field and top-level variable', (ctx) => {
if (!parser) return ctx.skip(`dart grammar unavailable: ${unavailable}`);
const { property, variable } = capturedNames();
const all = [...property, ...variable];
for (const declared of DECLARED) {
expect(all, `lost real declaration "${declared}"`).toContain(declared);
}
});
it('classifies class members as property and top-level names as variable', (ctx) => {
if (!parser) return ctx.skip(`dart grammar unavailable: ${unavailable}`);
const { property, variable } = capturedNames();
expect(property).toEqual(
expect.arrayContaining(['_title', '_body', 'nullableField', 'staticField', '_noInitializer']),
);
expect(variable).toEqual(expect.arrayContaining(['topLevelFinal', 'topLevelVar']));
});
});