mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-06 02:49:56 +00:00
* 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>
259 lines
9.6 KiB
TypeScript
259 lines
9.6 KiB
TypeScript
/**
|
|
* Regression tests for Dart scope-resolution / structure coverage gaps
|
|
* (issue #1919). Mirrors python-parsing-coverage.test.ts: the F28 scope-capture
|
|
* assertions exercise emitDartScopeCaptures directly, and a pipeline check
|
|
* verifies the TypeAlias symbol exists end-to-end.
|
|
*
|
|
* F28 — old-style function typedef (`typedef int Cmp(int a, int b);`) was never
|
|
* captured: DART_SCOPE_QUERY had no type_alias rule, and DART_QUERIES only
|
|
* captured the new-style (`=`-anchored) form. Both forms must now surface as a
|
|
* type-alias declaration / TypeAlias symbol.
|
|
*
|
|
* #1919 review CF2 — the GENERIC forms (`typedef int Cmp2<T>(T a, T b);` and
|
|
* `typedef Mapper<T> = T Function(T);`) were still dropped: a generic
|
|
* type_parameters node sits between the alias name and the next anchor, so the
|
|
* non-generic adjacency patterns never matched. Standalone generic patterns now
|
|
* capture them too.
|
|
*/
|
|
import { describe, it, expect, beforeAll } from 'vitest';
|
|
import path from 'path';
|
|
import { emitDartScopeCaptures } from '../../../src/core/ingestion/languages/dart/captures.js';
|
|
import {
|
|
FIXTURES,
|
|
edgeSet,
|
|
findDanglingEdges,
|
|
getNodesByLabel,
|
|
getRelationships,
|
|
runPipelineFromRepo,
|
|
type PipelineResult,
|
|
} from './helpers.js';
|
|
import {
|
|
isLanguageAvailable,
|
|
loadParser,
|
|
loadLanguage,
|
|
} 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) {
|
|
try {
|
|
await loadParser();
|
|
await loadLanguage(SupportedLanguages.Dart);
|
|
} catch {
|
|
dartAvailable = false;
|
|
}
|
|
}
|
|
|
|
const TYPEDEFS = `typedef int Cmp(int a, int b);
|
|
typedef int Cmp2<T>(T a, T b);
|
|
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[];
|
|
return matches
|
|
.filter((m) => m['@declaration.type_alias'] !== undefined)
|
|
.map((m) => m['@declaration.name']?.text)
|
|
.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)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe.skipIf(!dartAvailable)('F28 — Dart typedef capture (scope layer)', () => {
|
|
it('captures the old-style function typedef as a type-alias declaration', () => {
|
|
const names = typeAliasNames(TYPEDEFS);
|
|
expect(names).toContain('Cmp');
|
|
});
|
|
|
|
it('still captures the new-style typedef (regression)', () => {
|
|
const names = typeAliasNames(TYPEDEFS);
|
|
expect(names).toContain('Pred');
|
|
});
|
|
|
|
it('captures a private old-style typedef', () => {
|
|
const names = typeAliasNames(TYPEDEFS);
|
|
expect(names).toContain('_Internal');
|
|
});
|
|
|
|
it('captures the generic old-style typedef (CF2)', () => {
|
|
const names = typeAliasNames(TYPEDEFS);
|
|
expect(names).toContain('Cmp2');
|
|
});
|
|
|
|
it('captures the generic new-style typedef (CF2)', () => {
|
|
const names = typeAliasNames(TYPEDEFS);
|
|
expect(names).toContain('Mapper');
|
|
});
|
|
|
|
it('emits exactly one declaration per typedef (no double-match)', () => {
|
|
const names = typeAliasNames(TYPEDEFS);
|
|
expect(names.sort()).toEqual(['Cmp', 'Cmp2', 'Pred', 'Mapper', '_Internal'].sort());
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// #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)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe.skipIf(!dartAvailable)('F28 — Dart typedef symbols (end-to-end)', () => {
|
|
let result: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
result = await runPipelineFromRepo(path.join(FIXTURES, 'dart-coverage'), () => {});
|
|
}, 60000);
|
|
|
|
it('creates TypeAlias nodes for old-style, new-style, generic, and private typedefs', () => {
|
|
const aliases = getNodesByLabel(result, 'TypeAlias');
|
|
expect(aliases).toContain('Cmp'); // old-style (covers F28)
|
|
expect(aliases).toContain('Cmp2'); // generic old-style (covers CF2)
|
|
expect(aliases).toContain('Pred'); // new-style (regression)
|
|
expect(aliases).toContain('Mapper'); // generic new-style (covers CF2)
|
|
expect(aliases).toContain('_Internal'); // private old-style
|
|
});
|
|
|
|
it('emits exactly one TypeAlias per typedef (no duplicates)', () => {
|
|
const aliases = getNodesByLabel(result, 'TypeAlias');
|
|
const fromFixture = aliases.filter((n) =>
|
|
['Cmp', 'Cmp2', 'Pred', 'Mapper', '_Internal'].includes(n),
|
|
);
|
|
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([]);
|
|
});
|
|
});
|