From cc832dd5831ed152ef0d948d2444fdf361b176cd Mon Sep 17 00:00:00 2001 From: evolution Date: Thu, 7 May 2026 00:57:19 +0800 Subject: [PATCH] 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. --- .../ingestion/languages/go/range-binding.ts | 4 +- .../ingestion/languages/go/type-binding.ts | 6 +- .../go/go-range-binding-null-guard.test.ts | 104 ++++++++++++++++++ .../go/go-type-binding.test.ts | 52 +++++++++ 4 files changed, 161 insertions(+), 5 deletions(-) create mode 100644 gitnexus/test/unit/scope-resolution/go/go-range-binding-null-guard.test.ts 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); + }); +});