GitNexus/gitnexus/test/unit/scope-resolution/go/go-range-binding-null-guard.test.ts
evolution 28df98c997
fix(go): use loose equality for Array.find() null checks (#1384)
* fix(go): use loose equality for Array.find() null checks (#1346, #1366)

Array.find() returns undefined (not null) when no match is found, but
the code checked with === null / !== null which fails to intercept it.
This caused "Cannot read properties of undefined (reading 'type')" and
"Cannot read properties of undefined (reading 'namedChildren')" crashes
on Go files containing plain for loops, make(chan T), or other patterns
where the expected tree-sitter node type is absent.

* refactor(go): use strict undefined checks for Array.find() results

Address review feedback: Array.find() returns undefined by spec, so
check with === undefined / !== undefined instead of loose == null.
2026-05-06 17:57:19 +01:00

104 lines
3.2 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { extractParsedFile } from '../../../../src/core/ingestion/scope-extractor-bridge.js';
import { goScopeResolver } from '../../../../src/core/ingestion/languages/go/scope-resolver.js';
import { populateGoRangeBindings } from '../../../../src/core/ingestion/languages/go/range-binding.js';
import type { ParsedFile, ScopeResolutionIndexes } from 'gitnexus-shared';
function parseGo(src: string, path = 'main.go'): ParsedFile {
const p = extractParsedFile(goScopeResolver.languageProvider, src, path);
if (p === undefined) throw new Error(`scope extraction failed for ${path}`);
goScopeResolver.populateOwners(p);
return p;
}
function makeEmptyIndexes(): ScopeResolutionIndexes {
return {
bindings: new Map(),
imports: [],
scopeTree: { roots: [] } as any,
methodDispatch: new Map(),
sccs: [],
} as ScopeResolutionIndexes;
}
describe('Go range binding — null guard (#1346, #1366)', () => {
it('does not crash on plain for loop (no range_clause)', () => {
const src = `package main
func main() {
for i := 0; i < 10; i++ {
_ = i
}
}`;
const parsed = parseGo(src);
const fileContents = new Map<string, string>([['main.go', src]]);
// Before fix: rangeClause was undefined, checked with === null,
// then rangeClause.namedChildren crashed.
expect(() =>
populateGoRangeBindings([parsed], makeEmptyIndexes(), { fileContents }),
).not.toThrow();
});
it('does not crash on for-range without expression_list', () => {
// Single variable range without comma: for v := range ch
const src = `package main
func main() {
ch := make(chan int)
for v := range ch {
_ = v
}
}`;
const parsed = parseGo(src);
const fileContents = new Map<string, string>([['main.go', src]]);
expect(() =>
populateGoRangeBindings([parsed], makeEmptyIndexes(), { fileContents }),
).not.toThrow();
});
it('does not crash on for-range over map with blank identifier', () => {
const src = `package main
func main() {
m := map[string]int{"a": 1}
for _, v := range m {
_ = v
}
}`;
const parsed = parseGo(src);
const fileContents = new Map<string, string>([['main.go', src]]);
expect(() =>
populateGoRangeBindings([parsed], makeEmptyIndexes(), { fileContents }),
).not.toThrow();
});
it('does not crash on for-range with type alias target', () => {
const src = `package main
type Users []string
func main() {
var users Users
for _, u := range users {
_ = u
}
}`;
const parsed = parseGo(src);
const fileContents = new Map<string, string>([['main.go', src]]);
expect(() =>
populateGoRangeBindings([parsed], makeEmptyIndexes(), { fileContents }),
).not.toThrow();
});
it('does not crash on nested for loops mixing plain and range', () => {
const src = `package main
func main() {
items := []string{"a", "b"}
for i := 0; i < len(items); i++ {
for _, v := range items {
_ = v
}
}
}`;
const parsed = parseGo(src);
const fileContents = new Map<string, string>([['main.go', src]]);
expect(() =>
populateGoRangeBindings([parsed], makeEmptyIndexes(), { fileContents }),
).not.toThrow();
});
});