mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-11-13 08:51:28 +00:00
fix(dart): extract extension type symbols (#2539)
* fix(dart): extract extension type symbols * test(dart): update extension type benchmark baseline * fix(dart): emit extension type implements heritage * fix(dart): handle generic extension type implements --------- Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
This commit is contained in:
parent
d75e20654d
commit
28a70fc1df
7 changed files with 274 additions and 5 deletions
|
|
@ -79,8 +79,10 @@
|
|||
"_rebaselined_2522_review_fixes": "PR #2522 review fixes: assignment target:/result: fields join the shared fallback. Prior 7687ee2466e16020a12440a03fbda53e63aa05f94b4481f6133c09867a0d560d -> 115c5da807e36bb12fdeba28e44f2b6484ef322ff26c19fa0f191febaf774248; scaling ratio re-verified within budget."
|
||||
},
|
||||
"dart": {
|
||||
"fingerprint": "66a46d5ff09f3d11b2771db0f48596fe7057e95c5bc8f56241fdb911137298c3",
|
||||
"fingerprint": "ba93c90dcd341259e8e088816bc8c76ad27882419f665e35c056dc22fa54cf73",
|
||||
"scaling_budget": 1.5,
|
||||
"_rebaselined_2538": "#2538: Dart extension type headers are preprocessed into normal extension declarations before scope capture, so extension type symbols and their methods are now emitted. Intentional Dart-only capture fingerprint drift; CI measured scaling 1.042 < 1.5.",
|
||||
"_rebaselined_2538_implements": "#2538 tri-review follow-up: Dart extension type implements clauses now emit heritage markers and fixture coverage asserts IMPLEMENTS edges, including multi-arg generic interfaces. Prior committed baseline 66a46d5ff09f3d11b2771db0f48596fe7057e95c5bc8f56241fdb911137298c3 -> ba93c90dcd341259e8e088816bc8c76ad27882419f665e35c056dc22fa54cf73; scaling 0.945 < 1.5.",
|
||||
"_rebaselined_callable_flow_2522_review": "PR #2522 review hardening: callable operands retain expression/qualified identity and formals retain signature metadata. Prior 29ce2bfe70b246b1c9d5e99c0ec11e850c22e9672737592207242b7f4cc824b8 -> 66a46d5ff09f3d11b2771db0f48596fe7057e95c5bc8f56241fdb911137298c3; scaling 1.054 < 1.5.",
|
||||
"_rebaselined_callable_flow_2522_followup": "PR #2522 follow-up: Dart function-tearoff and selector callable flow facts, split signature/body lexical ownership, and invocation-result suppression. Prior 94bf2c26e1ba96f4211634aa572c0a989b503e717e75dfc5df04f66c417de80f -> 29ce2bfe70b246b1c9d5e99c0ec11e850c22e9672737592207242b7f4cc824b8; scaling 0.906 < 1.5.",
|
||||
"_added": "#939: dart added to the scope-capture bench with the registry-primary migration. Heritage-bearing scale source (Entity extends Base implements Marker) gates the @reference.inherits synth + the postfix-chain reference walk at scale. emitDartScopeCaptures threads tree-sitter captured nodes (no findNodeAtRange root-walk), so it is linear (~1.0).",
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ import {
|
|||
dartArityCompatibility,
|
||||
} from './dart/index.js';
|
||||
import { DART_BUILT_INS } from './dart/built-ins.js';
|
||||
import { preprocessDartExtensionTypes } from './dart/extension-type-preprocess.js';
|
||||
|
||||
/**
|
||||
* Resolve the enclosing function from a `function_body` node by looking at its
|
||||
|
|
@ -119,6 +120,7 @@ export const dartProvider = defineLanguage({
|
|||
},
|
||||
] satisfies AstFrameworkPatternConfig[],
|
||||
treeSitterQueries: DART_QUERIES,
|
||||
preprocessSource: preprocessDartExtensionTypes,
|
||||
typeConfig: dartConfig,
|
||||
exportChecker: dartExportChecker,
|
||||
importResolver: createImportResolver(dartImportConfig),
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js';
|
|||
import { encodeMarker } from '../../utils/heritage-marker.js';
|
||||
import { DART_BUILT_INS } from './built-ins.js';
|
||||
import { synthesizeCallableFlowCaptures } from '../../utils/callable-flow-captures.js';
|
||||
import { preprocessDartExtensionTypes } from './extension-type-preprocess.js';
|
||||
|
||||
const FUNCTION_DECL_TAGS = [
|
||||
'@declaration.function',
|
||||
|
|
@ -85,13 +86,14 @@ export function emitDartScopeCaptures(
|
|||
_filePath: string,
|
||||
cachedTree?: unknown,
|
||||
): readonly CaptureMatch[] {
|
||||
const parseText = preprocessDartExtensionTypes(sourceText);
|
||||
let tree: Parser.Tree;
|
||||
if (cachedTree !== undefined && cachedTree !== null) {
|
||||
tree = cachedTree as Parser.Tree;
|
||||
recordCacheHit();
|
||||
} else {
|
||||
tree = parseSourceSafe(getDartParser(), sourceText, undefined, {
|
||||
bufferSize: getTreeSitterBufferSize(sourceText),
|
||||
tree = parseSourceSafe(getDartParser(), parseText, undefined, {
|
||||
bufferSize: getTreeSitterBufferSize(parseText),
|
||||
});
|
||||
recordCacheMiss();
|
||||
}
|
||||
|
|
@ -186,6 +188,10 @@ export function emitDartScopeCaptures(
|
|||
emitHeritage(node, out);
|
||||
return;
|
||||
}
|
||||
if (node.type === 'extension_declaration') {
|
||||
emitExtensionImplementsHeritage(node, out);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
out.push(...synthesizeCallableFlowCaptures(root, DART_CALLABLE_CAPTURE_OPTIONS));
|
||||
|
|
@ -522,6 +528,50 @@ function emitHeritage(classNode: SyntaxNode, out: CaptureMatch[]): void {
|
|||
}
|
||||
}
|
||||
|
||||
function emitExtensionImplementsHeritage(extensionNode: SyntaxNode, out: CaptureMatch[]): void {
|
||||
const nameNode = extensionNode.childForFieldName('name');
|
||||
if (nameNode === null) return;
|
||||
|
||||
const bodyStart = extensionNode.text.indexOf('{');
|
||||
const header = bodyStart === -1 ? extensionNode.text : extensionNode.text.slice(0, bodyStart);
|
||||
const implementsIndex = header.indexOf('implements');
|
||||
if (implementsIndex === -1) return;
|
||||
|
||||
const className = nameNode.text;
|
||||
const interfaces = header.slice(implementsIndex + 'implements'.length);
|
||||
for (const rawInterface of splitTopLevelCommaList(interfaces)) {
|
||||
const target = /^[ \t]*([A-Za-z_$][A-Za-z0-9_$]*)/.exec(rawInterface)?.[1];
|
||||
if (target === undefined) continue;
|
||||
const payload = encodeMarker('heritage', ['implements', target, className]);
|
||||
out.push({ '@import.heritage': syntheticCapture('@import.heritage', nameNode, payload) });
|
||||
}
|
||||
}
|
||||
|
||||
function splitTopLevelCommaList(text: string): string[] {
|
||||
const parts: string[] = [];
|
||||
let start = 0;
|
||||
let angleDepth = 0;
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const ch = text[i];
|
||||
if (ch === '<') {
|
||||
angleDepth++;
|
||||
continue;
|
||||
}
|
||||
if (ch === '>' && angleDepth > 0) {
|
||||
angleDepth--;
|
||||
continue;
|
||||
}
|
||||
if (ch === ',' && angleDepth === 0) {
|
||||
parts.push(text.slice(start, i));
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
parts.push(text.slice(start));
|
||||
return parts;
|
||||
}
|
||||
|
||||
function emitHeritageMarkers(
|
||||
container: SyntaxNode,
|
||||
kind: 'implements' | 'with',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
const DART_EXTENSION_TYPE_HEADER =
|
||||
/\b(extension)([ \t]+type[ \t]+(?:const[ \t]+)?)([A-Za-z_$][A-Za-z0-9_$]*)([ \t]*<[^()\r\n]*>)?([ \t]*)\(([^()\r\n]*)\)/g;
|
||||
|
||||
function representationType(representation: string): string | null {
|
||||
const trimmed = representation.trim();
|
||||
const match = /^(.*\S)\s+[A-Za-z_$][A-Za-z0-9_$]*$/.exec(trimmed);
|
||||
const typeName = match?.[1]?.trim();
|
||||
return typeName && typeName.length > 0 ? typeName : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewrites Dart 3.3 `extension type Name(Type value)` headers into ordinary
|
||||
* extension headers before tree-sitter sees the source. The vendored grammar
|
||||
* currently recovers these declarations through an ERROR subtree, while the
|
||||
* existing Dart ingestion path already handles `extension Name on Type`.
|
||||
*/
|
||||
export function preprocessDartExtensionTypes(sourceText: string): string {
|
||||
return sourceText.replace(
|
||||
DART_EXTENSION_TYPE_HEADER,
|
||||
(
|
||||
match,
|
||||
extensionKeyword: string,
|
||||
extensionTypeGap: string,
|
||||
name: string,
|
||||
typeParameters: string | undefined,
|
||||
beforeRepresentation: string,
|
||||
representation: string,
|
||||
) => {
|
||||
const typeName = representationType(representation);
|
||||
if (typeName === null) return match;
|
||||
|
||||
const representationSpanLength = beforeRepresentation.length + representation.length + 2;
|
||||
const rewrittenRepresentation = ` on ${typeName}`;
|
||||
if (rewrittenRepresentation.length > representationSpanLength) return match;
|
||||
|
||||
return (
|
||||
extensionKeyword +
|
||||
' '.repeat(extensionTypeGap.length) +
|
||||
name +
|
||||
(typeParameters ?? '') +
|
||||
rewrittenRepresentation +
|
||||
' '.repeat(representationSpanLength - rewrittenRepresentation.length)
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
@ -249,7 +249,7 @@ export const CONTAINER_TYPE_TO_LABEL: Record<string, string> = {
|
|||
record_declaration: 'Record',
|
||||
protocol_declaration: 'Interface',
|
||||
mixin_declaration: 'Mixin',
|
||||
extension_declaration: 'Extension',
|
||||
extension_declaration: 'Class',
|
||||
class: 'Class',
|
||||
// Ruby `module` declarations map to `Trait` so they participate in the
|
||||
// class-like type registry used by `lookupClassByName` / inheritance
|
||||
|
|
|
|||
28
gitnexus/test/fixtures/lang-resolution/dart-extension-types/models.dart
vendored
Normal file
28
gitnexus/test/fixtures/lang-resolution/dart-extension-types/models.dart
vendored
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
class Identifiable {}
|
||||
|
||||
class SequenceLike<T> {}
|
||||
|
||||
class Comparator<A, B> {}
|
||||
|
||||
extension type const UserId(String value) implements Identifiable {
|
||||
String describe() => value;
|
||||
}
|
||||
|
||||
extension type const EmptyId(String value) {}
|
||||
|
||||
extension type Celsius(double degrees) {
|
||||
double toFahrenheit() => degrees * 9 / 5 + 32;
|
||||
}
|
||||
|
||||
extension type Box<T>(List<T> value) implements SequenceLike<T> {
|
||||
T first() => value.first;
|
||||
}
|
||||
|
||||
extension type Pair(String value) implements Comparator<String, int> {
|
||||
String describePair() => value;
|
||||
}
|
||||
|
||||
extension Fancy on String {
|
||||
int get doubledLength => length * 2;
|
||||
String shout() => toUpperCase();
|
||||
}
|
||||
|
|
@ -18,7 +18,15 @@
|
|||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import path from 'path';
|
||||
import { emitDartScopeCaptures } from '../../../src/core/ingestion/languages/dart/captures.js';
|
||||
import { FIXTURES, getNodesByLabel, runPipelineFromRepo, type PipelineResult } from './helpers.js';
|
||||
import {
|
||||
FIXTURES,
|
||||
edgeSet,
|
||||
findDanglingEdges,
|
||||
getNodesByLabel,
|
||||
getRelationships,
|
||||
runPipelineFromRepo,
|
||||
type PipelineResult,
|
||||
} from './helpers.js';
|
||||
import {
|
||||
isLanguageAvailable,
|
||||
loadParser,
|
||||
|
|
@ -26,6 +34,7 @@ import {
|
|||
} from '../../../src/core/tree-sitter/parser-loader.js';
|
||||
import { SupportedLanguages } from '../../../src/config/supported-languages.js';
|
||||
import type { CaptureMatch } from 'gitnexus-shared';
|
||||
import { preprocessDartExtensionTypes } from '../../../src/core/ingestion/languages/dart/extension-type-preprocess.js';
|
||||
|
||||
let dartAvailable = isLanguageAvailable(SupportedLanguages.Dart);
|
||||
if (dartAvailable) {
|
||||
|
|
@ -43,6 +52,35 @@ typedef Pred = bool Function(int);
|
|||
typedef Mapper<T> = T Function(T);
|
||||
typedef int _Internal(int);`;
|
||||
|
||||
const EXTENSION_TYPES = `class Identifiable {}
|
||||
|
||||
class SequenceLike<T> {}
|
||||
|
||||
class Comparator<A, B> {}
|
||||
|
||||
extension type const UserId(String value) implements Identifiable {
|
||||
String describe() => value;
|
||||
}
|
||||
|
||||
extension type const EmptyId(String value) {}
|
||||
|
||||
extension type Celsius(double degrees) {
|
||||
double toFahrenheit() => degrees * 9 / 5 + 32;
|
||||
}
|
||||
|
||||
extension type Box<T>(List<T> value) implements SequenceLike<T> {
|
||||
T first() => value.first;
|
||||
}
|
||||
|
||||
extension type Pair(String value) implements Comparator<String, int> {
|
||||
String describePair() => value;
|
||||
}
|
||||
|
||||
extension Fancy on String {
|
||||
int get doubledLength => length * 2;
|
||||
String shout() => toUpperCase();
|
||||
}`;
|
||||
|
||||
/** All @declaration.type_alias matches, as (name) tuples. */
|
||||
function typeAliasNames(src: string): string[] {
|
||||
const matches = emitDartScopeCaptures(src, 'test.dart') as CaptureMatch[];
|
||||
|
|
@ -52,6 +90,21 @@ function typeAliasNames(src: string): string[] {
|
|||
.filter((n): n is string => Boolean(n));
|
||||
}
|
||||
|
||||
/** All class-like Dart declaration matches, as names. */
|
||||
function classDeclarationNames(src: string): string[] {
|
||||
const matches = emitDartScopeCaptures(src, 'test.dart') as CaptureMatch[];
|
||||
return matches
|
||||
.filter((m) => m['@declaration.class'] !== undefined)
|
||||
.map((m) => m['@declaration.name']?.text)
|
||||
.filter((n): n is string => Boolean(n));
|
||||
}
|
||||
|
||||
/** Synthetic Dart implements markers, as marker payloads. */
|
||||
function heritageImports(src: string): string[] {
|
||||
const matches = emitDartScopeCaptures(src, 'test.dart') as CaptureMatch[];
|
||||
return matches.map((m) => m['@import.heritage']?.text).filter((n): n is string => Boolean(n));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// F28 — typedef capture (scope layer)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -88,6 +141,45 @@ describe.skipIf(!dartAvailable)('F28 — Dart typedef capture (scope layer)', ()
|
|||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// #2538 — extension type declarations (scope layer)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe.skipIf(!dartAvailable)('Dart extension type declarations (scope layer)', () => {
|
||||
it('rewrites extension type headers without changing source length or line count', () => {
|
||||
const rewritten = preprocessDartExtensionTypes(EXTENSION_TYPES);
|
||||
expect(rewritten).toHaveLength(EXTENSION_TYPES.length);
|
||||
expect(rewritten.split('\n')).toHaveLength(EXTENSION_TYPES.split('\n').length);
|
||||
for (const name of ['UserId', 'EmptyId', 'Celsius', 'Box', 'Pair']) {
|
||||
expect(rewritten.indexOf(name)).toBe(EXTENSION_TYPES.indexOf(name));
|
||||
}
|
||||
expect(rewritten).toContain('UserId on String');
|
||||
expect(rewritten).toContain('EmptyId on String');
|
||||
expect(rewritten).toContain('Celsius on double');
|
||||
expect(rewritten).toContain('Box<T> on List<T>');
|
||||
expect(rewritten).toContain('Pair on String');
|
||||
});
|
||||
|
||||
it('captures extension types as class-like declarations', () => {
|
||||
const names = classDeclarationNames(EXTENSION_TYPES);
|
||||
expect(names).toEqual(
|
||||
expect.arrayContaining(['UserId', 'EmptyId', 'Celsius', 'Box', 'Pair', 'Fancy']),
|
||||
);
|
||||
});
|
||||
|
||||
it('captures implements clauses on extension types as heritage markers', () => {
|
||||
const imports = heritageImports(EXTENSION_TYPES);
|
||||
expect(imports).toEqual(
|
||||
expect.arrayContaining([
|
||||
'__heritage__:implements:Identifiable:UserId',
|
||||
'__heritage__:implements:SequenceLike:Box',
|
||||
'__heritage__:implements:Comparator:Pair',
|
||||
]),
|
||||
);
|
||||
expect(imports).not.toContain('__heritage__:implements:int:Pair');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// F28 — typedef symbols exist end-to-end (structure phase)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -116,3 +208,52 @@ describe.skipIf(!dartAvailable)('F28 — Dart typedef symbols (end-to-end)', ()
|
|||
expect(fromFixture.sort()).toEqual(['Cmp', 'Cmp2', 'Mapper', 'Pred', '_Internal'].sort());
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// #2538 — extension type symbols exist end-to-end (structure phase)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe.skipIf(!dartAvailable)('Dart extension type symbols (end-to-end)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(path.join(FIXTURES, 'dart-extension-types'), () => {});
|
||||
}, 60000);
|
||||
|
||||
it('creates Class nodes for extension type declarations', () => {
|
||||
const classes = getNodesByLabel(result, 'Class');
|
||||
expect(classes).toEqual(
|
||||
expect.arrayContaining(['UserId', 'EmptyId', 'Celsius', 'Box', 'Pair', 'Fancy']),
|
||||
);
|
||||
});
|
||||
|
||||
it('emits IMPLEMENTS edges for extension type implements clauses', () => {
|
||||
const implementsEdges = edgeSet(getRelationships(result, 'IMPLEMENTS'));
|
||||
expect(implementsEdges).toEqual(
|
||||
expect.arrayContaining(['Box → SequenceLike', 'Pair → Comparator', 'UserId → Identifiable']),
|
||||
);
|
||||
expect(implementsEdges).not.toContain('Pair → int');
|
||||
});
|
||||
|
||||
it('keeps extension type methods owned by their extension type symbol', () => {
|
||||
const methods = getNodesByLabel(result, 'Method');
|
||||
expect(methods).toEqual(
|
||||
expect.arrayContaining(['describe', 'toFahrenheit', 'first', 'describePair', 'shout']),
|
||||
);
|
||||
|
||||
const hasMethod = edgeSet(getRelationships(result, 'HAS_METHOD'));
|
||||
expect(hasMethod).toEqual(
|
||||
expect.arrayContaining([
|
||||
'UserId → describe',
|
||||
'Celsius → toFahrenheit',
|
||||
'Box → first',
|
||||
'Pair → describePair',
|
||||
'Fancy → shout',
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('does not leave dangling method ownership edges', () => {
|
||||
expect(findDanglingEdges(result, ['HAS_METHOD', 'IMPLEMENTS'])).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue