mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-21 06:29:44 +08:00
fix(dart): a comment inside a member chain no longer loses a call or a read (#2385)
tree-sitter-dart keeps a comment as a named node wherever it is written, so in a member chain broken over lines (`tester //`, then `.state(…)` on the next line: the empty `//` keeps dart format from joining the lines) the comment, not `tester`, is the sibling right before the `.state` selector. Both extractors (the TS walker and the native kernel) read that sibling as the receiver or the callee: - A call with a comment between the member and its arguments (`box.grow /* by */ (3)`) was lost, and recorded as a read of `box.grow`. - A getter read (`box //` + `.area`) or a static access (`Config //` + `.instance`) written after a comment was lost. - A call after a comment kept only its bare name. Since #2376 the resolver reads such a chain link back past comments, so those calls already linked correctly; they now carry the receiver in the name. Every sibling step along a member chain now skips `comment` and `documentation_comment` nodes: the callee before an argument part, dartReceiverOf / receiver_of, dartCalleeOfArgPart, the type in front of a constructor's type arguments, and the member read's receiver and next selector. The misparsed-generic-call gate keeps reading raw siblings, and now rejects a comment against the `<` (`ref.read /* c */<Repo>(p)`), so that shape stays a comparison instead of losing its read. Validated with the kernel loaded on felangel/bloc @b9be1e2, rrousselGit/riverpod @4ba1be2 and flutter/samples @63411527. No edge is added or removed: the 14 comment-split chains there are all in riverpod, and its 5 calls among them gain their receiver and stay unlinked. Kernel/wasm parity has 0 diffs on all three. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
5f5b8db620
commit
249d9a860e
@@ -53,6 +53,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|||||||
- In Dart, a constructor or member written without a body, like flutter_bloc's `const BlocProvider.value(…)`, `Foo._();`, `Point.origin() : x = 0, y = 0;` or an abstract or `external` method, now keeps the `///` documentation and the annotations written above it, the way one with a body already did. Before, that documentation was lost, so searching for words in it didn't find the symbol, and an annotation like `@visibleForTesting` or `@protected` on it wasn't recorded. Re-index Dart projects after upgrading.
|
- In Dart, a constructor or member written without a body, like flutter_bloc's `const BlocProvider.value(…)`, `Foo._();`, `Point.origin() : x = 0, y = 0;` or an abstract or `external` method, now keeps the `///` documentation and the annotations written above it, the way one with a body already did. Before, that documentation was lost, so searching for words in it didn't find the symbol, and an annotation like `@visibleForTesting` or `@protected` on it wasn't recorded. Re-index Dart projects after upgrading.
|
||||||
- In Dart, a call on what a generic lookup returns, like flutter_bloc's `BlocProvider.of<CounterCubit>(context, listen: false).increment()` or Provider's `Provider.of<CartModel>(context, listen: false).add(item)`, now links to that method of the type in the angle brackets, whether the lookup is declared in your project or comes from a package. Before, such a call often linked nothing — for example whenever the lookup passed `listen: false` — so `callers` and impact missed those methods. A lookup that names no type, like `BlocProvider.of(context)`, still links nothing. Re-index Dart projects after upgrading. (#750)
|
- In Dart, a call on what a generic lookup returns, like flutter_bloc's `BlocProvider.of<CounterCubit>(context, listen: false).increment()` or Provider's `Provider.of<CartModel>(context, listen: false).add(item)`, now links to that method of the type in the angle brackets, whether the lookup is declared in your project or comes from a package. Before, such a call often linked nothing — for example whenever the lookup passed `listen: false` — so `callers` and impact missed those methods. A lookup that names no type, like `BlocProvider.of(context)`, still links nothing. Re-index Dart projects after upgrading. (#750)
|
||||||
- In JavaScript and TypeScript, a store whose name starts or ends with a `$`, like `const items$ = create(…)`, now has its actions indexed when an `export { … }` list on a later line exports it, as in `export { items$ };` or `export { $items as default };`. Before, those actions were missing from the graph, so their callers and impact came back empty. A store whose name only lacks the `$`, like `items`, is no longer treated as exported by that list. Re-index JavaScript and TypeScript projects after upgrading.
|
- In JavaScript and TypeScript, a store whose name starts or ends with a `$`, like `const items$ = create(…)`, now has its actions indexed when an `export { … }` list on a later line exports it, as in `export { items$ };` or `export { $items as default };`. Before, those actions were missing from the graph, so their callers and impact came back empty. A store whose name only lacks the `$`, like `items`, is no longer treated as exported by that list. Re-index JavaScript and TypeScript projects after upgrading.
|
||||||
|
- In Dart, a comment inside a chain of calls no longer loses part of it: a call with a comment before its arguments, like `box.grow /* by */ (3)`, now links to the method it calls, and a getter read or a static access written after a comment, like `Config //` followed by `.instance` on the next line (the empty `//` keeps `dart format` from joining the lines), now counts as a use. Re-index Dart projects after upgrading.
|
||||||
|
|
||||||
## [1.6.2] - 2026-10-03
|
## [1.6.2] - 2026-10-03
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
/**
|
||||||
|
* A comment inside a Dart member chain hid what the chain's parts are.
|
||||||
|
*
|
||||||
|
* tree-sitter-dart keeps a comment as a named node wherever it is written, so
|
||||||
|
* in `tester //` + newline + `.state(…)` — the empty `//` keeps dart format
|
||||||
|
* from joining the lines, a common idiom in riverpod's tests — the comment,
|
||||||
|
* not `tester`, sat right before the `.state` selector. Both extractors took
|
||||||
|
* that node for the receiver:
|
||||||
|
*
|
||||||
|
* - a call with a block comment between the member and its arguments
|
||||||
|
* (`box.grow`, the comment, `(3)`) was lost, and recorded as a read of
|
||||||
|
* `box.grow`;
|
||||||
|
* - a getter read (`box //` + `.area`) and a static access (`Config //` +
|
||||||
|
* `.instance`) written after a comment were lost;
|
||||||
|
* - a call after a comment kept only its bare name (`state`). The resolver now
|
||||||
|
* reads a bare chain link back past comments, so that one already linked
|
||||||
|
* right; it keeps doing so with the receiver in the name.
|
||||||
|
*
|
||||||
|
* Every sibling step along a chain now skips comments, so each line extracts
|
||||||
|
* what it would without the comment.
|
||||||
|
*
|
||||||
|
* Runs against the native kernel (when built) and the wasm extractor, which
|
||||||
|
* must agree.
|
||||||
|
*/
|
||||||
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as os from 'os';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { CodeGraph } from '../src';
|
||||||
|
import type { Node } from '../src/types';
|
||||||
|
|
||||||
|
const FILES: Record<string, string> = {
|
||||||
|
'pubspec.yaml': 'name: app\nenvironment:\n sdk: ">=3.0.0 <4.0.0"\n',
|
||||||
|
'lib/model.dart': `class Counter {
|
||||||
|
int state(int x) => x;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Box {
|
||||||
|
int get area => 1;
|
||||||
|
int grow(int by) => by;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Config {
|
||||||
|
static final Config instance = Config();
|
||||||
|
}
|
||||||
|
|
||||||
|
class Foo {
|
||||||
|
static Foo create() => Foo();
|
||||||
|
void bar() {}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
'test/scope_test.dart': `import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:app/model.dart';
|
||||||
|
|
||||||
|
// Same-named methods next to the calls, where a guess by name lands.
|
||||||
|
class Other {
|
||||||
|
void bar() {}
|
||||||
|
int grow(int by) => by;
|
||||||
|
}
|
||||||
|
|
||||||
|
int inline(Box box) => box.grow /* by */ (3);
|
||||||
|
|
||||||
|
int measure(Box box) {
|
||||||
|
return box //
|
||||||
|
.area;
|
||||||
|
}
|
||||||
|
|
||||||
|
void use() {
|
||||||
|
final config = Config //
|
||||||
|
.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
void scope(WidgetTester tester) {
|
||||||
|
tester //
|
||||||
|
.state(find.byType(Counter));
|
||||||
|
}
|
||||||
|
|
||||||
|
void run() {
|
||||||
|
Foo.create() //
|
||||||
|
.bar();
|
||||||
|
}
|
||||||
|
|
||||||
|
int widen(Box box) {
|
||||||
|
return box
|
||||||
|
// a whole-line comment
|
||||||
|
/// and a dartdoc one
|
||||||
|
.grow(2);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Dart member chains split by a comment', () => {
|
||||||
|
let root = '';
|
||||||
|
let cg: CodeGraph | undefined;
|
||||||
|
let kernel: string | undefined;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-dart-comment-chains-'));
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
kernel = process.env.CODEGRAPH_KERNEL;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cg?.close();
|
||||||
|
cg = undefined;
|
||||||
|
fs.rmSync(root, { recursive: true, force: true });
|
||||||
|
if (kernel === undefined) delete process.env.CODEGRAPH_KERNEL;
|
||||||
|
else process.env.CODEGRAPH_KERNEL = kernel;
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(['default', 'wasm'])('extract each part past the comment (%s)', async (backend) => {
|
||||||
|
if (backend === 'wasm') process.env.CODEGRAPH_KERNEL = '0';
|
||||||
|
else delete process.env.CODEGRAPH_KERNEL;
|
||||||
|
cg = await CodeGraph.init(root, { index: true });
|
||||||
|
const graph = cg;
|
||||||
|
const node = (qualifiedName: string): Node => {
|
||||||
|
const found = graph.getNodesInFile('test/scope_test.dart').find((n) => n.qualifiedName === qualifiedName);
|
||||||
|
expect(found, qualifiedName).toBeDefined();
|
||||||
|
return found!;
|
||||||
|
};
|
||||||
|
const targets = (from: string, kind: string): string[] =>
|
||||||
|
[...new Set(graph
|
||||||
|
.getOutgoingEdgesFrom([node(from).id])
|
||||||
|
.filter((e) => e.kind === kind)
|
||||||
|
.map((e) => graph.getNode(e.target)!.qualifiedName))]
|
||||||
|
.sort();
|
||||||
|
|
||||||
|
// A comment between the member and its arguments: still a call of the
|
||||||
|
// receiver's method.
|
||||||
|
expect(targets('inline', 'calls')).toEqual(['Box::grow']);
|
||||||
|
|
||||||
|
// A getter read runs the getter; a static access references its type.
|
||||||
|
expect(targets('measure', 'calls')).toEqual(['Box::area']);
|
||||||
|
expect(targets('use', 'references')).toEqual(['Config']);
|
||||||
|
|
||||||
|
// Calls keep resolving with the receiver in the name: `tester.state(…)` is
|
||||||
|
// WidgetTester's, a type outside the project, not the one `state` the
|
||||||
|
// project declares; a chain off a factory goes through what `create`
|
||||||
|
// returns; the receiver's type picks the method past a run of comments.
|
||||||
|
expect(targets('scope', 'calls')).toEqual([]);
|
||||||
|
expect(targets('run', 'calls')).toEqual(['Foo::bar', 'Foo::create']);
|
||||||
|
expect(targets('widen', 'calls')).toEqual(['Box::grow']);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
// Comments inside member chains. tree-sitter-dart keeps a comment as a named
|
||||||
|
// sibling between a chain's parts; every sibling step skips it, so each line
|
||||||
|
// emits what it would without the comment.
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
class ConfigT {
|
||||||
|
static final ConfigT instance = ConfigT();
|
||||||
|
static ConfigT load() => ConfigT();
|
||||||
|
int setting = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
class FactoryT {
|
||||||
|
static FactoryT create() => FactoryT();
|
||||||
|
void run() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Box {
|
||||||
|
int get area => 1;
|
||||||
|
int grow(int by) => by;
|
||||||
|
}
|
||||||
|
|
||||||
|
// An initializer, walked for its constant.
|
||||||
|
final initialized = ConfigT //
|
||||||
|
.instance;
|
||||||
|
|
||||||
|
class Chains extends Box {
|
||||||
|
void calls(WidgetTester tester, Box box, List<int> xs) {
|
||||||
|
// dart format's line-break idiom, a whole-line comment, dartdoc, a run of
|
||||||
|
// block comments.
|
||||||
|
tester //
|
||||||
|
.state(find.byType(Box));
|
||||||
|
tester
|
||||||
|
// a whole-line comment
|
||||||
|
.pump();
|
||||||
|
tester /// doc
|
||||||
|
.a();
|
||||||
|
tester /** doc */ .b();
|
||||||
|
tester /* c */ /* d */ .c();
|
||||||
|
// A comment between the member and its arguments.
|
||||||
|
tester.d // c
|
||||||
|
();
|
||||||
|
box.grow /* by */ (3);
|
||||||
|
// Chains: re-encoded off a capitalized call, bare off a lowercase one.
|
||||||
|
FactoryT.create() //
|
||||||
|
.run();
|
||||||
|
FactoryT //
|
||||||
|
.create() //
|
||||||
|
.run();
|
||||||
|
xs //
|
||||||
|
.map((e) => e) //
|
||||||
|
.toList();
|
||||||
|
// `this` / `super` keep the bare name.
|
||||||
|
this //
|
||||||
|
.grow(1);
|
||||||
|
super //
|
||||||
|
.grow(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
int reads(Box box, WidgetTester tester) {
|
||||||
|
final v = box //
|
||||||
|
.area;
|
||||||
|
final w = box.area /* c */ .isEven;
|
||||||
|
final s = ConfigT //
|
||||||
|
.instance;
|
||||||
|
ConfigT // static call: a call and a static reference
|
||||||
|
.load();
|
||||||
|
final t = ConfigT /* setting */ .instance.setting;
|
||||||
|
// A generic call the grammar reads as two comparisons.
|
||||||
|
final st = tester //
|
||||||
|
.state<ProviderScopeState>(find.byType(Box));
|
||||||
|
// A comment against the `<`: not laid out as a call, so a read and two
|
||||||
|
// comparisons, as written.
|
||||||
|
final cr = tester.read /* c */<Box>(box);
|
||||||
|
final dr = tester.read /** doc */<Box>(box);
|
||||||
|
return v + w.hashCode + s.setting + t + st.hashCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,9 +30,10 @@
|
|||||||
* arguments, as statements and as expressions; TortureGenericCalls.dart:
|
* arguments, as statements and as expressions; TortureGenericCalls.dart:
|
||||||
* generic calls parsed as two comparisons, recovered as calls;
|
* generic calls parsed as two comparisons, recovered as calls;
|
||||||
* TortureDeclarationDocs.dart: the dartdoc and annotations of a
|
* TortureDeclarationDocs.dart: the dartdoc and annotations of a
|
||||||
* `declaration`-wrapped member with no body, read from before the wrapper)
|
* `declaration`-wrapped member with no body, read from before the wrapper;
|
||||||
* and their CRLF variants
|
* TortureCommentChains.dart: comments between a member chain's parts skipped
|
||||||
* (derived in-memory — #1329), plus defer and generated-file pins.
|
* by every sibling step) and their CRLF variants (derived in-memory — #1329),
|
||||||
|
* plus defer and generated-file pins.
|
||||||
*
|
*
|
||||||
* The full-repo sweeps live in scripts/kernel-parity.mjs (shelf/bloc/flutter
|
* The full-repo sweeps live in scripts/kernel-parity.mjs (shelf/bloc/flutter
|
||||||
* with --max-deferral 0.3); this suite keeps the invariant alive in
|
* with --max-deferral 0.3); this suite keeps the invariant alive in
|
||||||
@@ -141,6 +142,10 @@ describe.skipIf(!kernelBuilt)('kernel Dart extraction parity', () => {
|
|||||||
// external members) take their dartdoc and annotations from before the
|
// external members) take their dartdoc and annotations from before the
|
||||||
// wrapper; a field's stay with the field.
|
// wrapper; a field's stay with the field.
|
||||||
['TortureDeclarationDocs.dart', 20],
|
['TortureDeclarationDocs.dart', 20],
|
||||||
|
// Comments between a chain's parts — `tester //` + newline + `.state(…)`,
|
||||||
|
// dartdoc, block comments, one between a member and its arguments — are
|
||||||
|
// skipped by every sibling step: calls, chains, reads, static references.
|
||||||
|
['TortureCommentChains.dart', 10],
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
for (const [file, minNodes] of FIXTURES) {
|
for (const [file, minNodes] of FIXTURES) {
|
||||||
|
|||||||
@@ -120,6 +120,31 @@ fn is_prefixed(kind: &str) -> bool {
|
|||||||
matches!(kind, "unary_expression" | "await_expression")
|
matches!(kind, "unary_expression" | "await_expression")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// DART_COMMENTS (dart.ts) — line and block comments, and dartdoc.
|
||||||
|
fn is_comment(node: Node) -> bool {
|
||||||
|
matches!(node.kind(), "comment" | "documentation_comment")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// dartPrevNamed (dart.ts) — the named sibling before `node`, past comments:
|
||||||
|
/// `tester //` + newline + `.state(…)` puts the comment, not `tester`, right
|
||||||
|
/// before the `.state` selector.
|
||||||
|
fn prev_named<'t>(node: Node<'t>) -> Option<Node<'t>> {
|
||||||
|
let mut prev = node.prev_named_sibling();
|
||||||
|
while let Some(p) = prev.filter(|p| is_comment(*p)) {
|
||||||
|
prev = p.prev_named_sibling();
|
||||||
|
}
|
||||||
|
prev
|
||||||
|
}
|
||||||
|
|
||||||
|
/// dartNextNamed (dart.ts) — the named sibling after `node`, past comments.
|
||||||
|
fn next_named<'t>(node: Node<'t>) -> Option<Node<'t>> {
|
||||||
|
let mut next = node.next_named_sibling();
|
||||||
|
while let Some(n) = next.filter(|n| is_comment(*n)) {
|
||||||
|
next = n.next_named_sibling();
|
||||||
|
}
|
||||||
|
next
|
||||||
|
}
|
||||||
|
|
||||||
struct Scope {
|
struct Scope {
|
||||||
row: u32,
|
row: u32,
|
||||||
kind: &'static str,
|
kind: &'static str,
|
||||||
@@ -1082,7 +1107,8 @@ impl<'t> Walker<'t> {
|
|||||||
if !has_arg_part {
|
if !has_arg_part {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let prev = node.prev_named_sibling()?;
|
// Past comments: `box.grow /* by */ (3)` calls `box.grow`.
|
||||||
|
let prev = prev_named(node)?;
|
||||||
return self.callee_name(prev);
|
return self.callee_name(prev);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1137,11 +1163,11 @@ impl<'t> Walker<'t> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// dartReceiverOf (dart.ts) — the receiver of the member access
|
/// dartReceiverOf (dart.ts) — the receiver of the member access
|
||||||
/// `selector`: the node in front of it, or the end of an arrow function's
|
/// `selector`: the node in front of it, past comments, or the end of an
|
||||||
/// body when the grammar ended the arrow in front of a generic call
|
/// arrow function's body when the grammar ended the arrow in front of a
|
||||||
/// (`(ref) => ref.watch<int>(p)` → `((ref) => ref).watch<int>(p)`).
|
/// generic call (`(ref) => ref.watch<int>(p)` → `((ref) => ref).watch<int>(p)`).
|
||||||
fn receiver_of(&self, selector: Node<'t>) -> Option<Node<'t>> {
|
fn receiver_of(&self, selector: Node<'t>) -> Option<Node<'t>> {
|
||||||
let prev = selector.prev_named_sibling();
|
let prev = prev_named(selector);
|
||||||
let body = prev
|
let body = prev
|
||||||
.filter(|p| p.kind() == "function_expression")
|
.filter(|p| p.kind() == "function_expression")
|
||||||
.and_then(|p| p.named_child(p.named_child_count().checked_sub(1)?));
|
.and_then(|p| p.named_child(p.named_child_count().checked_sub(1)?));
|
||||||
@@ -1242,8 +1268,10 @@ impl<'t> Walker<'t> {
|
|||||||
if !matches!(args.kind(), "parenthesized_expression" | "record_literal") || args.start_byte() != gt.end_byte() {
|
if !matches!(args.kind(), "parenthesized_expression" | "record_literal") || args.start_byte() != gt.end_byte() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
// A comment against the `<` (`ref.read /* c */<Repo>(p)`) is a
|
||||||
|
// sibling of its own, so the callee is not against it.
|
||||||
let before = lt.prev_named_sibling()?;
|
let before = lt.prev_named_sibling()?;
|
||||||
if before.end_byte() != lt.start_byte() {
|
if is_comment(before) || before.end_byte() != lt.start_byte() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
// The type argument: `T`, `p.T`, `T<…>` or `p.T<…>`, and nothing else.
|
// The type argument: `T`, `p.T`, `T<…>` or `p.T<…>`, and nothing else.
|
||||||
@@ -1321,7 +1349,7 @@ impl<'t> Walker<'t> {
|
|||||||
|
|
||||||
/// dartCalleeOfArgPart (dart.ts:100-116).
|
/// dartCalleeOfArgPart (dart.ts:100-116).
|
||||||
fn callee_of_arg_part(&self, arg_part: Node<'t>) -> Option<String> {
|
fn callee_of_arg_part(&self, arg_part: Node<'t>) -> Option<String> {
|
||||||
let prev = arg_part.prev_named_sibling()?;
|
let prev = prev_named(arg_part)?;
|
||||||
if prev.kind() == "identifier" {
|
if prev.kind() == "identifier" {
|
||||||
return Some(self.text(prev).to_string());
|
return Some(self.text(prev).to_string());
|
||||||
}
|
}
|
||||||
@@ -1339,7 +1367,7 @@ impl<'t> Walker<'t> {
|
|||||||
found
|
found
|
||||||
});
|
});
|
||||||
if let Some(method_id) = method_id {
|
if let Some(method_id) = method_id {
|
||||||
let accessor_prev = prev.prev_named_sibling();
|
let accessor_prev = prev_named(prev);
|
||||||
if let Some(ap) = accessor_prev {
|
if let Some(ap) = accessor_prev {
|
||||||
if ap.kind() == "identifier" {
|
if ap.kind() == "identifier" {
|
||||||
return Some(format!("{}.{}", self.text(ap), self.text(method_id)));
|
return Some(format!("{}.{}", self.text(ap), self.text(method_id)));
|
||||||
@@ -1364,7 +1392,7 @@ impl<'t> Walker<'t> {
|
|||||||
if selector.named_child(0)?.kind() != "type_arguments" {
|
if selector.named_child(0)?.kind() != "type_arguments" {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let type_node = selector.prev_named_sibling()?;
|
let type_node = prev_named(selector)?;
|
||||||
if type_node.kind() != "identifier" {
|
if type_node.kind() != "identifier" {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -1687,11 +1715,11 @@ impl<'t> Walker<'t> {
|
|||||||
found
|
found
|
||||||
});
|
});
|
||||||
let Some(member) = member else { return };
|
let Some(member) = member else { return };
|
||||||
let Some(receiver) = node.prev_named_sibling() else { return };
|
let Some(receiver) = prev_named(node) else { return };
|
||||||
if receiver.kind() != "identifier" {
|
if receiver.kind() != "identifier" {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let next = node.next_named_sibling();
|
let next = next_named(node);
|
||||||
if let Some(next) = next {
|
if let Some(next) = next {
|
||||||
if next.kind() == "selector" {
|
if next.kind() == "selector" {
|
||||||
let mut nc = next.walk();
|
let mut nc = next.walk();
|
||||||
|
|||||||
@@ -722,6 +722,18 @@ extract-mini.txt):
|
|||||||
| local-lambda body `final lam = (int a) { helper(a); }` | function_expression recursed transparently | `helper` attributed to the ENCLOSING function; `lam(5)` → `lam` |
|
| local-lambda body `final lam = (int a) { helper(a); }` | function_expression recursed transparently | `helper` attributed to the ENCLOSING function; `lam(5)` → `lam` |
|
||||||
| `final p = Provider((ref) => Repo(ref.watch(d)))`, `static var s = make()`, `final _c = Controller();` in a class, `var v = load();` | the hook's walkInitializer (§Constants) | the full matrix, from the constant / the class / the file |
|
| `final p = Provider((ref) => Repo(ref.watch(d)))`, `static var s = make()`, `final _c = Controller();` in a class, `var v = load();` | the hook's walkInitializer (§Constants) | the full matrix, from the constant / the class / the file |
|
||||||
| ctor initializers (`: size = seed()`), enum-constant args (`ok(200)`), default param values | never body-walked | NOTHING |
|
| ctor initializers (`: size = seed()`), enum-constant args (`ok(200)`), default param values | never body-walked | NOTHING |
|
||||||
|
| `tester //` + newline + `.state(x)`, `box.grow /* by */ (3)` | comments are named SIBLINGS between the chain's parts | `tester.state` / `box.grow` — every sibling step skips `comment` and `documentation_comment` (dartPrevNamed / dartNextNamed; kernel `prev_named` / `next_named`), so a comment changes nothing |
|
||||||
|
|
||||||
|
Every sibling step along a member chain goes past comments: the callee before
|
||||||
|
an argument part, dartReceiverOf / receiver_of, dartCalleeOfArgPart's two,
|
||||||
|
dartTypeArgumentsReceiver's type, and dartMemberRead's receiver and its
|
||||||
|
next-selector check. tree-sitter-dart keeps a comment as a named node wherever
|
||||||
|
it is written, so before (2026-10-06) a comment between a member and its
|
||||||
|
arguments lost the call and emitted a member read, a getter read or static
|
||||||
|
access after a comment (`Config //`, dart format's line-break idiom) was lost,
|
||||||
|
and a call after one kept only its bare name. The misparsed-generic-call
|
||||||
|
layout checks below still read the raw siblings: a comment there means the
|
||||||
|
code is not laid out as a call.
|
||||||
|
|
||||||
extractCall (:3684), LITERAL_RECEIVER_TYPES (:373-388), SKIP_RECEIVERS, the
|
extractCall (:3684), LITERAL_RECEIVER_TYPES (:373-388), SKIP_RECEIVERS, the
|
||||||
parenthesized-conversion regex (:4530), template-strip — ALL UNREACHABLE for
|
parenthesized-conversion regex (:4530), template-strip — ALL UNREACHABLE for
|
||||||
@@ -743,7 +755,10 @@ selector (`p.Repo`) and/or a type_arguments-only selector (`Map<K, V>`); the
|
|||||||
callee part sits under `unary_expression` / `await_expression` for `await` and
|
callee part sits under `unary_expression` / `await_expression` for `await` and
|
||||||
prefix operators. Recovered at the `<` when the type names a type (`/^[_$]*[A-Z]/`
|
prefix operators. Recovered at the `<` when the type names a type (`/^[_$]*[A-Z]/`
|
||||||
or `int double num bool dynamic void`), `<` touches the callee and `(` touches
|
or `int double num bool dynamic void`), `<` touches the callee and `(` touches
|
||||||
the `>`; `a < B > (c)`, `a<b>(c)` and `a <B>(c)` stay comparisons. The
|
the `>`; `a < B > (c)`, `a<b>(c)` and `a <B>(c)` stay comparisons, and so does
|
||||||
|
`a.b /* c */<B>(c)`: the comment is a sibling of its own against the `<`, so
|
||||||
|
it is rejected as the callee (else the gate passed with no call, and the
|
||||||
|
comment-skipping member read was suppressed). The
|
||||||
recovered form emits what the parsed call does:
|
recovered form emits what the parsed call does:
|
||||||
|
|
||||||
- `calls` at the `<` (the parsed call's argument_part starts there);
|
- `calls` at the `<` (the parsed call's argument_part starts there);
|
||||||
@@ -773,8 +788,9 @@ do not, so `() => BlocProvider.of<CounterCubit>(context).increment()` keeps
|
|||||||
|
|
||||||
Called from the body walker only (:5218). The DART-SPECIFIC branch (the
|
Called from the body walker only (:5218). The DART-SPECIFIC branch (the
|
||||||
shared MEMBER_ACCESS_TYPES path is never reached — it returns first):
|
shared MEMBER_ACCESS_TYPES path is never reached — it returns first):
|
||||||
node.type === `selector` AND it has NO `argument_part` child AND
|
node.type === `selector` AND it has NO `argument_part` child AND the
|
||||||
previousNamedSibling is an `identifier` matching `/^[A-Z][A-Za-z0-9_]*$/` →
|
receiver (dartReceiverOf: the previous named sibling past comments) is an
|
||||||
|
`identifier` matching `/^[A-Z][A-Za-z0-9_]*$/` →
|
||||||
`references <identifier text>` from the enclosing symbol at the
|
`references <identifier text>` from the enclosing symbol at the
|
||||||
**IDENTIFIER's (receiver's) position** (pushStaticMemberRef :4800). Pins:
|
**IDENTIFIER's (receiver's) position** (pushStaticMemberRef :4800). Pins:
|
||||||
|
|
||||||
|
|||||||
@@ -63,17 +63,40 @@ function isDartFieldOrTopLevelEntry(node: SyntaxNode): boolean {
|
|||||||
return owner?.type === 'declaration' && owner.parent !== null && DART_MEMBER_BODIES.has(owner.parent.type);
|
return owner?.type === 'declaration' && owner.parent !== null && DART_MEMBER_BODIES.has(owner.parent.type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The comments tree-sitter-dart keeps as named nodes: line and block comments, and dartdoc. */
|
||||||
|
const DART_COMMENTS: ReadonlySet<string> = new Set(['comment', 'documentation_comment']);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The receiver of the member access `selector`: the node in front of it, or
|
* The named sibling before `node`, past any comments. A Dart member chain is
|
||||||
* the end of an arrow function's body when the grammar ended the arrow in
|
* an identifier followed by sibling selectors, and the grammar keeps a comment
|
||||||
* front of a generic call — `(ref) => ref.watch<int>(p)` comes out as
|
* as a sibling wherever it is written, so in `tester //` + newline +
|
||||||
* `((ref) => ref).watch<int>(p)`. Dart writes a member access on the arrow
|
* `.state(…)` (the empty `//` keeps dart format from joining the lines) the
|
||||||
* itself in parentheses, so a receiver in that place is never the arrow. It
|
* comment, not `tester`, is right before the `.state` selector.
|
||||||
* names the call (`ref.watch`) and a static member's class; a call chained on
|
*/
|
||||||
* that call keeps its bare name (see dartMisparsedGenericCall).
|
function dartPrevNamed(node: SyntaxNode): SyntaxNode | null {
|
||||||
|
let prev = node.previousNamedSibling;
|
||||||
|
while (prev && DART_COMMENTS.has(prev.type)) prev = prev.previousNamedSibling;
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The named sibling after `node`, past any comments (see dartPrevNamed). */
|
||||||
|
function dartNextNamed(node: SyntaxNode): SyntaxNode | null {
|
||||||
|
let next = node.nextNamedSibling;
|
||||||
|
while (next && DART_COMMENTS.has(next.type)) next = next.nextNamedSibling;
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The receiver of the member access `selector`: the node in front of it, past
|
||||||
|
* any comments, or the end of an arrow function's body when the grammar ended
|
||||||
|
* the arrow in front of a generic call — `(ref) => ref.watch<int>(p)` comes
|
||||||
|
* out as `((ref) => ref).watch<int>(p)`. Dart writes a member access on the
|
||||||
|
* arrow itself in parentheses, so a receiver in that place is never the
|
||||||
|
* arrow. It names the call (`ref.watch`) and a static member's class; a call
|
||||||
|
* chained on that call keeps its bare name (see dartMisparsedGenericCall).
|
||||||
*/
|
*/
|
||||||
export function dartReceiverOf(selector: SyntaxNode): SyntaxNode | null {
|
export function dartReceiverOf(selector: SyntaxNode): SyntaxNode | null {
|
||||||
const prev = selector.previousNamedSibling;
|
const prev = dartPrevNamed(selector);
|
||||||
const body = prev?.type === 'function_expression' ? prev.lastNamedChild : null;
|
const body = prev?.type === 'function_expression' ? prev.lastNamedChild : null;
|
||||||
return body?.type === 'function_expression_body' ? body.lastNamedChild : prev;
|
return body?.type === 'function_expression_body' ? body.lastNamedChild : prev;
|
||||||
}
|
}
|
||||||
@@ -123,8 +146,10 @@ export function dartMisparsedGenericCall(lt: SyntaxNode): DartMisparsedCall | un
|
|||||||
const args = outer.namedChild(2);
|
const args = outer.namedChild(2);
|
||||||
if (args?.type !== 'parenthesized_expression' && args?.type !== 'record_literal') return undefined;
|
if (args?.type !== 'parenthesized_expression' && args?.type !== 'record_literal') return undefined;
|
||||||
if (args.startIndex !== gt.endIndex) return undefined;
|
if (args.startIndex !== gt.endIndex) return undefined;
|
||||||
|
// A comment against the `<` (`ref.read /* c */<Repo>(p)`) is a sibling of
|
||||||
|
// its own, so the callee is not against it: not laid out as a call.
|
||||||
const before = lt.previousNamedSibling;
|
const before = lt.previousNamedSibling;
|
||||||
if (!before || before.endIndex !== lt.startIndex) return undefined;
|
if (!before || DART_COMMENTS.has(before.type) || before.endIndex !== lt.startIndex) return undefined;
|
||||||
// The type argument: `T`, `p.T`, `T<…>` or `p.T<…>`, and nothing else.
|
// The type argument: `T`, `p.T`, `T<…>` or `p.T<…>`, and nothing else.
|
||||||
const head = lt.nextNamedSibling;
|
const head = lt.nextNamedSibling;
|
||||||
if (head?.type !== 'identifier') return undefined;
|
if (head?.type !== 'identifier') return undefined;
|
||||||
@@ -194,9 +219,9 @@ export function dartMemberRead(
|
|||||||
);
|
);
|
||||||
const member = accessor?.namedChildren.find((c: SyntaxNode) => c.type === 'identifier');
|
const member = accessor?.namedChildren.find((c: SyntaxNode) => c.type === 'identifier');
|
||||||
if (!member) return undefined;
|
if (!member) return undefined;
|
||||||
const receiver = node.previousNamedSibling;
|
const receiver = dartPrevNamed(node);
|
||||||
if (receiver?.type !== 'identifier') return undefined;
|
if (receiver?.type !== 'identifier') return undefined;
|
||||||
const next = node.nextNamedSibling;
|
const next = dartNextNamed(node);
|
||||||
if (next?.type === 'selector' && next.namedChildren.some((c: SyntaxNode) => c.type === 'argument_part')) {
|
if (next?.type === 'selector' && next.namedChildren.some((c: SyntaxNode) => c.type === 'argument_part')) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -322,7 +347,7 @@ function dartTypeArgumentsReceiver(selector: SyntaxNode | null): string | undefi
|
|||||||
if (selector?.type !== 'selector' || selector.namedChildCount !== 1 || selector.namedChild(0)?.type !== 'type_arguments') {
|
if (selector?.type !== 'selector' || selector.namedChildCount !== 1 || selector.namedChild(0)?.type !== 'type_arguments') {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const type = selector.previousNamedSibling;
|
const type = dartPrevNamed(selector);
|
||||||
return type?.type === 'identifier' ? type.text : undefined;
|
return type?.type === 'identifier' ? type.text : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,7 +358,7 @@ function dartTypeArgumentsReceiver(selector: SyntaxNode | null): string | undefi
|
|||||||
* `Foo.create`, a bare `create`, or `Foo` (constructor) — or undefined.
|
* `Foo.create`, a bare `create`, or `Foo` (constructor) — or undefined.
|
||||||
*/
|
*/
|
||||||
function dartCalleeOfArgPart(argPart: SyntaxNode): string | undefined {
|
function dartCalleeOfArgPart(argPart: SyntaxNode): string | undefined {
|
||||||
const prev = argPart.previousNamedSibling;
|
const prev = dartPrevNamed(argPart);
|
||||||
if (!prev) return undefined;
|
if (!prev) return undefined;
|
||||||
if (prev.type === 'identifier') return prev.text; // bare `Foo()` / `create()`
|
if (prev.type === 'identifier') return prev.text; // bare `Foo()` / `create()`
|
||||||
if (prev.type === 'selector') {
|
if (prev.type === 'selector') {
|
||||||
@@ -342,7 +367,7 @@ function dartCalleeOfArgPart(argPart: SyntaxNode): string | undefined {
|
|||||||
);
|
);
|
||||||
const methodId = accessor?.namedChildren.find((c: SyntaxNode) => c.type === 'identifier');
|
const methodId = accessor?.namedChildren.find((c: SyntaxNode) => c.type === 'identifier');
|
||||||
if (methodId) {
|
if (methodId) {
|
||||||
const accessorPrev = prev.previousNamedSibling;
|
const accessorPrev = dartPrevNamed(prev);
|
||||||
if (accessorPrev?.type === 'identifier') return accessorPrev.text + '.' + methodId.text;
|
if (accessorPrev?.type === 'identifier') return accessorPrev.text + '.' + methodId.text;
|
||||||
const typeName = dartTypeArgumentsReceiver(accessorPrev);
|
const typeName = dartTypeArgumentsReceiver(accessorPrev);
|
||||||
if (typeName) return typeName + '.' + methodId.text;
|
if (typeName) return typeName + '.' + methodId.text;
|
||||||
@@ -673,7 +698,8 @@ export const dartExtractor: LanguageExtractor = {
|
|||||||
const hasArgPart = node.namedChildren.some((c: SyntaxNode) => c.type === 'argument_part');
|
const hasArgPart = node.namedChildren.some((c: SyntaxNode) => c.type === 'argument_part');
|
||||||
if (!hasArgPart) return undefined;
|
if (!hasArgPart) return undefined;
|
||||||
|
|
||||||
const prev = node.previousNamedSibling;
|
// Past comments: `box.grow /* by */ (3)` calls `box.grow`.
|
||||||
|
const prev = dartPrevNamed(node);
|
||||||
return prev ? dartCallee(prev) : undefined;
|
return prev ? dartCallee(prev) : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user