mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-05 02:43:32 +00:00
* feat(ingestion): tree-sitter node-type/field validation gate + remove dead literals
Add a CI gate (test/integration/grammar-literal-validation.test.ts) validating
every node-type and field-name literal in the ingestion code layer against each
grammar's node-types.json, with a live `new Parser.Query` probe fallback for
literals the static JSON under-reports. Covers all three surfaces:
- legacy Call-Resolution DAG (type-extractors, *-extractors/configs) + the
ungated structure phase (field/method extractors, export-detection) — AST scan;
- registry scope-resolution captures + scope queries (Mode 3 compile);
- the registry RESOLUTION layer (scope-resolver/type-binding/receiver-binding/
interpret/arity/import-decomposer …) via a TS-TypeChecker discriminator that
collects a literal ONLY when its `.type` receiver is a tree-sitter SyntaxNode
(so resolved-symbol `.type` kinds like 'Class' are never mistaken for nodes).
Helpers: test/helpers/{grammar-introspection,literal-collectors}.ts.
Remove every existence-dead literal the gate surfaces (behavior-neutral
dead-branch/fallback deletions verified absent from the installed grammar),
spanning the legacy, structure-phase, and registry production paths:
reference_type/pointer_type/scoped_identifier/scoped_type_identifier/
rvalue_reference_declarator/variadic_parameter (C/C++), equals_value_clause/
identifier_name/simple_identifier/record_struct_declaration/record_class_declaration
(C#), generic_type/`type` field (Dart), nullable_type (PHP), method_call/symbol
(Ruby), method_call_expression/slice_type/shorthand_field_pattern (Rust),
struct_declaration/internal_name (Swift), comment (Java), parameter/
parameterized_type and dead childForFieldName('pattern'|'modifiers'|
'formal_parameters'|'declaration'|'default'|'return_value'|'alias_clause') /
class_expression fallbacks. Gate ships with an empty allowlist.
One behavior FIX (scope-resolution): PHP `findEnclosingTypeDeclaration` omitted
`anonymous_class`, so a method inside an anonymous class mis-bound `$this` to the
enclosing named class; add `anonymous_class` so it is correctly skipped.
Verified: tsc clean; gate green (empty allowlist); scope-resolution parity 26/26
on both REGISTRY_PRIMARY_*=0 and =1; resolver suite no new failures.
Issue #1920 (epic #1919).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(ingestion): assert real grammar node types in #1920 dead-literal tests
Three tests asserted defensive handling of node types the installed
grammars never emit (verified via real tree-sitter parse), so they broke
once the dead literals were removed in af9d709f:
- parsing.test.ts isNodeExported / csharp: `record struct` and `record
class` both parse to `record_declaration` (kept in CSHARP_DECL_TYPES) —
tree-sitter-c-sharp emits no `record_struct_declaration` /
`record_class_declaration` node. Switch the two mock nodes to
`record_declaration`.
- extract-generic-type-args.test.ts: Java emits `generic_type` and Kotlin
`user_type`+`type_projection`; `parameterized_type` is produced by no
installed grammar, so the shared extractor returns [] for it. Convert the
case to a documented negative assertion (real paths already covered by the
generic_type cases).
No source behavior change: production export detection (record_declaration)
and generic type-arg extraction (generic_type / type_projection) were
already correct. Fixes the 3 CI failures on PR #1937.
Issue #1920 (epic #1919).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(ingestion): keep parameterized_type generic-arg extraction (allowlisted)
Restore the `parameterized_type` branch in extractSimpleTypeName /
extractGenericTypeArgs (type-extractors/shared.ts) so a parameterized_type
node still yields its type arguments (List<User> -> [User]). Current
tree-sitter-java emits `generic_type` and tree-sitter-kotlin
`user_type`+`type_projection`, so this is a defensive alternate node kept
for grammar-version resilience; it is allowlisted in the node-type
validation gate with a documented justification rather than removed.
extract-generic-type-args.test.ts now asserts the User type argument is
captured from a parameterized_type node.
Issue #1920 (epic #1919).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ingestion): extract generic args from real grammar nodes, drop parameterized_type guess
extractGenericTypeArgs / extractSimpleTypeName special-cased `parameterized_type`,
a node type NO installed grammar emits (real parse: Java/TypeScript/Rust ->
generic_type, C# -> generic_name, Kotlin -> user_type). It was a guess masking a
real gap; remove it.
The genuine 'Kotlin alternate node type' is `user_type` (`List<User>` parses to
user_type > [type_identifier, type_arguments]), which the extractor returned []
for. Handle it: read a user_type's own type_arguments, else recurse into its
wrapped child (preserving the existing user_type > generic_type unwrap). No
production caller passes user_type today (Kotlin generics resolve via jvm.ts), so
this only makes the function's documented Kotlin contract correct — zero
behaviour change for current callers (Java/TS/C#/Rust pass generic_type/name).
Replace the mock parameterized_type test with REAL-PARSE coverage across
Java/TypeScript/C#/Rust/Kotlin (+ Java Map<String,User>) so a wrong node-type
guess can't silently pass again. Gate allowlist returns to empty.
Issue #1920 (epic #1919).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* style(test): wrap real-parse cases to prettier printWidth (CI format gate)
CI runs `prettier --check .` from the repo root (printWidth 100) and flagged the
new real-parse cases array's long single-line object literals. Wrap them.
Format-only; no behaviour change.
Issue #1920 (epic #1919).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(ingestion): node-scoped field probe oracle for the literal gate (U1)
Add probeField(language, nodeType, field) — the node-scoped analogue of
probeNodeType: compiles `(<nodeType> <field>: (_)) @_` against the live
grammar and classifies TSQueryErrorStructure/Field -> dead, TSQueryErrorNodeType
(node absent here) -> unavailable, compile -> valid. Conservative-toward-valid
(supertype-typed fields make some wrong fields compile), so it never produces a
false positive. Add isFieldError classifier; make validateField's node-scoped
path membership-then-probe so node-types.json field under-reporting can't yield
a false `dead`.
Foundation for the node-scoped field validation gate (no gate behavior change
yet). Issue #1920 (epic #1919).
* test(ingestion): capture receiver node type + extend Mode-4 to type-env.ts (U2)
CollectedField gains receiverNodeType, captured conservatively by
receiverNodeTypeOf: only when a childForFieldName receiver is unambiguously
narrowed by a single enclosing positive guard (if (recv.type==='X') then-branch,
or switch case 'X') with no reassignment/shadowing of the receiver in the
enclosing function. Any uncertainty -> undefined (sound global fallback);
fail-safe (benign false negative, never a false positive).
Extend Mode-4's resolutionLayerFiles to include shared resolution files directly
under ingestion/ (type-env.ts), tagged with the full gated language set via
fileLanguages (valid-if-any). Entries now carry a language SET. Rename
Mode2Result -> ScanResult; fix the header doc (THREE -> FOUR modes).
Gate behavior unchanged until U3 consumes receiverNodeType. Issue #1920.
* feat(ingestion): node-scoped field gate + remove gate-flagged dead literals (U3, U4)
U3: the gate validates childForFieldName lookups node-scoped (validateField with
the captured receiverNodeType) and fails loudly on a degraded/vacuous run
(asserts resolutionLayerProgramOk, floors collected counts, requires
knownFailures empty).
U4: remove every dead field/literal the hardened gate flags — all behavior-neutral
(the dead disjunct never fired on reachable nodes; verified by real parse + the
type-extractor/resolution unit suites, 484 passing):
- type-env.ts: parameterized_type (emitted by no grammar) and switch_block_label
(real Java enhanced switch is switch_label/switch_rule) from the SyntaxNode .type sets
- languages/csharp/captures.ts: generic_name has no `name` field -> firstNamedChild
- type-extractors/jvm.ts: Kotlin property_declaration has no name/type fields
(positional children) -> findChild; drop the else-branch `pattern` fallbacks x2
- type-extractors/csharp.ts: drop the else-branch `pattern` fallback (parity with go/php/python/swift)
Gate green with node-scoped validation on; tsc clean. Closes the Mode-4
type-env coverage opened in U2. Latent follow-up: Java enhanced-switch arms
(switch_rule) are absent from NARROWING_BRANCH_TYPES — a separate behavior fix.
Issue #1920 (epic #1919).
* fix(java): exclude interleaved comments from call arity (U5)
tree-sitter-java emits block_comment/line_comment as named children of
argument_list; counting them inflated @reference.arity / @reference.parameter-
types / @reference.arg-names for any Java call with an inline comment, which
skews arity-based overload resolution (arity feeds call-processor symbol-ID
generation). Filter them at the single arg-list site (also corrects the
downstream args.map). The previously-removed `comment` literal never matched —
the real nodes are block_comment/line_comment (the #1920 gate lesson).
Isolated from the behavior-neutral gate units (U1-U4) since this changes
production graph output. Java resolver suite 178/178; new java-call-arity test
covers block/line comments, leading comment, constructor calls, and the
no-comment regression. Issue #1920 (epic #1919).
* test(ingestion): cover Kotlin/C# multi-arg generics + tighten probe assertions (U6)
- extract-generic-type-args: add real-parse Kotlin Map<String,User>
(user_type > type_arguments > type_projection) and C# Dictionary<string,User>
(generic_name > type_argument_list) multi-arg cases.
- grammar-introspection: the probeNodeType test now asserts 'dead' for a bogus
node on installed grammars (not merely not-throw), and documents the null-model
split (validateField -> unavailable; validateNodeType -> still probes the live
grammar). Issue #1920 (epic #1919).
* style(test): apply root prettier formatting (CI format gate)
CI runs `prettier --check .` from the repo root (printWidth 100); the gitnexus/
pre-commit hook formatted these two files differently. Format-only, no behavior
change. Issue #1920.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
948 lines
38 KiB
TypeScript
948 lines
38 KiB
TypeScript
/**
|
|
* P1 Integration Tests: Tree-sitter Parsing
|
|
*
|
|
* Tests parsing of sample files via tree-sitter.
|
|
* Covers hardening fixes: Swift init constructor (#18),
|
|
* PHP export detection (#20), symbol ID with startLine (#19),
|
|
* definition node range (#22).
|
|
*/
|
|
import { describe, it, expect } from 'vitest';
|
|
import fs from 'fs/promises';
|
|
import path from 'path';
|
|
import { loadParser, loadLanguage } from '../../src/core/tree-sitter/parser-loader.js';
|
|
import { getLanguageFromFilename, SupportedLanguages } from 'gitnexus-shared';
|
|
import { getProvider } from '../../src/core/ingestion/languages/index.js';
|
|
|
|
const FIXTURES_DIR = path.join(process.cwd(), 'test', 'fixtures', 'sample-code');
|
|
|
|
// Test helper: replaces the removed isNodeExported wrapper using the provider pattern.
|
|
function isNodeExported(node: any, name: string, language: string): boolean {
|
|
const provider = (getProvider as any)(language);
|
|
if (!provider) return false;
|
|
return provider.exportChecker(node, name);
|
|
}
|
|
|
|
/**
|
|
* Minimal mock of a tree-sitter AST node.
|
|
*/
|
|
function mockNode(
|
|
type: string,
|
|
text: string = '',
|
|
parent?: any,
|
|
children?: any[],
|
|
fields?: Record<string, any>,
|
|
): any {
|
|
const node: any = {
|
|
type,
|
|
text,
|
|
parent: parent || null,
|
|
childCount: children?.length ?? 0,
|
|
child: (i: number) => children?.[i] ?? null,
|
|
childForFieldName: (name: string) => fields?.[name] ?? null,
|
|
};
|
|
// Set parent references on children
|
|
if (children) {
|
|
for (const child of children) {
|
|
child.parent = node;
|
|
}
|
|
}
|
|
return node;
|
|
}
|
|
|
|
// ─── isNodeExported per-language ─────────────────────────────────────
|
|
|
|
describe('parsing', () => {
|
|
describe('isNodeExported', () => {
|
|
// TypeScript/JavaScript
|
|
describe('typescript', () => {
|
|
it('returns true when ancestor is export_statement', () => {
|
|
const exportStmt = mockNode('export_statement', 'export function foo() {}');
|
|
const fnDecl = mockNode('function_declaration', 'function foo() {}', exportStmt);
|
|
const nameNode = mockNode('identifier', 'foo', fnDecl);
|
|
expect(isNodeExported(nameNode, 'foo', 'typescript')).toBe(true);
|
|
});
|
|
|
|
it('returns false for non-exported function', () => {
|
|
const fnDecl = mockNode('function_declaration', 'function foo() {}');
|
|
const nameNode = mockNode('identifier', 'foo', fnDecl);
|
|
expect(isNodeExported(nameNode, 'foo', 'typescript')).toBe(false);
|
|
});
|
|
|
|
it('returns true when text starts with "export "', () => {
|
|
const parent = mockNode('lexical_declaration', 'export const foo = 1');
|
|
const nameNode = mockNode('identifier', 'foo', parent);
|
|
expect(isNodeExported(nameNode, 'foo', 'typescript')).toBe(true);
|
|
});
|
|
});
|
|
|
|
// Python
|
|
describe('python', () => {
|
|
it('public function (no underscore prefix)', () => {
|
|
const node = mockNode('identifier', 'public_function');
|
|
expect(isNodeExported(node, 'public_function', 'python')).toBe(true);
|
|
});
|
|
|
|
it('private function (underscore prefix)', () => {
|
|
const node = mockNode('identifier', '_private_helper');
|
|
expect(isNodeExported(node, '_private_helper', 'python')).toBe(false);
|
|
});
|
|
|
|
it('dunder method is private', () => {
|
|
const node = mockNode('identifier', '__init__');
|
|
expect(isNodeExported(node, '__init__', 'python')).toBe(false);
|
|
});
|
|
});
|
|
|
|
// Go
|
|
describe('go', () => {
|
|
it('uppercase first letter is exported', () => {
|
|
const node = mockNode('identifier', 'ExportedFunction');
|
|
expect(isNodeExported(node, 'ExportedFunction', 'go')).toBe(true);
|
|
});
|
|
|
|
it('lowercase first letter is unexported', () => {
|
|
const node = mockNode('identifier', 'unexportedFunction');
|
|
expect(isNodeExported(node, 'unexportedFunction', 'go')).toBe(false);
|
|
});
|
|
|
|
it('empty name is not exported', () => {
|
|
const node = mockNode('identifier', '');
|
|
expect(isNodeExported(node, '', 'go')).toBe(false);
|
|
});
|
|
});
|
|
|
|
// Rust
|
|
describe('rust', () => {
|
|
it('pub function is exported', () => {
|
|
const visMod = mockNode('visibility_modifier', 'pub');
|
|
const nameNode = mockNode('identifier', 'foo');
|
|
// visibility_modifier is a sibling of the name inside function_item
|
|
const fnDecl = mockNode('function_item', 'pub fn foo() {}', undefined, [visMod, nameNode]);
|
|
expect(isNodeExported(nameNode, 'foo', 'rust')).toBe(true);
|
|
});
|
|
|
|
it('non-pub function is not exported', () => {
|
|
const nameNode = mockNode('identifier', 'foo');
|
|
const fnDecl = mockNode('function_item', 'fn foo() {}', undefined, [nameNode]);
|
|
expect(isNodeExported(nameNode, 'foo', 'rust')).toBe(false);
|
|
});
|
|
});
|
|
|
|
// PHP (hardening fix #20)
|
|
describe('php', () => {
|
|
it('top-level function is exported (globally accessible)', () => {
|
|
// PHP: top-level functions fall through all checks and return true
|
|
const program = mockNode('program', '<?php function topLevel() {}');
|
|
const fnDecl = mockNode('function_definition', 'function topLevel() {}', program);
|
|
const nameNode = mockNode('name', 'topLevel', fnDecl);
|
|
expect(isNodeExported(nameNode, 'topLevel', 'php')).toBe(true);
|
|
});
|
|
|
|
it('class declaration is exported', () => {
|
|
const classDecl = mockNode('class_declaration', 'class Foo {}');
|
|
const nameNode = mockNode('name', 'Foo', classDecl);
|
|
expect(isNodeExported(nameNode, 'Foo', 'php')).toBe(true);
|
|
});
|
|
|
|
it('public method has visibility_modifier = public', () => {
|
|
const visMod = mockNode('visibility_modifier', 'public');
|
|
const nameNode = mockNode('name', 'addUser', visMod);
|
|
expect(isNodeExported(nameNode, 'addUser', 'php')).toBe(true);
|
|
});
|
|
|
|
it('private method has visibility_modifier = private', () => {
|
|
const visMod = mockNode('visibility_modifier', 'private');
|
|
const nameNode = mockNode('name', 'validate', visMod);
|
|
expect(isNodeExported(nameNode, 'validate', 'php')).toBe(false);
|
|
});
|
|
});
|
|
|
|
// Swift
|
|
describe('swift', () => {
|
|
it('public function is exported', () => {
|
|
const visMod = mockNode('modifiers', 'public');
|
|
const nameNode = mockNode('identifier', 'getCount', visMod);
|
|
expect(isNodeExported(nameNode, 'getCount', 'swift')).toBe(true);
|
|
});
|
|
|
|
it('open function is exported', () => {
|
|
const visMod = mockNode('modifiers', 'open');
|
|
const nameNode = mockNode('identifier', 'doStuff', visMod);
|
|
expect(isNodeExported(nameNode, 'doStuff', 'swift')).toBe(true);
|
|
});
|
|
|
|
it('non-public (internal) function is exported (Swift default is module-scoped)', () => {
|
|
const fnDecl = mockNode('function_declaration', 'func helper() {}');
|
|
const nameNode = mockNode('identifier', 'helper', fnDecl);
|
|
expect(isNodeExported(nameNode, 'helper', 'swift')).toBe(true);
|
|
});
|
|
});
|
|
|
|
// C/C++
|
|
describe('c/cpp', () => {
|
|
it('C functions without static are exported (external linkage)', () => {
|
|
const nameNode = mockNode('identifier', 'add');
|
|
const fnDef = mockNode('function_definition', 'int add(int a, int b) {}', undefined, [
|
|
nameNode,
|
|
]);
|
|
expect(isNodeExported(nameNode, 'add', 'c')).toBe(true);
|
|
});
|
|
|
|
it('C++ functions without static are exported', () => {
|
|
const nameNode = mockNode('identifier', 'helperFunction');
|
|
const fnDef = mockNode('function_definition', 'void helperFunction() {}', undefined, [
|
|
nameNode,
|
|
]);
|
|
expect(isNodeExported(nameNode, 'helperFunction', 'cpp')).toBe(true);
|
|
});
|
|
|
|
it('static C function is not exported', () => {
|
|
const nameNode = mockNode('identifier', 'internalHelper');
|
|
const staticSpec = mockNode('storage_class_specifier', 'static');
|
|
const fnDef = mockNode(
|
|
'function_definition',
|
|
'static void internalHelper() {}',
|
|
undefined,
|
|
[staticSpec, nameNode],
|
|
);
|
|
expect(isNodeExported(nameNode, 'internalHelper', 'c')).toBe(false);
|
|
});
|
|
});
|
|
|
|
// C#
|
|
describe('csharp', () => {
|
|
it('public modifier means exported', () => {
|
|
const modifier = mockNode('modifier', 'public');
|
|
const nameNode = mockNode('identifier', 'Add');
|
|
// modifier is a sibling of nameNode inside method_declaration
|
|
const methodDecl = mockNode('method_declaration', 'public int Add() {}', undefined, [
|
|
modifier,
|
|
nameNode,
|
|
]);
|
|
expect(isNodeExported(nameNode, 'Add', 'csharp')).toBe(true);
|
|
});
|
|
|
|
it('no public modifier means not exported', () => {
|
|
const nameNode = mockNode('identifier', 'Helper');
|
|
const classDecl = mockNode('class_declaration', 'class Helper {}', undefined, [nameNode]);
|
|
expect(isNodeExported(nameNode, 'Helper', 'csharp')).toBe(false);
|
|
});
|
|
});
|
|
|
|
// Java
|
|
describe('java', () => {
|
|
it('public method is exported', () => {
|
|
const modifiers = mockNode('modifiers', 'public');
|
|
const nameNode = mockNode('identifier', 'getUser');
|
|
const methodDecl = mockNode('method_declaration', 'public User getUser() {}', undefined, [
|
|
modifiers,
|
|
nameNode,
|
|
]);
|
|
expect(isNodeExported(nameNode, 'getUser', 'java')).toBe(true);
|
|
});
|
|
|
|
it('public class method via text check is exported', () => {
|
|
const nameNode = mockNode('identifier', 'doGet');
|
|
const methodDecl = mockNode('method_declaration', 'public void doGet() {}', undefined, [
|
|
nameNode,
|
|
]);
|
|
// text starts with 'public' so it should be detected
|
|
expect(isNodeExported(nameNode, 'doGet', 'java')).toBe(true);
|
|
});
|
|
|
|
it('private method is not exported', () => {
|
|
const modifiers = mockNode('modifiers', 'private');
|
|
const nameNode = mockNode('identifier', 'helper');
|
|
const methodDecl = mockNode('method_declaration', 'private void helper() {}', undefined, [
|
|
modifiers,
|
|
nameNode,
|
|
]);
|
|
expect(isNodeExported(nameNode, 'helper', 'java')).toBe(false);
|
|
});
|
|
|
|
it('package-private (no modifier) is not exported', () => {
|
|
const nameNode = mockNode('identifier', 'internal');
|
|
const methodDecl = mockNode('method_declaration', 'void internal() {}', undefined, [
|
|
nameNode,
|
|
]);
|
|
expect(isNodeExported(nameNode, 'internal', 'java')).toBe(false);
|
|
});
|
|
});
|
|
|
|
// Kotlin
|
|
describe('kotlin', () => {
|
|
it('function without visibility modifier is public by default', () => {
|
|
const nameNode = mockNode('identifier', 'greet');
|
|
const fnDecl = mockNode('function_declaration', 'fun greet() {}', undefined, [nameNode]);
|
|
expect(isNodeExported(nameNode, 'greet', 'kotlin')).toBe(true);
|
|
});
|
|
|
|
it('public function is exported', () => {
|
|
const visMod = mockNode('visibility_modifier', 'public');
|
|
const modifiers = mockNode('modifiers', 'public', undefined, [visMod]);
|
|
const nameNode = mockNode('identifier', 'greet');
|
|
const fnDecl = mockNode('function_declaration', 'public fun greet() {}', undefined, [
|
|
modifiers,
|
|
nameNode,
|
|
]);
|
|
expect(isNodeExported(nameNode, 'greet', 'kotlin')).toBe(true);
|
|
});
|
|
|
|
it('private function is not exported', () => {
|
|
const visMod = mockNode('visibility_modifier', 'private');
|
|
const modifiers = mockNode('modifiers', 'private', undefined, [visMod]);
|
|
const nameNode = mockNode('identifier', 'secret');
|
|
const fnDecl = mockNode('function_declaration', 'private fun secret() {}', undefined, [
|
|
modifiers,
|
|
nameNode,
|
|
]);
|
|
expect(isNodeExported(nameNode, 'secret', 'kotlin')).toBe(false);
|
|
});
|
|
|
|
it('internal function is not exported', () => {
|
|
const visMod = mockNode('visibility_modifier', 'internal');
|
|
const modifiers = mockNode('modifiers', 'internal', undefined, [visMod]);
|
|
const nameNode = mockNode('identifier', 'moduleOnly');
|
|
const fnDecl = mockNode('function_declaration', 'internal fun moduleOnly() {}', undefined, [
|
|
modifiers,
|
|
nameNode,
|
|
]);
|
|
expect(isNodeExported(nameNode, 'moduleOnly', 'kotlin')).toBe(false);
|
|
});
|
|
});
|
|
|
|
// C# additional cases
|
|
describe('csharp additional', () => {
|
|
it('internal modifier is not exported', () => {
|
|
const modifier = mockNode('modifier', 'internal');
|
|
const nameNode = mockNode('identifier', 'InternalService');
|
|
const classDecl = mockNode(
|
|
'class_declaration',
|
|
'internal class InternalService {}',
|
|
undefined,
|
|
[modifier, nameNode],
|
|
);
|
|
expect(isNodeExported(nameNode, 'InternalService', 'csharp')).toBe(false);
|
|
});
|
|
|
|
it('private modifier is not exported', () => {
|
|
const modifier = mockNode('modifier', 'private');
|
|
const nameNode = mockNode('identifier', 'helper');
|
|
const methodDecl = mockNode('method_declaration', 'private void helper() {}', undefined, [
|
|
modifier,
|
|
nameNode,
|
|
]);
|
|
expect(isNodeExported(nameNode, 'helper', 'csharp')).toBe(false);
|
|
});
|
|
|
|
it('struct with public modifier is exported', () => {
|
|
const modifier = mockNode('modifier', 'public');
|
|
const nameNode = mockNode('identifier', 'Point');
|
|
const structDecl = mockNode('struct_declaration', 'public struct Point {}', undefined, [
|
|
modifier,
|
|
nameNode,
|
|
]);
|
|
expect(isNodeExported(nameNode, 'Point', 'csharp')).toBe(true);
|
|
});
|
|
|
|
it('enum with public modifier is exported', () => {
|
|
const modifier = mockNode('modifier', 'public');
|
|
const nameNode = mockNode('identifier', 'Status');
|
|
const enumDecl = mockNode('enum_declaration', 'public enum Status {}', undefined, [
|
|
modifier,
|
|
nameNode,
|
|
]);
|
|
expect(isNodeExported(nameNode, 'Status', 'csharp')).toBe(true);
|
|
});
|
|
|
|
it('record with public modifier is exported', () => {
|
|
const modifier = mockNode('modifier', 'public');
|
|
const nameNode = mockNode('identifier', 'UserDto');
|
|
const recordDecl = mockNode('record_declaration', 'public record UserDto {}', undefined, [
|
|
modifier,
|
|
nameNode,
|
|
]);
|
|
expect(isNodeExported(nameNode, 'UserDto', 'csharp')).toBe(true);
|
|
});
|
|
|
|
it('interface with public modifier is exported', () => {
|
|
const modifier = mockNode('modifier', 'public');
|
|
const nameNode = mockNode('identifier', 'IService');
|
|
const ifaceDecl = mockNode(
|
|
'interface_declaration',
|
|
'public interface IService {}',
|
|
undefined,
|
|
[modifier, nameNode],
|
|
);
|
|
expect(isNodeExported(nameNode, 'IService', 'csharp')).toBe(true);
|
|
});
|
|
});
|
|
|
|
// Rust additional cases
|
|
describe('rust additional', () => {
|
|
it('pub(crate) is exported', () => {
|
|
const visMod = mockNode('visibility_modifier', 'pub(crate)');
|
|
const nameNode = mockNode('identifier', 'internal_fn');
|
|
const fnDecl = mockNode('function_item', 'pub(crate) fn internal_fn() {}', undefined, [
|
|
visMod,
|
|
nameNode,
|
|
]);
|
|
expect(isNodeExported(nameNode, 'internal_fn', 'rust')).toBe(true);
|
|
});
|
|
|
|
it('pub struct is exported', () => {
|
|
const visMod = mockNode('visibility_modifier', 'pub');
|
|
const nameNode = mockNode('type_identifier', 'Config');
|
|
const structDecl = mockNode('struct_item', 'pub struct Config {}', undefined, [
|
|
visMod,
|
|
nameNode,
|
|
]);
|
|
expect(isNodeExported(nameNode, 'Config', 'rust')).toBe(true);
|
|
});
|
|
|
|
it('private struct is not exported', () => {
|
|
const nameNode = mockNode('type_identifier', 'Inner');
|
|
const structDecl = mockNode('struct_item', 'struct Inner {}', undefined, [nameNode]);
|
|
expect(isNodeExported(nameNode, 'Inner', 'rust')).toBe(false);
|
|
});
|
|
|
|
it('pub enum is exported', () => {
|
|
const visMod = mockNode('visibility_modifier', 'pub');
|
|
const nameNode = mockNode('type_identifier', 'ErrorKind');
|
|
const enumDecl = mockNode('enum_item', 'pub enum ErrorKind {}', undefined, [
|
|
visMod,
|
|
nameNode,
|
|
]);
|
|
expect(isNodeExported(nameNode, 'ErrorKind', 'rust')).toBe(true);
|
|
});
|
|
|
|
it('pub trait is exported', () => {
|
|
const visMod = mockNode('visibility_modifier', 'pub');
|
|
const nameNode = mockNode('type_identifier', 'Handler');
|
|
const traitDecl = mockNode('trait_item', 'pub trait Handler {}', undefined, [
|
|
visMod,
|
|
nameNode,
|
|
]);
|
|
expect(isNodeExported(nameNode, 'Handler', 'rust')).toBe(true);
|
|
});
|
|
});
|
|
|
|
// C/C++ additional cases
|
|
describe('c/cpp additional', () => {
|
|
it('static C++ function is not exported', () => {
|
|
const nameNode = mockNode('identifier', 'localHelper');
|
|
const staticSpec = mockNode('storage_class_specifier', 'static');
|
|
const fnDef = mockNode('function_definition', 'static int localHelper() {}', undefined, [
|
|
staticSpec,
|
|
nameNode,
|
|
]);
|
|
expect(isNodeExported(nameNode, 'localHelper', 'cpp')).toBe(false);
|
|
});
|
|
|
|
it('declaration (not definition) without static is exported', () => {
|
|
const nameNode = mockNode('identifier', 'compute');
|
|
const decl = mockNode('declaration', 'int compute(int x);', undefined, [nameNode]);
|
|
expect(isNodeExported(nameNode, 'compute', 'c')).toBe(true);
|
|
});
|
|
|
|
it('static declaration is not exported', () => {
|
|
const nameNode = mockNode('identifier', 'internalFn');
|
|
const staticSpec = mockNode('storage_class_specifier', 'static');
|
|
const decl = mockNode('declaration', 'static int internalFn(void);', undefined, [
|
|
staticSpec,
|
|
nameNode,
|
|
]);
|
|
expect(isNodeExported(nameNode, 'internalFn', 'c')).toBe(false);
|
|
});
|
|
|
|
it('detached node defaults to exported (external linkage)', () => {
|
|
const nameNode = mockNode('identifier', 'orphan');
|
|
expect(isNodeExported(nameNode, 'orphan', 'c')).toBe(true);
|
|
});
|
|
|
|
it('C++ anonymous namespace function is not exported (internal linkage)', () => {
|
|
const nameNode = mockNode('identifier', 'anonHelper');
|
|
const fnDef = mockNode('function_definition', 'void anonHelper() {}', undefined, [
|
|
nameNode,
|
|
]);
|
|
// Anonymous namespace: namespace_definition with no name field
|
|
const anonNs = mockNode(
|
|
'namespace_definition',
|
|
'namespace { void anonHelper() {} }',
|
|
undefined,
|
|
[fnDef],
|
|
);
|
|
expect(isNodeExported(nameNode, 'anonHelper', 'cpp')).toBe(false);
|
|
});
|
|
|
|
it('C++ named namespace function is still exported', () => {
|
|
const nameNode = mockNode('identifier', 'namedHelper');
|
|
const fnDef = mockNode('function_definition', 'void namedHelper() {}', undefined, [
|
|
nameNode,
|
|
]);
|
|
const nsName = mockNode('namespace_identifier', 'utils');
|
|
const namedNs = mockNode(
|
|
'namespace_definition',
|
|
'namespace utils { void namedHelper() {} }',
|
|
undefined,
|
|
[fnDef],
|
|
{ name: nsName },
|
|
);
|
|
expect(isNodeExported(nameNode, 'namedHelper', 'cpp')).toBe(true);
|
|
});
|
|
});
|
|
|
|
// C/C++ with real tree-sitter (validates structural storage_class_specifier detection)
|
|
describe('c/cpp real tree-sitter', () => {
|
|
it('non-static function is exported using real AST', async () => {
|
|
const parser = await loadParser();
|
|
await loadLanguage(SupportedLanguages.C);
|
|
const tree = parser.parse('int add(int a, int b) { return a + b; }');
|
|
const funcDef = tree.rootNode.child(0)!;
|
|
// Find the identifier name node inside the function_definition
|
|
const declNode = funcDef.childForFieldName('declarator');
|
|
const nameNode = declNode?.childForFieldName?.('declarator') || declNode;
|
|
expect(isNodeExported(nameNode, 'add', 'c')).toBe(true);
|
|
});
|
|
|
|
it('static function is not exported using real AST', async () => {
|
|
const parser = await loadParser();
|
|
await loadLanguage(SupportedLanguages.C);
|
|
const tree = parser.parse('static int internal_helper(void) { return 0; }');
|
|
const funcDef = tree.rootNode.child(0)!;
|
|
const declNode = funcDef.childForFieldName('declarator');
|
|
const nameNode = declNode?.childForFieldName?.('declarator') || declNode;
|
|
expect(isNodeExported(nameNode, 'internal_helper', 'c')).toBe(false);
|
|
});
|
|
|
|
it('extern function is exported using real AST', async () => {
|
|
const parser = await loadParser();
|
|
await loadLanguage(SupportedLanguages.C);
|
|
const tree = parser.parse('extern int shared_func(void);');
|
|
const decl = tree.rootNode.child(0)!;
|
|
// Declaration nodes should not have storage_class_specifier 'static'
|
|
const nameNode = decl.descendantsOfType?.('identifier')?.[0] || decl;
|
|
expect(isNodeExported(nameNode, 'shared_func', 'c')).toBe(true);
|
|
});
|
|
|
|
it('C++ anonymous namespace detected via real AST', async () => {
|
|
const parser = await loadParser();
|
|
await loadLanguage(SupportedLanguages.CPlusPlus);
|
|
const code = 'namespace { void hidden() {} }';
|
|
const tree = parser.parse(code);
|
|
const nsDef = tree.rootNode.child(0)!;
|
|
// Find function_definition inside the namespace body
|
|
const body = nsDef.childForFieldName('body');
|
|
const funcDef = body?.namedChild(0);
|
|
const declNode = funcDef?.childForFieldName?.('declarator');
|
|
const nameNode = declNode?.childForFieldName?.('declarator') || declNode;
|
|
expect(isNodeExported(nameNode, 'hidden', 'cpp')).toBe(false);
|
|
});
|
|
|
|
it('C++ named namespace is still exported via real AST', async () => {
|
|
const parser = await loadParser();
|
|
await loadLanguage(SupportedLanguages.CPlusPlus);
|
|
const code = 'namespace utils { void helper() {} }';
|
|
const tree = parser.parse(code);
|
|
const nsDef = tree.rootNode.child(0)!;
|
|
const body = nsDef.childForFieldName('body');
|
|
const funcDef = body?.namedChild(0);
|
|
const declNode = funcDef?.childForFieldName?.('declarator');
|
|
const nameNode = declNode?.childForFieldName?.('declarator') || declNode;
|
|
expect(isNodeExported(nameNode, 'helper', 'cpp')).toBe(true);
|
|
});
|
|
});
|
|
|
|
// C/C++ edge cases with mocks
|
|
describe('c/cpp edge cases', () => {
|
|
it('nested anonymous namespace (double nesting) is not exported', () => {
|
|
const nameNode = mockNode('identifier', 'deepHidden');
|
|
const fnDef = mockNode('function_definition', 'void deepHidden() {}', undefined, [
|
|
nameNode,
|
|
]);
|
|
const innerNs = mockNode('namespace_definition', 'namespace { }', undefined, [fnDef]);
|
|
const outerNs = mockNode(
|
|
'namespace_definition',
|
|
'namespace outer { }',
|
|
undefined,
|
|
[innerNs],
|
|
{ name: mockNode('namespace_identifier', 'outer') },
|
|
);
|
|
expect(isNodeExported(nameNode, 'deepHidden', 'cpp')).toBe(false);
|
|
});
|
|
|
|
it('static function inside named namespace is not exported', () => {
|
|
const nameNode = mockNode('identifier', 'staticInNs');
|
|
const staticSpec = mockNode('storage_class_specifier', 'static');
|
|
const fnDef = mockNode('function_definition', 'static void staticInNs() {}', undefined, [
|
|
staticSpec,
|
|
nameNode,
|
|
]);
|
|
const ns = mockNode('namespace_definition', 'namespace foo { }', undefined, [fnDef], {
|
|
name: mockNode('namespace_identifier', 'foo'),
|
|
});
|
|
expect(isNodeExported(nameNode, 'staticInNs', 'cpp')).toBe(false);
|
|
});
|
|
|
|
it('extern storage class is not confused with static', () => {
|
|
const nameNode = mockNode('identifier', 'externFn');
|
|
const externSpec = mockNode('storage_class_specifier', 'extern');
|
|
const fnDef = mockNode('function_definition', 'extern void externFn() {}', undefined, [
|
|
externSpec,
|
|
nameNode,
|
|
]);
|
|
expect(isNodeExported(nameNode, 'externFn', 'c')).toBe(true);
|
|
});
|
|
});
|
|
|
|
// Rust additional edge cases
|
|
describe('rust edge cases', () => {
|
|
it('pub(super) is treated as exported', () => {
|
|
const visMod = mockNode('visibility_modifier', 'pub(super)');
|
|
const nameNode = mockNode('identifier', 'parent_fn');
|
|
const fnDecl = mockNode('function_item', 'pub(super) fn parent_fn() {}', undefined, [
|
|
visMod,
|
|
nameNode,
|
|
]);
|
|
expect(isNodeExported(nameNode, 'parent_fn', 'rust')).toBe(true);
|
|
});
|
|
|
|
it('pub union is exported', () => {
|
|
const visMod = mockNode('visibility_modifier', 'pub');
|
|
const nameNode = mockNode('type_identifier', 'MyUnion');
|
|
const unionDecl = mockNode('union_item', 'pub union MyUnion {}', undefined, [
|
|
visMod,
|
|
nameNode,
|
|
]);
|
|
expect(isNodeExported(nameNode, 'MyUnion', 'rust')).toBe(true);
|
|
});
|
|
|
|
it('private union is not exported', () => {
|
|
const nameNode = mockNode('type_identifier', 'InternalUnion');
|
|
const unionDecl = mockNode('union_item', 'union InternalUnion {}', undefined, [nameNode]);
|
|
expect(isNodeExported(nameNode, 'InternalUnion', 'rust')).toBe(false);
|
|
});
|
|
|
|
it('pub type alias is exported', () => {
|
|
const visMod = mockNode('visibility_modifier', 'pub');
|
|
const nameNode = mockNode('type_identifier', 'Result');
|
|
const typeDecl = mockNode('type_item', 'pub type Result = ...', undefined, [
|
|
visMod,
|
|
nameNode,
|
|
]);
|
|
expect(isNodeExported(nameNode, 'Result', 'rust')).toBe(true);
|
|
});
|
|
|
|
it('pub const is exported', () => {
|
|
const visMod = mockNode('visibility_modifier', 'pub');
|
|
const nameNode = mockNode('identifier', 'MAX_SIZE');
|
|
const constDecl = mockNode('const_item', 'pub const MAX_SIZE: usize = 100;', undefined, [
|
|
visMod,
|
|
nameNode,
|
|
]);
|
|
expect(isNodeExported(nameNode, 'MAX_SIZE', 'rust')).toBe(true);
|
|
});
|
|
|
|
it('private const is not exported', () => {
|
|
const nameNode = mockNode('identifier', 'INTERNAL_LIMIT');
|
|
const constDecl = mockNode('const_item', 'const INTERNAL_LIMIT: usize = 50;', undefined, [
|
|
nameNode,
|
|
]);
|
|
expect(isNodeExported(nameNode, 'INTERNAL_LIMIT', 'rust')).toBe(false);
|
|
});
|
|
|
|
it('pub static is exported', () => {
|
|
const visMod = mockNode('visibility_modifier', 'pub');
|
|
const nameNode = mockNode('identifier', 'INSTANCE');
|
|
const staticDecl = mockNode('static_item', 'pub static INSTANCE: ...', undefined, [
|
|
visMod,
|
|
nameNode,
|
|
]);
|
|
expect(isNodeExported(nameNode, 'INSTANCE', 'rust')).toBe(true);
|
|
});
|
|
|
|
it('associated_type without pub is not exported', () => {
|
|
const nameNode = mockNode('type_identifier', 'Item');
|
|
const assocType = mockNode('associated_type', 'type Item;', undefined, [nameNode]);
|
|
expect(isNodeExported(nameNode, 'Item', 'rust')).toBe(false);
|
|
});
|
|
});
|
|
|
|
// C# edge cases
|
|
describe('csharp edge cases', () => {
|
|
it('protected modifier is not exported', () => {
|
|
const modifier = mockNode('modifier', 'protected');
|
|
const nameNode = mockNode('identifier', 'OnInit');
|
|
const methodDecl = mockNode('method_declaration', 'protected void OnInit() {}', undefined, [
|
|
modifier,
|
|
nameNode,
|
|
]);
|
|
expect(isNodeExported(nameNode, 'OnInit', 'csharp')).toBe(false);
|
|
});
|
|
|
|
it('protected internal is not exported (first modifier wins)', () => {
|
|
const mod1 = mockNode('modifier', 'protected');
|
|
const mod2 = mockNode('modifier', 'internal');
|
|
const nameNode = mockNode('identifier', 'Setup');
|
|
const methodDecl = mockNode(
|
|
'method_declaration',
|
|
'protected internal void Setup() {}',
|
|
undefined,
|
|
[mod1, mod2, nameNode],
|
|
);
|
|
// Neither modifier is 'public', so not exported
|
|
expect(isNodeExported(nameNode, 'Setup', 'csharp')).toBe(false);
|
|
});
|
|
|
|
it('record_struct with public modifier is exported', () => {
|
|
const modifier = mockNode('modifier', 'public');
|
|
const nameNode = mockNode('identifier', 'Coord');
|
|
// tree-sitter-c-sharp emits `record_declaration` for `record`, `record
|
|
// struct`, and `record class` alike (verified via real parse, #1920) —
|
|
// there is no separate `record_struct_declaration` node type, so the
|
|
// export check sees a `record_declaration`.
|
|
const recStruct = mockNode(
|
|
'record_declaration',
|
|
'public record struct Coord {}',
|
|
undefined,
|
|
[modifier, nameNode],
|
|
);
|
|
expect(isNodeExported(nameNode, 'Coord', 'csharp')).toBe(true);
|
|
});
|
|
|
|
it('record_class with public modifier is exported', () => {
|
|
const modifier = mockNode('modifier', 'public');
|
|
const nameNode = mockNode('identifier', 'UserRecord');
|
|
// `record class` also parses to `record_declaration` (see note above);
|
|
// there is no `record_class_declaration` node in tree-sitter-c-sharp.
|
|
const recClass = mockNode(
|
|
'record_declaration',
|
|
'public record class UserRecord {}',
|
|
undefined,
|
|
[modifier, nameNode],
|
|
);
|
|
expect(isNodeExported(nameNode, 'UserRecord', 'csharp')).toBe(true);
|
|
});
|
|
|
|
it('file_scoped_namespace_declaration is a valid context', () => {
|
|
const modifier = mockNode('modifier', 'public');
|
|
const nameNode = mockNode('identifier', 'MyClass');
|
|
const classDecl = mockNode('class_declaration', 'public class MyClass {}', undefined, [
|
|
modifier,
|
|
nameNode,
|
|
]);
|
|
// class_declaration is found before namespace, so public is detected
|
|
expect(isNodeExported(nameNode, 'MyClass', 'csharp')).toBe(true);
|
|
});
|
|
|
|
it('delegate with public modifier is exported', () => {
|
|
const modifier = mockNode('modifier', 'public');
|
|
const nameNode = mockNode('identifier', 'OnChange');
|
|
const delegateDecl = mockNode(
|
|
'delegate_declaration',
|
|
'public delegate void OnChange();',
|
|
undefined,
|
|
[modifier, nameNode],
|
|
);
|
|
expect(isNodeExported(nameNode, 'OnChange', 'csharp')).toBe(true);
|
|
});
|
|
|
|
it('event with public modifier is exported', () => {
|
|
const modifier = mockNode('modifier', 'public');
|
|
const nameNode = mockNode('identifier', 'Changed');
|
|
const eventDecl = mockNode(
|
|
'event_declaration',
|
|
'public event EventHandler Changed;',
|
|
undefined,
|
|
[modifier, nameNode],
|
|
);
|
|
expect(isNodeExported(nameNode, 'Changed', 'csharp')).toBe(true);
|
|
});
|
|
|
|
it('property with public modifier is exported', () => {
|
|
const modifier = mockNode('modifier', 'public');
|
|
const nameNode = mockNode('identifier', 'Name');
|
|
const propDecl = mockNode(
|
|
'property_declaration',
|
|
'public string Name { get; set; }',
|
|
undefined,
|
|
[modifier, nameNode],
|
|
);
|
|
expect(isNodeExported(nameNode, 'Name', 'csharp')).toBe(true);
|
|
});
|
|
});
|
|
|
|
// Kotlin edge cases
|
|
describe('kotlin edge cases', () => {
|
|
it('protected function is not exported', () => {
|
|
const visMod = mockNode('visibility_modifier', 'protected');
|
|
const modifiers = mockNode('modifiers', 'protected', undefined, [visMod]);
|
|
const nameNode = mockNode('identifier', 'onInit');
|
|
const fnDecl = mockNode('function_declaration', 'protected fun onInit() {}', undefined, [
|
|
modifiers,
|
|
nameNode,
|
|
]);
|
|
expect(isNodeExported(nameNode, 'onInit', 'kotlin')).toBe(false);
|
|
});
|
|
});
|
|
|
|
// Java edge cases
|
|
describe('java edge cases', () => {
|
|
it('protected method is not exported', () => {
|
|
const modifiers = mockNode('modifiers', 'protected');
|
|
const nameNode = mockNode('identifier', 'onInit');
|
|
const methodDecl = mockNode('method_declaration', 'protected void onInit() {}', undefined, [
|
|
modifiers,
|
|
nameNode,
|
|
]);
|
|
expect(isNodeExported(nameNode, 'onInit', 'java')).toBe(false);
|
|
});
|
|
|
|
it('static public method is exported', () => {
|
|
const modifiers = mockNode('modifiers', 'public static');
|
|
const nameNode = mockNode('identifier', 'main');
|
|
const methodDecl = mockNode(
|
|
'method_declaration',
|
|
'public static void main(String[] args) {}',
|
|
undefined,
|
|
[modifiers, nameNode],
|
|
);
|
|
expect(isNodeExported(nameNode, 'main', 'java')).toBe(true);
|
|
});
|
|
});
|
|
|
|
// PHP edge cases
|
|
describe('php edge cases', () => {
|
|
it('protected method is not exported', () => {
|
|
const visMod = mockNode('visibility_modifier', 'protected');
|
|
const nameNode = mockNode('name', 'init', visMod);
|
|
expect(isNodeExported(nameNode, 'init', 'php')).toBe(false);
|
|
});
|
|
|
|
it('interface declaration is exported', () => {
|
|
const ifaceDecl = mockNode('interface_declaration', 'interface Loggable {}');
|
|
const nameNode = mockNode('name', 'Loggable', ifaceDecl);
|
|
expect(isNodeExported(nameNode, 'Loggable', 'php')).toBe(true);
|
|
});
|
|
|
|
it('trait declaration is exported', () => {
|
|
const traitDecl = mockNode('trait_declaration', 'trait Cacheable {}');
|
|
const nameNode = mockNode('name', 'Cacheable', traitDecl);
|
|
expect(isNodeExported(nameNode, 'Cacheable', 'php')).toBe(true);
|
|
});
|
|
|
|
it('enum declaration is exported', () => {
|
|
const enumDecl = mockNode('enum_declaration', 'enum Status {}');
|
|
const nameNode = mockNode('name', 'Status', enumDecl);
|
|
expect(isNodeExported(nameNode, 'Status', 'php')).toBe(true);
|
|
});
|
|
});
|
|
|
|
// Swift edge cases
|
|
describe('swift edge cases', () => {
|
|
it('internal function is exported (Swift internal = module-scoped visibility)', () => {
|
|
const visMod = mockNode('visibility_modifier', 'internal');
|
|
const nameNode = mockNode('identifier', 'setup', visMod);
|
|
expect(isNodeExported(nameNode, 'setup', 'swift')).toBe(true);
|
|
});
|
|
|
|
it('private function is not exported', () => {
|
|
const visMod = mockNode('visibility_modifier', 'private');
|
|
const nameNode = mockNode('identifier', 'helper', visMod);
|
|
expect(isNodeExported(nameNode, 'helper', 'swift')).toBe(false);
|
|
});
|
|
|
|
it('fileprivate function is not exported', () => {
|
|
const visMod = mockNode('visibility_modifier', 'fileprivate');
|
|
const nameNode = mockNode('identifier', 'localHelper', visMod);
|
|
expect(isNodeExported(nameNode, 'localHelper', 'swift')).toBe(false);
|
|
});
|
|
});
|
|
|
|
// Unknown language
|
|
describe('unknown language', () => {
|
|
it('returns false for unknown language', () => {
|
|
const node = mockNode('identifier', 'foo');
|
|
expect(isNodeExported(node, 'foo', 'unknown')).toBe(false);
|
|
});
|
|
});
|
|
});
|
|
|
|
// ─── Fixture files exist ─────────────────────────────────────────────
|
|
|
|
describe('fixture files', () => {
|
|
const fixtures = [
|
|
'simple.ts',
|
|
'simple.py',
|
|
'simple.go',
|
|
'simple.swift',
|
|
'simple.php',
|
|
'simple.rs',
|
|
'simple.java',
|
|
'simple.c',
|
|
'simple.cpp',
|
|
'simple.cs',
|
|
];
|
|
|
|
for (const fixture of fixtures) {
|
|
it(`${fixture} exists and is non-empty`, async () => {
|
|
const content = await fs.readFile(path.join(FIXTURES_DIR, fixture), 'utf-8');
|
|
expect(content.length).toBeGreaterThan(0);
|
|
});
|
|
}
|
|
});
|
|
|
|
// ─── Unhappy path ─────────────────────────────────────────────────────
|
|
|
|
describe('unhappy path', () => {
|
|
it('returns empty AST or handles empty file content', async () => {
|
|
const parser = await loadParser();
|
|
await loadLanguage(SupportedLanguages.TypeScript, 'empty.ts');
|
|
|
|
// Parsing a zero-length string must not throw and must return a valid tree.
|
|
const tree = parser.parse('');
|
|
expect(tree).toBeDefined();
|
|
expect(tree.rootNode).toBeDefined();
|
|
|
|
// An empty file produces a root node with no named children — no symbols.
|
|
// isNodeExported on a bare node with no ancestors returns false regardless of language.
|
|
const detachedNode = mockNode('identifier', 'foo');
|
|
expect(isNodeExported(detachedNode, 'foo', 'typescript')).toBe(false);
|
|
});
|
|
|
|
it('handles binary/non-UTF8 content gracefully', async () => {
|
|
const parser = await loadParser();
|
|
await loadLanguage(SupportedLanguages.TypeScript, 'binary.ts');
|
|
|
|
// Construct a string that contains the Unicode replacement character (U+FFFD)
|
|
// and a mix of high-byte sequences that are not valid UTF-8 when treated as Latin-1.
|
|
// JavaScript strings are UTF-16 internally, so this is always a valid string —
|
|
// but it exercises tree-sitter's ability to handle unusual byte patterns.
|
|
const binaryLikeContent = '\uFFFD\u0000\u0001\u001F' + '\xFF\xFE'.repeat(10) + '\uFFFD';
|
|
|
|
// Must not throw — tree-sitter should return an error-recovery tree.
|
|
let tree: any;
|
|
expect(() => {
|
|
tree = parser.parse(binaryLikeContent);
|
|
}).not.toThrow();
|
|
|
|
expect(tree).toBeDefined();
|
|
expect(tree.rootNode).toBeDefined();
|
|
});
|
|
|
|
it('falls back gracefully for unsupported language', async () => {
|
|
// getLanguageFromFilename returns null for extensions with no grammar mapping.
|
|
const scalaLang = getLanguageFromFilename('Main.scala');
|
|
expect(scalaLang).toBeNull();
|
|
|
|
const luaLang = getLanguageFromFilename('module.lua');
|
|
expect(luaLang).toBeNull();
|
|
|
|
// loadLanguage throws an explicit error for a language not in the grammar map.
|
|
// Cast through unknown to simulate a caller passing an unrecognised language key.
|
|
await expect(loadLanguage('erlang' as unknown as SupportedLanguages)).rejects.toThrow(
|
|
'Unsupported language',
|
|
);
|
|
});
|
|
});
|
|
});
|