Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- PHP static calls through imported class aliases now reach the correct class when services and repositories share method names, so callers and impact analysis show the right dependencies after re-indexing. (#1545)
- TypeScript/JavaScript: a call through a field of the enclosing class — `this.mailer.send()` — now resolves on the field's declared type, so a delegating wrapper that shares the method's name no longer records itself as its own callee and `callers`, `impact` and trace stop lying on that shape. A field whose type is external or a builtin stays unresolved rather than guessed. Re-index after upgrading. (#1496)
- TypeScript and JavaScript collection calls through local variables and their nested properties no longer link to unrelated project methods; re-index after upgrading. (#1566)
- **Dart `extension type` members are indexed again.** A Dart 3 `extension type` and everything declared inside it had stopped appearing in the graph, so its methods showed no callers and calls into them went nowhere. They are back, and now belong to the extension type itself rather than looking like loose top-level functions. Re-index to pick them up. (#1784)

- Objective-C headers now index in a project that has no `.m` file. A `.h` file is read as C from its name alone, and only later — once its contents are read — recognized as Objective-C; the grammar for that was never loaded up front, so the file failed with a parser error and nothing in it reached the index. Adding any `.m` file used to make the same header work, which is what made this look arbitrary. Thanks @Juddd. (#1628)

Expand Down
66 changes: 66 additions & 0 deletions __tests__/dart-extension-type.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* Dart 3 `extension type` members (#1784).
*
* Dart spells an ordinary implemented method `method_signature`, the same node
* type TypeScript uses for a bodiless interface member. #1780 gated that node
* type behind `isInsideClassLikeNode()` to stop a TS interface member minting a
* phantom free function — correct for TS, but an `extension type` body was not
* class-like, so its members stopped being indexed at all.
*/
import { describe, it, expect, beforeAll } from 'vitest';
import { extractFromSource } from '../src/extraction';
import { initGrammars, loadAllGrammars } from '../src/extraction/grammars';

beforeAll(async () => {
await initGrammars();
await loadAllGrammars();
});

describe('Dart extension type members (#1784)', () => {
it('indexes an extension type member, as a method of the extension type', () => {
const code = `extension type Meters(double value) {
double get km => value / 1000;
void show() {
print(km);
}
}
`;
const result = extractFromSource('meters.dart', code);
const kinds = result.nodes.filter((n) => n.kind !== 'file').map((n) => `${n.kind}:${n.qualifiedName}`).sort();
expect(kinds).toContain('class:Meters');
expect(kinds).toContain('method:Meters::km');
expect(kinds).toContain('method:Meters::show');
});

it('names an extension type constructor after the constructor, not the type', () => {
const code = `extension type Meters(double value) {
Meters.fromKm(double km) : this(km * 1000);
factory Meters.zero() => Meters(0);
}
`;
const result = extractFromSource('meters.dart', code);
const ctors = result.nodes.filter((n) => n.kind === 'method');
expect(ctors.map((n) => n.qualifiedName).sort()).toEqual(['Meters::fromKm', 'Meters::zero']);
for (const c of ctors) expect(c.returnType).toBe('Meters');
});

it('leaves extension, mixin and class bodies alone', () => {
const code = `extension StringHelpers on String {
String shout() => toUpperCase();
}

mixin Logger {
void log(String m) {}
}

class Widget {
void build() {}
}
`;
const result = extractFromSource('rest.dart', code);
const kinds = result.nodes.filter((n) => n.kind !== 'file').map((n) => `${n.kind}:${n.qualifiedName}`).sort();
expect(kinds).toContain('method:StringHelpers::shout');
expect(kinds).toContain('method:Logger::log');
expect(kinds).toContain('method:Widget::build');
});
});
11 changes: 9 additions & 2 deletions codegraph-kernel/src/dart.rs
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,8 @@ impl<'t> Walker<'t> {
while let Some(parent) = p {
if matches!(
parent.kind(),
"class_definition" | "mixin_declaration" | "extension_declaration" | "enum_declaration"
"class_definition" | "mixin_declaration" | "extension_declaration"
| "extension_type_declaration" | "enum_declaration"
) {
return parent.child_by_field_name("name").map(|n| self.text(n));
}
Expand Down Expand Up @@ -630,7 +631,13 @@ impl<'t> Walker<'t> {
self.extract_function(node);
return;
}
"class_definition" | "mixin_declaration" | "extension_declaration" => {
// `extension_type_declaration` is Dart 3's `extension type Meters(double v)`.
// Mirrors dart.ts's extraClassNodeTypes: its body holds ordinary members,
// and a `method_signature` is only extracted when its enclosing node is
// class-like, so both arms must agree on this list or they disagree on
// every member of an extension type (#1784).
"class_definition" | "mixin_declaration" | "extension_declaration"
| "extension_type_declaration" => {
self.extract_class(node);
return;
}
Expand Down
13 changes: 11 additions & 2 deletions src/extraction/languages/dart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ function dartEnclosingTypeName(node: SyntaxNode): string | undefined {
while (p) {
if (
p.type === 'class_definition' || p.type === 'mixin_declaration' ||
p.type === 'extension_declaration' || p.type === 'enum_declaration'
p.type === 'extension_declaration' || p.type === 'extension_type_declaration' ||
p.type === 'enum_declaration'
) {
return p.childForFieldName('name')?.text;
}
Expand Down Expand Up @@ -132,7 +133,15 @@ export const dartExtractor: LanguageExtractor = {
importTypes: ['import_or_export'],
callTypes: [], // Dart calls use identifier+selector, handled via extractBareCall
variableTypes: [],
extraClassNodeTypes: ['mixin_declaration', 'extension_declaration'],
// `extension_type_declaration` is Dart 3's `extension type Meters(double v)`.
// It belongs here for the same reason `mixin`/`extension` do — its body holds
// ordinary members — but it also has to be here for them to be indexed AT ALL:
// Dart spells an implemented method `method_signature`, the node type #1780
// gated behind `isInsideClassLikeNode()` to stop a bodiless TS interface
// member minting a phantom free function. Without the extension type itself
// being a class-like node, its body is not class-like, so every
// `method_signature` in it failed that gate and was dropped (#1784).
extraClassNodeTypes: ['mixin_declaration', 'extension_declaration', 'extension_type_declaration'],
// A Dart `static_final_declaration` is exactly a top-level or class-`static`
// `const`/`final` — the shared-constant idiom — so extract it as `constant`
// for value-reference edges. Instance fields, `var`, and typed declarations
Expand Down