diff --git a/gitnexus/src/core/ingestion/languages/go/range-binding.ts b/gitnexus/src/core/ingestion/languages/go/range-binding.ts index d956764809..14e520c2de 100644 --- a/gitnexus/src/core/ingestion/languages/go/range-binding.ts +++ b/gitnexus/src/core/ingestion/languages/go/range-binding.ts @@ -30,10 +30,10 @@ export function populateGoRangeBindings( for (const rangeNode of tree.rootNode.descendantsOfType('for_statement')) { const rangeClause = rangeNode.namedChildren.find((c) => c.type === 'range_clause'); - if (rangeClause === null) continue; + if (rangeClause === undefined) continue; const left = rangeClause.namedChildren.find((c) => c.type === 'expression_list'); - if (left === null) continue; + if (left === undefined) continue; const rangeExpr = rangeClause.namedChildren.find( (c, idx) => c.type !== 'expression_list' && idx > rangeClause.namedChildren.indexOf(left), diff --git a/gitnexus/src/core/ingestion/languages/go/type-binding.ts b/gitnexus/src/core/ingestion/languages/go/type-binding.ts index c0808a9909..2db7379300 100644 --- a/gitnexus/src/core/ingestion/languages/go/type-binding.ts +++ b/gitnexus/src/core/ingestion/languages/go/type-binding.ts @@ -50,7 +50,7 @@ export function synthesizeGoTypeBindings(rootNode: SyntaxNode): CaptureMatch[] { const typeArg = args.namedChildren.find((c) => ['type_identifier', 'qualified_type'].includes(c.type), ); - if (typeArg !== null) { + if (typeArg !== undefined) { const typeName = extractSimpleTypeNameText(typeArg); const nameNodes = lhs.namedChildren.filter((c) => c.type === 'identifier'); if (nameNodes.length > 0) { @@ -71,13 +71,13 @@ export function synthesizeGoTypeBindings(rootNode: SyntaxNode): CaptureMatch[] { // V1: channel_type not handled — make(chan T) produces no typeBinding. ['slice_type', 'map_type'].includes(c.type), ); - if (sliceOrMap !== null) { + if (sliceOrMap !== undefined) { let typeName = ''; if (sliceOrMap.type === 'slice_type') { const elem = sliceOrMap.namedChildren.find((c) => ['type_identifier', 'qualified_type'].includes(c.type), ); - if (elem !== null) typeName = extractSimpleTypeNameText(elem); + if (elem !== undefined) typeName = extractSimpleTypeNameText(elem); } else if (sliceOrMap.type === 'map_type') { const typeChildren = sliceOrMap.namedChildren.filter((c) => ['type_identifier', 'qualified_type'].includes(c.type), diff --git a/gitnexus/test/unit/scope-resolution/go/go-range-binding-null-guard.test.ts b/gitnexus/test/unit/scope-resolution/go/go-range-binding-null-guard.test.ts new file mode 100644 index 0000000000..edf48b33a8 --- /dev/null +++ b/gitnexus/test/unit/scope-resolution/go/go-range-binding-null-guard.test.ts @@ -0,0 +1,104 @@ +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([['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([['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([['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([['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([['main.go', src]]); + expect(() => + populateGoRangeBindings([parsed], makeEmptyIndexes(), { fileContents }), + ).not.toThrow(); + }); +}); diff --git a/gitnexus/test/unit/scope-resolution/go/go-type-binding.test.ts b/gitnexus/test/unit/scope-resolution/go/go-type-binding.test.ts index 9ea213128f..96da250fbc 100644 --- a/gitnexus/test/unit/scope-resolution/go/go-type-binding.test.ts +++ b/gitnexus/test/unit/scope-resolution/go/go-type-binding.test.ts @@ -112,3 +112,55 @@ describe('Go type binding synthesis — 7 patterns', () => { expect(normalizeGoTypeName('List[User]')).toBe('List'); }); }); + +describe('Go type binding — null guard (#1346, #1366)', () => { + it('does not crash on make(chan T) — channel_type has no matching child', () => { + const src = 'package main\nfunc main() {\n ch := make(chan int)\n}'; + const tree = getGoParser().parse(src); + // Before fix: .find() returned undefined, checked with !== null, crashed + // accessing .type on undefined. + const matches = synthesizeGoTypeBindings(tree.rootNode as any); + // make(chan T) is not handled (V1 limitation) — should produce no crash + // and no make-binding, not crash the pipeline. + const makeMatch = matches.find((m) => m['@type-binding.make']); + expect(makeMatch).toBeUndefined(); + }); + + it('does not crash on new() with no type arguments', () => { + const src = 'package main\nfunc main() {\n x := new(complex128)\n _ = x\n}'; + const tree = getGoParser().parse(src); + // complex128 is a builtin type_identifier — this should work normally, + // but tests the path where .find() must not return undefined unchecked. + const matches = synthesizeGoTypeBindings(tree.rootNode as any); + const newMatch = matches.find((m) => m['@type-binding.new']); + expect(newMatch).toBeDefined(); + expect(newMatch?.['@type-binding.type']?.text).toBe('complex128'); + }); + + it('does not crash on make with only generic type arguments', () => { + const src = 'package main\nfunc main() {\n ch := make(chan *User)\n}'; + const tree = getGoParser().parse(src); + // chan *User: sliceOrMap is undefined (channel_type not in ['slice_type','map_type']) + const matches = synthesizeGoTypeBindings(tree.rootNode as any); + expect(matches.find((m) => m['@type-binding.make'])).toBeUndefined(); + }); + + it('does not crash on composite literal with embedded struct', () => { + const src = `package main +type Base struct{ ID int } +type User struct{ Base } +func main() { u := User{Base: Base{ID: 1}} }`; + const matches = emitGoScopeCaptures(src, 'main.go'); + // Should produce captures without crashing + expect(matches.length).toBeGreaterThan(0); + }); + + it('does not crash on short var decl with function call (no typeBinding)', () => { + const src = 'package main\nfunc main() {\n result := DoSomething()\n}'; + const tree = getGoParser().parse(src); + // RHS is a call_expression with no new/make — no typeBinding expected + const matches = synthesizeGoTypeBindings(tree.rootNode as any); + // Should not crash; may or may not produce bindings depending on the call + expect(Array.isArray(matches)).toBe(true); + }); +});