From 87ca65ebe4779519f31becb48ebda0cb10477659 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Thu, 30 Jul 2026 06:31:28 +0800 Subject: [PATCH 01/10] test/goroot: match declaration recovery diagnostics --- test/goroot/runner_test.go | 4 +++ test/goroot/runner_unit_test.go | 45 +++++++++++++++++++++++++++++++++ test/goroot/xfail.yaml | 8 ------ 3 files changed, 49 insertions(+), 8 deletions(-) diff --git a/test/goroot/runner_test.go b/test/goroot/runner_test.go index 9cf94de330..39be925ce4 100644 --- a/test/goroot/runner_test.go +++ b/test/goroot/runner_test.go @@ -1662,6 +1662,10 @@ func parserRecoverySecondaries(primary string) []string { return []string{"expected type, found '}'", "expected type, found ')'", "expected type, found ','"} case "syntax error: else must be followed by if or statement block": return []string{"expected if statement or block, found ';'"} + case "expected 'package', found 'func'": + return []string{"expected ';', found '('"} + case "syntax error: unexpected { after top level declaration": + return []string{"expected ';', found '{'"} case "syntax error: unexpected newline in type declaration", "syntax error: unexpected EOF in type declaration": return []string{"expected type, found newline"} case "syntax error: unexpected newline in composite literal; possibly missing comma or }": diff --git a/test/goroot/runner_unit_test.go b/test/goroot/runner_unit_test.go index 08a2126c04..5f00dc5c04 100644 --- a/test/goroot/runner_unit_test.go +++ b/test/goroot/runner_unit_test.go @@ -726,6 +726,51 @@ func TestCheckExpectedErrorsDiscardsExactParserPair(t *testing.T) { } } +func TestCheckExpectedErrorsDiscardsPairedDeclarationRecoveryDiagnostics(t *testing.T) { + tests := []struct { + name string + source string + line int + primary string + secondary string + }{ + { + name: "missing package clause", + source: "func main() { // ERROR \"package\"\n}\n", + line: 1, + primary: "expected 'package', found 'func'", + secondary: "expected ';', found '('", + }, + { + name: "top-level composite literal", + source: `package p +var x map[string]string{"a":"b"} // ERROR "unexpected { at end of statement|unexpected { after top level declaration|expected ';' or newline after top level declaration" +`, + line: 2, + primary: "syntax error: unexpected { after top level declaration", + secondary: "expected ';', found '{'", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + file := filepath.Join(t.TempDir(), "case.go") + if err := os.WriteFile(file, []byte(tt.source), 0o644); err != nil { + t.Fatal(err) + } + output := fmt.Sprintf("%s:%d: %s\n%s:%d: %s\n", file, tt.line, tt.primary, file, tt.line, tt.secondary) + if err := checkExpectedErrors(output, file, "case.go"); err != nil { + t.Fatal(err) + } + if err := checkExpectedErrors(fmt.Sprintf("%s:%d: %s\n", file, tt.line, tt.secondary), file, "case.go"); err == nil { + t.Fatal("secondary diagnostic passed without its primary") + } + if got := parserRecoverySecondaries(tt.primary + "."); got != nil { + t.Fatalf("near-match primary activated parser recovery: %v", got) + } + }) + } +} + func TestCheckExpectedErrorsScopesImportAlias(t *testing.T) { tests := []struct { name, src string diff --git a/test/goroot/xfail.yaml b/test/goroot/xfail.yaml index 8183574996..b7c99a9d5a 100644 --- a/test/goroot/xfail.yaml +++ b/test/goroot/xfail.yaml @@ -2101,10 +2101,6 @@ xfails: directive: errorcheck case: typeparam/pragma.go reason: gc-specific pragma optimization diagnostics are not implemented by llgo - - version: go1.26 - directive: errorcheck - case: fixedbugs/bug050.go - reason: llgo parser emits an additional malformed-declaration diagnostic - version: go1.26 directive: errorcheck case: devirt.go @@ -2309,10 +2305,6 @@ xfails: directive: errorcheck case: fixedbugs/issue34723.go reason: "not applicable: llgo's BDWGC and tinygogc runtimes do not use Go GC write barriers" - - version: go1.26 - directive: errorcheck - case: syntax/vareq1.go - reason: llgo parser recovery emits an additional diagnostic after the expected syntax error - version: go1.26 directive: errorcheck case: escape_bloop.go From 470eac316cf8d7fc32aee85732779a9d4587c6e0 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Thu, 30 Jul 2026 06:44:58 +0800 Subject: [PATCH 02/10] test/goroot: document declaration recovery mapping --- test/goroot/runner_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/test/goroot/runner_test.go b/test/goroot/runner_test.go index 39be925ce4..035e04dedd 100644 --- a/test/goroot/runner_test.go +++ b/test/goroot/runner_test.go @@ -1662,6 +1662,7 @@ func parserRecoverySecondaries(primary string) []string { return []string{"expected type, found '}'", "expected type, found ')'", "expected type, found ','"} case "syntax error: else must be followed by if or statement block": return []string{"expected if statement or block, found ';'"} + // go/parser omits the "syntax error:" prefix when the package clause is missing. case "expected 'package', found 'func'": return []string{"expected ';', found '('"} case "syntax error: unexpected { after top level declaration": From dbeb8e0e8d7c742d4319c1192509ed7639af50c5 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Thu, 30 Jul 2026 09:29:32 +0800 Subject: [PATCH 03/10] test/goroot: scope declaration recovery by source --- test/goroot/runner_test.go | 50 ++++++++++++++++++++++++++++----- test/goroot/runner_unit_test.go | 18 ++++++++++++ 2 files changed, 61 insertions(+), 7 deletions(-) diff --git a/test/goroot/runner_test.go b/test/goroot/runner_test.go index 035e04dedd..4dc6b3f31a 100644 --- a/test/goroot/runner_test.go +++ b/test/goroot/runner_test.go @@ -1166,7 +1166,11 @@ func checkExpectedErrorsForFiles(output string, sources []diagnosticSource) erro lexicalLocations[diagnostic.locationKey()] = true } if sourceOK { - if secondaries := parserRecoverySecondaries(message); len(secondaries) != 0 { + secondaries := parserRecoverySecondaries(message) + if len(secondaries) == 0 { + secondaries = parserRecoverySourceSecondaries(message, expected.source) + } + if len(secondaries) != 0 { parserRecoveryPairs = append(parserRecoveryPairs, parserRecoveryPair{ file: sourceDiagnostic.file, line: sourceDiagnostic.line, secondaries: secondaries, }) @@ -1649,7 +1653,8 @@ func parseSourceDiagnostic(line string, resolver diagnosticPathResolver) (source } // parserRecoverySecondaries is deliberately limited to exact diagnostic pairs -// emitted by GOROOT cases enabled with this compatibility shim. +// whose secondary does not depend on the source shape. Source-dependent pairs +// belong in parserRecoverySourceSecondaries. func parserRecoverySecondaries(primary string) []string { switch primary { case "syntax error: cannot use a := 10 as value": @@ -1662,11 +1667,6 @@ func parserRecoverySecondaries(primary string) []string { return []string{"expected type, found '}'", "expected type, found ')'", "expected type, found ','"} case "syntax error: else must be followed by if or statement block": return []string{"expected if statement or block, found ';'"} - // go/parser omits the "syntax error:" prefix when the package clause is missing. - case "expected 'package', found 'func'": - return []string{"expected ';', found '('"} - case "syntax error: unexpected { after top level declaration": - return []string{"expected ';', found '{'"} case "syntax error: unexpected newline in type declaration", "syntax error: unexpected EOF in type declaration": return []string{"expected type, found newline"} case "syntax error: unexpected newline in composite literal; possibly missing comma or }": @@ -1677,6 +1677,42 @@ func parserRecoverySecondaries(primary string) []string { return nil } +// parserRecoverySourceSecondaries handles diagnostic spellings shared by +// unrelated malformed programs. Exact source matching keeps those allowances +// scoped to the GOROOT cases that require them. +func parserRecoverySourceSecondaries(primary, source string) []string { + source = parserRecoverySourceCode(source) + switch primary { + // GOROOT/test/fixedbugs/bug050.go. go/parser omits the "syntax error:" + // prefix when the package clause is missing. + case "expected 'package', found 'func'": + if source == "func main() {" { + return []string{"expected ';', found '('"} + } + // GOROOT/test/syntax/vareq1.go + case "syntax error: unexpected { after top level declaration": + if source == `var x map[string]string{"a":"b"}` { + return []string{"expected ';', found '{'"} + } + } + return nil +} + +func parserRecoverySourceCode(source string) string { + // Prefer the last recognized marker so marker-like text in the source + // expression cannot truncate the shape before the actual ERROR comment. + comment := -1 + for _, marker := range []string{"// ERROR", "// GC_ERROR"} { + if index := strings.LastIndex(source, marker); index > comment { + comment = index + } + } + if comment >= 0 { + source = source[:comment] + } + return strings.TrimSpace(source) +} + func discardPairedParserDiagnostics(lines []string, resolver diagnosticPathResolver, pairs []parserRecoveryPair) []string { out := lines[:0] nextLine: diff --git a/test/goroot/runner_unit_test.go b/test/goroot/runner_unit_test.go index 5f00dc5c04..886a159967 100644 --- a/test/goroot/runner_unit_test.go +++ b/test/goroot/runner_unit_test.go @@ -730,6 +730,7 @@ func TestCheckExpectedErrorsDiscardsPairedDeclarationRecoveryDiagnostics(t *test tests := []struct { name string source string + wrong string line int primary string secondary string @@ -737,6 +738,7 @@ func TestCheckExpectedErrorsDiscardsPairedDeclarationRecoveryDiagnostics(t *test { name: "missing package clause", source: "func main() { // ERROR \"package\"\n}\n", + wrong: "func other() { // ERROR \"package\"\n}\n", line: 1, primary: "expected 'package', found 'func'", secondary: "expected ';', found '('", @@ -745,6 +747,9 @@ func TestCheckExpectedErrorsDiscardsPairedDeclarationRecoveryDiagnostics(t *test name: "top-level composite literal", source: `package p var x map[string]string{"a":"b"} // ERROR "unexpected { at end of statement|unexpected { after top level declaration|expected ';' or newline after top level declaration" +`, + wrong: `package p +var y map[string]string{"a":"b"} // ERROR "unexpected { at end of statement|unexpected { after top level declaration|expected ';' or newline after top level declaration" `, line: 2, primary: "syntax error: unexpected { after top level declaration", @@ -764,9 +769,22 @@ var x map[string]string{"a":"b"} // ERROR "unexpected { at end of statement|unex if err := checkExpectedErrors(fmt.Sprintf("%s:%d: %s\n", file, tt.line, tt.secondary), file, "case.go"); err == nil { t.Fatal("secondary diagnostic passed without its primary") } + if got := parserRecoverySecondaries(tt.primary); got != nil { + t.Fatalf("source-dependent primary activated source-independent recovery: %v", got) + } if got := parserRecoverySecondaries(tt.primary + "."); got != nil { t.Fatalf("near-match primary activated parser recovery: %v", got) } + + wrongFile := filepath.Join(t.TempDir(), "case.go") + if err := os.WriteFile(wrongFile, []byte(tt.wrong), 0o644); err != nil { + t.Fatal(err) + } + wrongOutput := fmt.Sprintf("%s:%d: %s\n%s:%d: %s\n", wrongFile, tt.line, tt.primary, wrongFile, tt.line, tt.secondary) + err := checkExpectedErrors(wrongOutput, wrongFile, "case.go") + if err == nil || !strings.Contains(err.Error(), tt.secondary) { + t.Fatalf("wrong source shape discarded secondary: %v", err) + } }) } } From d271fbdc24eed7277e999efd3709b2199cd2eae1 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Thu, 30 Jul 2026 08:19:12 +0800 Subject: [PATCH 04/10] test/goroot: match more parser recovery diagnostics --- test/goroot/runner_test.go | 72 +++++++++- test/goroot/runner_unit_test.go | 233 ++++++++++++++++++++++++++++++++ test/goroot/xfail.yaml | 12 -- 3 files changed, 298 insertions(+), 19 deletions(-) diff --git a/test/goroot/runner_test.go b/test/goroot/runner_test.go index 4dc6b3f31a..ca2acca098 100644 --- a/test/goroot/runner_test.go +++ b/test/goroot/runner_test.go @@ -1121,6 +1121,7 @@ func checkExpectedErrorsForFiles(output string, sources []diagnosticSource) erro lines = preferSpecificDiagnostics(lines) var wanted []wantedError sourceLines := make(map[string][]string, len(sources)) + physicalDiagnosticSources := make(map[string]bool, len(sources)) for _, source := range sources { expected, err := wantedErrors(source.full, source.short) if err != nil { @@ -1132,6 +1133,14 @@ func checkExpectedErrorsForFiles(output string, sources []diagnosticSource) erro return err } sourceLines[normalizeDiagnosticPath(source.short)] = strings.Split(string(data), "\n") + file := canonicalDiagnosticPath(source.full) + // Pairing is keyed by physical source lines. A line directive makes that + // relationship ambiguous, so leave every recovery diagnostic visible. + physical := !hasLineDirective(data) + if previous, ok := physicalDiagnosticSources[file]; ok { + physical = previous && physical + } + physicalDiagnosticSources[file] = physical } pathResolver := newDiagnosticPathResolver(sources) lexicalLocations := make(map[string]bool) @@ -1165,12 +1174,8 @@ func checkExpectedErrorsForFiles(output string, sources []diagnosticSource) erro if ok && isScopedLexicalDiagnostic(message) { lexicalLocations[diagnostic.locationKey()] = true } - if sourceOK { - secondaries := parserRecoverySecondaries(message) - if len(secondaries) == 0 { - secondaries = parserRecoverySourceSecondaries(message, expected.source) - } - if len(secondaries) != 0 { + if sourceOK && physicalDiagnosticSources[sourceDiagnostic.file] { + for _, secondaries := range parserRecoverySecondaryGroups(message, expected.source) { parserRecoveryPairs = append(parserRecoveryPairs, parserRecoveryPair{ file: sourceDiagnostic.file, line: sourceDiagnostic.line, secondaries: secondaries, }) @@ -1634,7 +1639,14 @@ func (resolver diagnosticPathResolver) resolve(file string) (string, bool) { return full, full != "" } if filepath.IsAbs(file) { - return canonicalDiagnosticPath(file), true + full := canonicalDiagnosticPath(file) + // An absolute path is not sufficient by itself: it must still identify + // one of the sources whose ERROR comments are being checked. + for _, source := range resolver { + if source == full { + return full, true + } + } } return "", false } @@ -1698,6 +1710,43 @@ func parserRecoverySourceSecondaries(primary, source string) []string { return nil } +// parserRecoverySecondaryGroups returns independent one-use allowances for an +// exact primary. Source-independent pairs are checked first; all other pairs +// require an exact ERROR source shape. Most recovery spellings are alternatives +// in one group; issue11610 deterministically emits two separate follow-ons, so +// each receives its own group. +func parserRecoverySecondaryGroups(primary, source string) [][]string { + if secondaries := parserRecoverySecondaries(primary); len(secondaries) != 0 { + return [][]string{secondaries} + } + if secondaries := parserRecoverySourceSecondaries(primary, source); len(secondaries) != 0 { + return [][]string{secondaries} + } + source = parserRecoverySourceCode(source) + switch primary { + case "syntax error: ... is missing type": + if source == "func g(x int, y float32) (...)" { + return [][]string{{"expected type, found ')'"}} + } + case "syntax error: cannot use c <- v as value": + if source == "if c <- v {" { + return [][]string{{"expected boolean expression, found simple statement (missing parentheses around composite literal?)"}} + } + case "syntax error: unexpected <- after top level declaration": + if source == "var _ = c <- v" { + return [][]string{{"expected ';', found '<-'"}} + } + case "invalid character U+003F '?'": + if source == "var?" { + return [][]string{ + {"expected 'IDENT', found 'ILLEGAL'"}, + {"illegal character U+003F '?'"}, + } + } + } + return nil +} + func parserRecoverySourceCode(source string) string { // Prefer the last recognized marker so marker-like text in the source // expression cannot truncate the shape before the actual ERROR comment. @@ -1713,6 +1762,15 @@ func parserRecoverySourceCode(source string) string { return strings.TrimSpace(source) } +func hasLineDirective(data []byte) bool { + for _, line := range strings.Split(string(data), "\n") { + if strings.Contains(line, "//line") || strings.Contains(line, "/*line") { + return true + } + } + return false +} + func discardPairedParserDiagnostics(lines []string, resolver diagnosticPathResolver, pairs []parserRecoveryPair) []string { out := lines[:0] nextLine: diff --git a/test/goroot/runner_unit_test.go b/test/goroot/runner_unit_test.go index 886a159967..0978ab9c99 100644 --- a/test/goroot/runner_unit_test.go +++ b/test/goroot/runner_unit_test.go @@ -789,6 +789,208 @@ var y map[string]string{"a":"b"} // ERROR "unexpected { at end of statement|unex } } +func TestCheckExpectedErrorsDiscardsAdditionalParserRecoveryDiagnostics(t *testing.T) { + tests := []struct { + name string + source string + output func(file string) string + }{ + { + name: "variadic result Go 1.24", + source: `package p +func g(x int, y float32) (...) // ERROR "[.][.][.]" +`, + output: func(file string) string { + return file + ":2: syntax error: ... is missing type\n" + + file + ":2: expected type, found ')'\n" + }, + }, + { + name: "variadic result Go 1.25 and newer", + source: `package p +func g(x int, y float32) (...) // ERROR "[.][.][.]" +`, + output: func(file string) string { + return file + ":2: syntax error: ... is missing type\n" + + file + ":2: invalid use of ...\n" + + file + ":2: expected type, found ')'\n" + }, + }, + { + name: "channel send in if condition", + source: `package p +var c chan int +var v int +func f() { + if c <- v { // ERROR "cannot use c <- v as value|send statement used as value" + } +} +`, + output: func(file string) string { + return file + ":5: syntax error: cannot use c <- v as value\n" + + file + ":5: expected boolean expression, found simple statement (missing parentheses around composite literal?)\n" + }, + }, + { + name: "channel send at top level", + source: `package p +var c chan int +var v int +var _ = c <- v // ERROR "unexpected <-|send statement used as value" +`, + output: func(file string) string { + return file + ":4: syntax error: unexpected <- after top level declaration\n" + + file + ":4: expected ';', found '<-'\n" + }, + }, + { + name: "illegal declaration character", + source: `package p +var? // ERROR "invalid character U\+003F '\?'|invalid character 0x3f in input file" +`, + output: func(file string) string { + return file + ":2: invalid character U+003F '?'\n" + + file + ":2: expected 'IDENT', found 'ILLEGAL'\n" + + file + ":2: illegal character U+003F '?'\n" + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + file := filepath.Join(t.TempDir(), "case.go") + if err := os.WriteFile(file, []byte(tt.source), 0o644); err != nil { + t.Fatal(err) + } + if err := checkExpectedErrors(tt.output(file), file, "case.go"); err != nil { + t.Fatal(err) + } + }) + } +} + +func TestAdditionalParserRecoveryDiagnosticsFailOpen(t *testing.T) { + t.Run("missing primary", func(t *testing.T) { + file := filepath.Join(t.TempDir(), "case.go") + source := `package p +var? // ERROR "invalid character U\+003F '\?'" +` + if err := os.WriteFile(file, []byte(source), 0o644); err != nil { + t.Fatal(err) + } + output := file + ":2: expected 'IDENT', found 'ILLEGAL'\n" + + file + ":2: illegal character U+003F '?'\n" + err := checkExpectedErrors(output, file, "case.go") + if err == nil || !strings.Contains(err.Error(), `no match for "invalid character`) { + t.Fatalf("err=%v, want missing primary to fail", err) + } + }) + + t.Run("primary does not match ERROR", func(t *testing.T) { + file := filepath.Join(t.TempDir(), "case.go") + source := `package p +var? // ERROR "different diagnostic" +` + if err := os.WriteFile(file, []byte(source), 0o644); err != nil { + t.Fatal(err) + } + output := file + ":2: invalid character U+003F '?'\n" + + file + ":2: expected 'IDENT', found 'ILLEGAL'\n" + err := checkExpectedErrors(output, file, "case.go") + if err == nil || !strings.Contains(err.Error(), "expected 'IDENT', found 'ILLEGAL'") { + t.Fatalf("err=%v, want unmatched primary to leave recovery diagnostic visible", err) + } + }) + + t.Run("same line different illegal token", func(t *testing.T) { + file := filepath.Join(t.TempDir(), "case.go") + source := `package p +var _ = ?; var@ // ERROR "invalid character U\+003F '\?'" +` + if err := os.WriteFile(file, []byte(source), 0o644); err != nil { + t.Fatal(err) + } + output := file + ":2: invalid character U+003F '?'\n" + + file + ":2: expected 'IDENT', found 'ILLEGAL'\n" + err := checkExpectedErrors(output, file, "case.go") + if err == nil || !strings.Contains(err.Error(), "expected 'IDENT', found 'ILLEGAL'") { + t.Fatalf("err=%v, want the unrelated @ recovery diagnostic to remain", err) + } + }) + + t.Run("wrong line", func(t *testing.T) { + file := filepath.Join(t.TempDir(), "case.go") + source := `package p +var? // ERROR "invalid character U\+003F '\?'" + +` + if err := os.WriteFile(file, []byte(source), 0o644); err != nil { + t.Fatal(err) + } + output := file + ":2: invalid character U+003F '?'\n" + + file + ":3: expected 'IDENT', found 'ILLEGAL'\n" + err := checkExpectedErrors(output, file, "case.go") + if err == nil || !strings.Contains(err.Error(), ":3: expected 'IDENT', found 'ILLEGAL'") { + t.Fatalf("err=%v, want wrong-line recovery to remain", err) + } + }) + + t.Run("near match primary", func(t *testing.T) { + file := filepath.Join(t.TempDir(), "case.go") + source := `package p +var? // ERROR "invalid character U\+003F '\?'" +` + if err := os.WriteFile(file, []byte(source), 0o644); err != nil { + t.Fatal(err) + } + output := file + ":2: invalid character U+003F '?'.\n" + + file + ":2: expected 'IDENT', found 'ILLEGAL'\n" + err := checkExpectedErrors(output, file, "case.go") + if err == nil || !strings.Contains(err.Error(), "expected 'IDENT', found 'ILLEGAL'") { + t.Fatalf("err=%v, want recovery after near-match primary to remain", err) + } + }) + + t.Run("wrong file", func(t *testing.T) { + dir := t.TempDir() + primaryFile := filepath.Join(dir, "primary.go") + otherFile := filepath.Join(dir, "other.go") + if err := os.WriteFile(primaryFile, []byte(`package p +var? // ERROR "invalid character U\+003F '\?'" +`), 0o644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(otherFile, []byte("package p\nvar x int\n"), 0o644); err != nil { + t.Fatal(err) + } + output := primaryFile + ":2: invalid character U+003F '?'\n" + + otherFile + ":2: expected 'IDENT', found 'ILLEGAL'\n" + err := checkExpectedErrorsForFiles(output, []diagnosticSource{ + {full: primaryFile, short: "primary.go"}, + {full: otherFile, short: "other.go"}, + }) + if err == nil || !strings.Contains(err.Error(), "other.go:2: expected 'IDENT', found 'ILLEGAL'") { + t.Fatalf("err=%v, want wrong-file recovery to remain", err) + } + }) + + t.Run("line directive", func(t *testing.T) { + file := filepath.Join(t.TempDir(), "case.go") + source := `package p +//line remapped.go:2 +var? // ERROR "invalid character U\+003F '\?'" +` + if err := os.WriteFile(file, []byte(source), 0o644); err != nil { + t.Fatal(err) + } + output := file + ":3: invalid character U+003F '?'\n" + + file + ":3: expected 'IDENT', found 'ILLEGAL'\n" + err := checkExpectedErrors(output, file, "case.go") + if err == nil || !strings.Contains(err.Error(), "expected 'IDENT', found 'ILLEGAL'") { + t.Fatalf("err=%v, want line-remapped recovery to remain", err) + } + }) +} + func TestCheckExpectedErrorsScopesImportAlias(t *testing.T) { tests := []struct { name, src string @@ -831,6 +1033,37 @@ func TestDiscardPairedParserDiagnosticsIsExactMultiset(t *testing.T) { if !reflect.DeepEqual(got, want) { t.Fatalf("discardPairedParserDiagnostics()=%v, want %v", got, want) } + + identifier := fileA + ":2: expected 'IDENT', found 'ILLEGAL'" + illegal := fileA + ":2: illegal character U+003F '?'" + pairs = []parserRecoveryPair{ + {file: canonicalDiagnosticPath(fileA), line: 2, secondaries: []string{"expected 'IDENT', found 'ILLEGAL'"}}, + {file: canonicalDiagnosticPath(fileA), line: 2, secondaries: []string{"illegal character U+003F '?'"}}, + } + got = discardPairedParserDiagnostics([]string{identifier, identifier, illegal, illegal}, resolver, pairs) + want = []string{identifier, illegal} + if !reflect.DeepEqual(got, want) { + t.Fatalf("independent groups=%v, want %v", got, want) + } +} + +func TestDiagnosticPathResolverRejectsUnknownAndAmbiguousSources(t *testing.T) { + dir := t.TempDir() + left := filepath.Join(dir, "left", "case.go") + right := filepath.Join(dir, "right", "case.go") + resolver := newDiagnosticPathResolver([]diagnosticSource{ + {full: left, short: "case.go"}, + {full: right, short: "case.go"}, + }) + if _, ok := resolver.resolve("case.go"); ok { + t.Fatal("ambiguous short path resolved") + } + if _, ok := resolver.resolve(filepath.Join(dir, "unknown", "case.go")); ok { + t.Fatal("unknown absolute path resolved") + } + if got, ok := resolver.resolve(left); !ok || got != canonicalDiagnosticPath(left) { + t.Fatalf("known absolute path resolved to %q, %v", got, ok) + } } func TestPreferSpecificDiagnostics(t *testing.T) { diff --git a/test/goroot/xfail.yaml b/test/goroot/xfail.yaml index b7c99a9d5a..88b5361a27 100644 --- a/test/goroot/xfail.yaml +++ b/test/goroot/xfail.yaml @@ -2201,14 +2201,6 @@ xfails: directive: errorcheck case: fixedbugs/bug121.go reason: llgo parser recovery emits additional malformed-declaration diagnostics - - version: go1.26 - directive: errorcheck - case: fixedbugs/bug228.go - reason: llgo parser emits an additional malformed-parameter diagnostic - - version: go1.26 - directive: errorcheck - case: syntax/chan1.go - reason: llgo parser recovery emits additional malformed-channel diagnostics - version: go1.26 directive: errorcheck case: escape_field.go @@ -2269,10 +2261,6 @@ xfails: directive: errorcheck case: internal/runtime/sys/inlinegcpc.go reason: gc runtime inlining diagnostics are not implemented by llgo - - version: go1.26 - directive: errorcheck - case: fixedbugs/issue11610.go - reason: llgo parser reports additional illegal-character diagnostics - version: go1.26 directive: errorcheck case: fixedbugs/issue22164.go From 160c2716b9bc3b80b943520b203501481b14713e Mon Sep 17 00:00:00 2001 From: Li Jie Date: Thu, 30 Jul 2026 08:37:24 +0800 Subject: [PATCH 05/10] test: tighten parser recovery pairing --- test/goroot/runner_test.go | 35 ++++++++++-- test/goroot/runner_unit_test.go | 95 +++++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+), 5 deletions(-) diff --git a/test/goroot/runner_test.go b/test/goroot/runner_test.go index ca2acca098..2033e90c11 100644 --- a/test/goroot/runner_test.go +++ b/test/goroot/runner_test.go @@ -12,6 +12,7 @@ import ( "go/constant" "go/format" "go/parser" + "go/scanner" "go/token" "io" "io/fs" @@ -1159,6 +1160,7 @@ func checkExpectedErrorsForFiles(output string, sources []diagnosticSource) erro continue } matched := false + parserRecoveryAuthorized := false for _, candidate := range candidates { diagnostic, ok := parseCompilerDiagnostic(candidate) sourceDiagnostic, sourceOK := parseSourceDiagnostic(candidate, pathResolver) @@ -1174,12 +1176,14 @@ func checkExpectedErrorsForFiles(output string, sources []diagnosticSource) erro if ok && isScopedLexicalDiagnostic(message) { lexicalLocations[diagnostic.locationKey()] = true } - if sourceOK && physicalDiagnosticSources[sourceDiagnostic.file] { - for _, secondaries := range parserRecoverySecondaryGroups(message, expected.source) { + if !parserRecoveryAuthorized && sourceOK && physicalDiagnosticSources[sourceDiagnostic.file] { + groups := parserRecoverySecondaryGroups(message, expected.source) + for _, secondaries := range groups { parserRecoveryPairs = append(parserRecoveryPairs, parserRecoveryPair{ file: sourceDiagnostic.file, line: sourceDiagnostic.line, secondaries: secondaries, }) } + parserRecoveryAuthorized = len(groups) != 0 } if !ok { diagnostic = compilerDiagnostic{ @@ -1724,18 +1728,22 @@ func parserRecoverySecondaryGroups(primary, source string) [][]string { } source = parserRecoverySourceCode(source) switch primary { + // GOROOT/test/fixedbugs/bug228.go case "syntax error: ... is missing type": if source == "func g(x int, y float32) (...)" { return [][]string{{"expected type, found ')'"}} } + // GOROOT/test/syntax/chan1.go: channel send in an if condition. case "syntax error: cannot use c <- v as value": if source == "if c <- v {" { return [][]string{{"expected boolean expression, found simple statement (missing parentheses around composite literal?)"}} } + // GOROOT/test/syntax/chan1.go: channel send in a top-level declaration. case "syntax error: unexpected <- after top level declaration": if source == "var _ = c <- v" { return [][]string{{"expected ';', found '<-'"}} } + // GOROOT/test/fixedbugs/issue11610.go case "invalid character U+003F '?'": if source == "var?" { return [][]string{ @@ -1763,12 +1771,29 @@ func parserRecoverySourceCode(source string) string { } func hasLineDirective(data []byte) bool { - for _, line := range strings.Split(string(data), "\n") { - if strings.Contains(line, "//line") || strings.Contains(line, "/*line") { + file := token.NewFileSet().AddFile("", -1, len(data)) + var sourceScanner scanner.Scanner + sourceScanner.Init(file, data, func(token.Position, string) {}, scanner.ScanComments) + for { + position, kind, literal := sourceScanner.Scan() + if kind == token.EOF { + return false + } + if kind != token.COMMENT { + continue + } + if strings.HasPrefix(literal, "/*line ") { + return true + } + if !strings.HasPrefix(literal, "//line ") { + continue + } + offset := file.Offset(position) + lineStart := bytes.LastIndexByte(data[:offset], '\n') + 1 + if len(bytes.TrimSpace(data[lineStart:offset])) == 0 { return true } } - return false } func discardPairedParserDiagnostics(lines []string, resolver diagnosticPathResolver, pairs []parserRecoveryPair) []string { diff --git a/test/goroot/runner_unit_test.go b/test/goroot/runner_unit_test.go index 0978ab9c99..54415a1670 100644 --- a/test/goroot/runner_unit_test.go +++ b/test/goroot/runner_unit_test.go @@ -869,6 +869,27 @@ var? // ERROR "invalid character U\+003F '\?'|invalid character 0x3f in input fi } func TestAdditionalParserRecoveryDiagnosticsFailOpen(t *testing.T) { + t.Run("one ERROR authorizes one recovery group", func(t *testing.T) { + file := filepath.Join(t.TempDir(), "case.go") + source := `package p +func f() { + if a := 10 { // ERROR "cannot use [ab] := 10 as value" + } +} +` + if err := os.WriteFile(file, []byte(source), 0o644); err != nil { + t.Fatal(err) + } + output := file + ":3: syntax error: cannot use a := 10 as value\n" + + file + ":3: syntax error: cannot use b := 10 as value\n" + + file + ":3: expected boolean expression, found assignment (missing parentheses around composite literal?)\n" + + file + ":3: expected boolean or range expression, found assignment (missing parentheses around composite literal?)\n" + err := checkExpectedErrors(output, file, "case.go") + if err == nil || !strings.Contains(err.Error(), "expected boolean or range expression") { + t.Fatalf("err=%v, want the second matching primary's recovery diagnostic to remain", err) + } + }) + t.Run("missing primary", func(t *testing.T) { file := filepath.Join(t.TempDir(), "case.go") source := `package p @@ -950,6 +971,27 @@ var? // ERROR "invalid character U\+003F '\?'" } }) + t.Run("wrong channel source shape", func(t *testing.T) { + file := filepath.Join(t.TempDir(), "case.go") + source := `package p +var c chan int +var v int +func f() { + if (c <- v) { // ERROR "cannot use c <- v as value" + } +} +` + if err := os.WriteFile(file, []byte(source), 0o644); err != nil { + t.Fatal(err) + } + output := file + ":5: syntax error: cannot use c <- v as value\n" + + file + ":5: expected boolean expression, found simple statement (missing parentheses around composite literal?)\n" + err := checkExpectedErrors(output, file, "case.go") + if err == nil || !strings.Contains(err.Error(), "expected boolean expression") { + t.Fatalf("err=%v, want recovery for a different source shape to remain", err) + } + }) + t.Run("wrong file", func(t *testing.T) { dir := t.TempDir() primaryFile := filepath.Join(dir, "primary.go") @@ -991,6 +1033,59 @@ var? // ERROR "invalid character U\+003F '\?'" }) } +func TestParserRecoverySourceCode(t *testing.T) { + tests := []struct { + name string + source string + want string + }{ + { + name: "URL before ERROR comment", + source: `var _ = "http://example.com" // ERROR "broken"`, + want: `var _ = "http://example.com"`, + }, + { + name: "URL without ERROR comment", + source: `var _ = "http://example.com"`, + want: `var _ = "http://example.com"`, + }, + { + name: "GC ERRORAUTO comment", + source: `var _ = "http://example.com" // GC_ERRORAUTO "broken"`, + want: `var _ = "http://example.com"`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := parserRecoverySourceCode(tt.source); got != tt.want { + t.Fatalf("parserRecoverySourceCode(%q)=%q, want %q", tt.source, got, tt.want) + } + }) + } +} + +func TestHasLineDirective(t *testing.T) { + tests := []struct { + name string + data string + want bool + }{ + {name: "line comment", data: "\t//line remapped.go:1\n", want: true}, + {name: "block comment", data: " /*line remapped.go:1*/\n", want: true}, + {name: "block comment after source", data: "x /*line remapped.go:1*/\n", want: true}, + {name: "missing separator", data: "//linefoo.go:1\n", want: false}, + {name: "after source", data: "x //line remapped.go:1\n", want: false}, + {name: "inside string", data: `var _ = "/*line remapped.go:1*/"` + "\n", want: false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := hasLineDirective([]byte(tt.data)); got != tt.want { + t.Fatalf("hasLineDirective(%q)=%v, want %v", tt.data, got, tt.want) + } + }) + } +} + func TestCheckExpectedErrorsScopesImportAlias(t *testing.T) { tests := []struct { name, src string From c802813fd9124c8549d70d595e331c6b7842ce85 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Thu, 30 Jul 2026 09:56:57 +0800 Subject: [PATCH 06/10] test/goroot: align parser recovery helpers --- test/goroot/runner_test.go | 42 ++++++++++++++++----------------- test/goroot/runner_unit_test.go | 2 ++ 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/test/goroot/runner_test.go b/test/goroot/runner_test.go index 2033e90c11..6ce5a6d7ed 100644 --- a/test/goroot/runner_test.go +++ b/test/goroot/runner_test.go @@ -1710,15 +1710,29 @@ func parserRecoverySourceSecondaries(primary, source string) []string { if source == `var x map[string]string{"a":"b"}` { return []string{"expected ';', found '{'"} } + // GOROOT/test/fixedbugs/bug228.go + case "syntax error: ... is missing type": + if source == "func g(x int, y float32) (...)" { + return []string{"expected type, found ')'"} + } + // GOROOT/test/syntax/chan1.go: channel send in an if condition. + case "syntax error: cannot use c <- v as value": + if source == "if c <- v {" { + return []string{"expected boolean expression, found simple statement (missing parentheses around composite literal?)"} + } + // GOROOT/test/syntax/chan1.go: channel send in a top-level declaration. + case "syntax error: unexpected <- after top level declaration": + if source == "var _ = c <- v" { + return []string{"expected ';', found '<-'"} + } } return nil } // parserRecoverySecondaryGroups returns independent one-use allowances for an -// exact primary. Source-independent pairs are checked first; all other pairs -// require an exact ERROR source shape. Most recovery spellings are alternatives -// in one group; issue11610 deterministically emits two separate follow-ons, so -// each receives its own group. +// exact primary. Source-independent pairs are checked first, followed by +// source-dependent single groups. issue11610 deterministically emits two +// separate follow-ons, so each receives its own group here. func parserRecoverySecondaryGroups(primary, source string) [][]string { if secondaries := parserRecoverySecondaries(primary); len(secondaries) != 0 { return [][]string{secondaries} @@ -1728,21 +1742,6 @@ func parserRecoverySecondaryGroups(primary, source string) [][]string { } source = parserRecoverySourceCode(source) switch primary { - // GOROOT/test/fixedbugs/bug228.go - case "syntax error: ... is missing type": - if source == "func g(x int, y float32) (...)" { - return [][]string{{"expected type, found ')'"}} - } - // GOROOT/test/syntax/chan1.go: channel send in an if condition. - case "syntax error: cannot use c <- v as value": - if source == "if c <- v {" { - return [][]string{{"expected boolean expression, found simple statement (missing parentheses around composite literal?)"}} - } - // GOROOT/test/syntax/chan1.go: channel send in a top-level declaration. - case "syntax error: unexpected <- after top level declaration": - if source == "var _ = c <- v" { - return [][]string{{"expected ';', found '<-'"}} - } // GOROOT/test/fixedbugs/issue11610.go case "invalid character U+003F '?'": if source == "var?" { @@ -1782,7 +1781,7 @@ func hasLineDirective(data []byte) bool { if kind != token.COMMENT { continue } - if strings.HasPrefix(literal, "/*line ") { + if strings.HasPrefix(literal, "/*line ") && strings.Contains(literal[len("/*line "):], ":") { return true } if !strings.HasPrefix(literal, "//line ") { @@ -1790,7 +1789,8 @@ func hasLineDirective(data []byte) bool { } offset := file.Offset(position) lineStart := bytes.LastIndexByte(data[:offset], '\n') + 1 - if len(bytes.TrimSpace(data[lineStart:offset])) == 0 { + if len(bytes.TrimSpace(data[lineStart:offset])) == 0 && + strings.Contains(literal[len("//line "):], ":") { return true } } diff --git a/test/goroot/runner_unit_test.go b/test/goroot/runner_unit_test.go index 54415a1670..deab81b0ce 100644 --- a/test/goroot/runner_unit_test.go +++ b/test/goroot/runner_unit_test.go @@ -1074,6 +1074,8 @@ func TestHasLineDirective(t *testing.T) { {name: "block comment", data: " /*line remapped.go:1*/\n", want: true}, {name: "block comment after source", data: "x /*line remapped.go:1*/\n", want: true}, {name: "missing separator", data: "//linefoo.go:1\n", want: false}, + {name: "line comment without line number", data: "//line remapped\n", want: false}, + {name: "block comment without line number", data: "/*line remapped*/\n", want: false}, {name: "after source", data: "x //line remapped.go:1\n", want: false}, {name: "inside string", data: `var _ = "/*line remapped.go:1*/"` + "\n", want: false}, } From f75b935790334609690ed3229d6e3398173f8f50 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Thu, 30 Jul 2026 10:01:20 +0800 Subject: [PATCH 07/10] cl: tolerate invalid receivers during syntax preload --- cl/import.go | 84 ++++++++++++++++++++++++++------------ cl/import_coverage_test.go | 51 +++++++++++++++++++++++ 2 files changed, 110 insertions(+), 25 deletions(-) diff --git a/cl/import.go b/cl/import.go index 5b18496eac..32b4e69627 100644 --- a/cl/import.go +++ b/cl/import.go @@ -383,25 +383,38 @@ func (p *context) initLink(line string, prefix int, export bool, f func(inPkgNam } func recvTypeName(typ ast.Expr) string { -retry: + name, _, ok := recvTypeNameInfo(typ) + if !ok { + panic("unreachable") + } + return name +} + +// recvTypeNameInfo normalizes the parentheses permitted in receiver +// declarations and reports the uninstantiated base name and pointer form. +func recvTypeNameInfo(typ ast.Expr) (name string, pointer bool, ok bool) { + typ = ast.Unparen(typ) + if star, isPointer := typ.(*ast.StarExpr); isPointer { + pointer = true + typ = ast.Unparen(star.X) + } switch t := typ.(type) { case *ast.Ident: - return t.Name + return t.Name, pointer, true case *ast.IndexExpr: - return trecvTypeName(t.X, t.Index) + base, valid := ast.Unparen(t.X).(*ast.Ident) + if !valid { + return "", false, false + } + return base.Name, pointer, true case *ast.IndexListExpr: - return trecvTypeName(t.X, t.Indices...) - case *ast.ParenExpr: - typ = t.X - goto retry + base, valid := ast.Unparen(t.X).(*ast.Ident) + if !valid { + return "", false, false + } + return base.Name, pointer, true } - panic("unreachable") -} - -// TODO(xsw): support generic type -func trecvTypeName(t ast.Expr, indices ...ast.Expr) string { - _ = indices - return t.(*ast.Ident).Name + return "", false, false } // inPkgName: @@ -411,18 +424,33 @@ func trecvTypeName(t ast.Expr, indices ...ast.Expr) string { // - func: pkg.name // - method: pkg.(T).name, pkg.(*T).name func astFuncName(pkgPath string, fn *ast.FuncDecl) (fullName, inPkgName string) { + fullName, inPkgName, ok := astFuncNameOK(pkgPath, fn) + if !ok { + panic("unreachable") + } + return fullName, inPkgName +} + +func astFuncNameOK(pkgPath string, fn *ast.FuncDecl) (fullName, inPkgName string, ok bool) { + if fn == nil || fn.Name == nil { + return "", "", false + } name := fn.Name.Name - if recv := fn.Recv; recv != nil && len(recv.List) == 1 { - var method string - t := recv.List[0].Type - if tp, ok := t.(*ast.StarExpr); ok { - method = "(*" + recvTypeName(tp.X) + ")." + name - } else { - method = recvTypeName(t) + "." + name - } - return pkgPath + "." + method, method + if fn.Recv == nil { + return pkgPath + "." + name, name, true } - return pkgPath + "." + name, name + if len(fn.Recv.List) != 1 || fn.Recv.List[0] == nil { + return "", "", false + } + receiverName, pointer, ok := recvTypeNameInfo(fn.Recv.List[0].Type) + if !ok { + return "", "", false + } + method := receiverName + "." + name + if pointer { + method = "(*" + receiverName + ")." + name + } + return pkgPath + "." + method, method, true } func typesFuncName(pkgPath string, fn *types.Func) (fullName, inPkgName string) { @@ -772,7 +800,13 @@ func ParsePkgSyntax(prog llssa.Program, fset *token.FileSet, pkg *types.Package, if err := locality.ValidateFuncBody(fset, decl.Body); err != nil { return err } - fullName, inPkgName := astFuncName(pkgPath, decl) + // A syntactically valid declaration may still have an invalid + // receiver type. The type checker reports that error; this + // syntax-only pass must not assume its receiver shape. + fullName, inPkgName, ok := astFuncNameOK(pkgPath, decl) + if !ok { + continue + } collectLinknameByDoc(prog, decl.Doc, fullName, inPkgName) ctx.processNoInterfaceByDoc(decl.Doc, fullName) case *ast.GenDecl: diff --git a/cl/import_coverage_test.go b/cl/import_coverage_test.go index 4afa5b0110..7d040b3813 100644 --- a/cl/import_coverage_test.go +++ b/cl/import_coverage_test.go @@ -127,6 +127,57 @@ func TestParsePkgSyntaxReportsLocalityErrors(t *testing.T) { } } +func TestParsePkgSyntaxSkipsInvalidReceiver(t *testing.T) { + const source = `package p +//go:linkname m C.invalid +func ([]int) m() {} + +type T struct{} + +//go:linkname (*T).ParenPointer C.parenPointer +func ((*T)) ParenPointer() {} + +//go:linkname (*T).InnerParenPointer C.innerParenPointer +func (*(T)) InnerParenPointer() {} + +type G[P any] struct{} + +//go:linkname G.Value C.genericValue +func (G[P]) Value() {} + +//go:linkname (*G).Pointer C.genericPointer +func ((*G[P])) Pointer() {} + +//go:linkname F C.f +func F() +` + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, "p.go", source, parser.ParseComments) + if err != nil { + t.Fatalf("ParseFile failed: %v", err) + } + prog := llssa.NewProgram(nil) + pkg := types.NewPackage("example.com/p", "p") + if err := ParsePkgSyntax(prog, fset, pkg, []*ast.File{file}); err != nil { + t.Fatal(err) + } + if _, ok := prog.Linkname(pkg.Path() + ".m"); ok { + t.Fatal("linkname was collected for an invalid receiver") + } + want := map[string]string{ + pkg.Path() + ".(*T).ParenPointer": "C.parenPointer", + pkg.Path() + ".(*T).InnerParenPointer": "C.innerParenPointer", + pkg.Path() + ".G.Value": "C.genericValue", + pkg.Path() + ".(*G).Pointer": "C.genericPointer", + pkg.Path() + ".F": "C.f", + } + for fullName, target := range want { + if got, ok := prog.Linkname(fullName); !ok || got != target { + t.Errorf("linkname %q = (%q,%v), want (%q,%v)", fullName, got, ok, target, true) + } + } +} + func TestPkgSymInfoAddSymAndInitLinknamesCoverage(t *testing.T) { dir := t.TempDir() srcPath := filepath.Join(dir, "p.go") From e8809f1840d5faf941f3fa5c69e11522a4cb441a Mon Sep 17 00:00:00 2001 From: Li Jie Date: Thu, 30 Jul 2026 09:13:02 +0800 Subject: [PATCH 08/10] test/goroot: match malformed receiver recovery --- test/goroot/runner_test.go | 23 ++++++++++- test/goroot/runner_unit_test.go | 72 +++++++++++++++++++++++++++++++++ test/goroot/xfail.yaml | 4 -- 3 files changed, 93 insertions(+), 6 deletions(-) diff --git a/test/goroot/runner_test.go b/test/goroot/runner_test.go index 6ce5a6d7ed..98eaf4eb6a 100644 --- a/test/goroot/runner_test.go +++ b/test/goroot/runner_test.go @@ -1731,8 +1731,8 @@ func parserRecoverySourceSecondaries(primary, source string) []string { // parserRecoverySecondaryGroups returns independent one-use allowances for an // exact primary. Source-independent pairs are checked first, followed by -// source-dependent single groups. issue11610 deterministically emits two -// separate follow-ons, so each receives its own group here. +// source-dependent single groups. A few exact GOROOT sources deterministically +// emit multiple follow-ons, so each receives its own group here. func parserRecoverySecondaryGroups(primary, source string) [][]string { if secondaries := parserRecoverySecondaries(primary); len(secondaries) != 0 { return [][]string{secondaries} @@ -1750,6 +1750,25 @@ func parserRecoverySecondaryGroups(primary, source string) [][]string { {"illegal character U+003F '?'"}, } } + // GOROOT/test/fixedbugs/issue20789.go + case "syntax error: unexpected name u, expected (": + if source == "func([<-chan<-[func u){go" { + return [][]string{ + {"expected channel type"}, + {"expected '(', found u"}, + {"expected '(', found 'EOF'"}, + {"expected ')', found 'EOF'"}, + {"expected ';', found 'EOF'"}, + {"expected 'IDENT', found 'EOF'"}, + {"expected ']', found 'EOF'"}, + {"expected '}', found 'EOF'"}, + {"expected operand, found 'EOF'"}, + {"expected type, found 'EOF'"}, + {"missing ',' in parameter list"}, + {"array length (func() literal) (value of type func()) must be constant"}, + {"<-chan [(func() literal)](ast: *ast.BadExpr) (type) is not an expression"}, + } + } } return nil } diff --git a/test/goroot/runner_unit_test.go b/test/goroot/runner_unit_test.go index deab81b0ce..25a2a8d033 100644 --- a/test/goroot/runner_unit_test.go +++ b/test/goroot/runner_unit_test.go @@ -1033,6 +1033,78 @@ var? // ERROR "invalid character U\+003F '\?'" }) } +func TestCheckExpectedErrorsScopesMalformedReceiverRecovery(t *testing.T) { + const primary = "syntax error: unexpected name u, expected (" + secondaries := []string{ + "expected channel type", + "expected '(', found u", + "expected '(', found 'EOF'", + "expected ')', found 'EOF'", + "expected ';', found 'EOF'", + "expected 'IDENT', found 'EOF'", + "expected ']', found 'EOF'", + "expected '}', found 'EOF'", + "expected operand, found 'EOF'", + "expected type, found 'EOF'", + "missing ',' in parameter list", + "array length (func() literal) (value of type func()) must be constant", + "<-chan [(func() literal)](ast: *ast.BadExpr) (type) is not an expression", + } + outputFor := func(file string) string { + lines := []string{file + ":2: " + primary} + for _, secondary := range secondaries { + lines = append(lines, file+":2: "+secondary) + } + return strings.Join(lines, "\n") + } + tests := []struct { + name string + source string + extra string + wantOK bool + wantErr string + }{ + { + name: "exact source", + source: `package e +func([<-chan<-[func u){go // ERROR "unexpected name u" +`, + wantOK: true, + }, + { + name: "different source", + source: `package e +func([<-chan<-[func v){go // ERROR "unexpected name u" +`, + wantErr: "expected channel type", + }, + { + name: "unrelated diagnostic", + source: `package e +func([<-chan<-[func u){go // ERROR "unexpected name u" +`, + extra: "\ncase.go:2: unrelated diagnostic", + wantErr: "unrelated diagnostic", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + file := filepath.Join(t.TempDir(), "case.go") + if err := os.WriteFile(file, []byte(tt.source), 0o644); err != nil { + t.Fatal(err) + } + extra := strings.ReplaceAll(tt.extra, "case.go", file) + err := checkExpectedErrors(outputFor(file)+extra, file, "case.go") + if tt.wantOK && err != nil { + t.Fatalf("err=%v, want success", err) + } + if !tt.wantOK && (err == nil || !strings.Contains(err.Error(), tt.wantErr)) { + t.Fatalf("err=%v, want failure containing %q", err, tt.wantErr) + } + }) + } +} + func TestParserRecoverySourceCode(t *testing.T) { tests := []struct { name string diff --git a/test/goroot/xfail.yaml b/test/goroot/xfail.yaml index 88b5361a27..6a03acc443 100644 --- a/test/goroot/xfail.yaml +++ b/test/goroot/xfail.yaml @@ -2045,10 +2045,6 @@ xfails: directive: errorcheck case: fixedbugs/issue67329.go reason: gc-specific bounds-check elimination diagnostics are not implemented by llgo - - version: go1.26 - directive: errorcheck - case: fixedbugs/issue20789.go - reason: llgo parser does not emit the expected malformed-name diagnostic - version: go1.26 directive: errorcheck case: syntax/initvar.go From b54af11c82baae7cb5394986dc072e9db9c1b456 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Thu, 30 Jul 2026 10:33:21 +0800 Subject: [PATCH 09/10] cl: share generic receiver normalization --- cl/import.go | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/cl/import.go b/cl/import.go index 32b4e69627..5dbe6c486c 100644 --- a/cl/import.go +++ b/cl/import.go @@ -402,19 +402,17 @@ func recvTypeNameInfo(typ ast.Expr) (name string, pointer bool, ok bool) { case *ast.Ident: return t.Name, pointer, true case *ast.IndexExpr: - base, valid := ast.Unparen(t.X).(*ast.Ident) - if !valid { - return "", false, false - } - return base.Name, pointer, true + typ = t.X case *ast.IndexListExpr: - base, valid := ast.Unparen(t.X).(*ast.Ident) - if !valid { - return "", false, false - } - return base.Name, pointer, true + typ = t.X + default: + return "", false, false + } + base, valid := ast.Unparen(typ).(*ast.Ident) + if !valid { + return "", false, false } - return "", false, false + return base.Name, pointer, true } // inPkgName: From 51d67b23cf9fb277e591cf2d77097d6b4e5a10c4 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Thu, 30 Jul 2026 12:35:40 +0800 Subject: [PATCH 10/10] test: cover malformed receiver helper guards --- cl/import.go | 4 +++ cl/import_coverage_test.go | 63 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/cl/import.go b/cl/import.go index 5dbe6c486c..2f3a9fb9b7 100644 --- a/cl/import.go +++ b/cl/import.go @@ -382,6 +382,8 @@ func (p *context) initLink(line string, prefix int, export bool, f func(inPkgNam } } +// recvTypeName asserts the post-typecheck receiver invariant. Syntax preload +// uses recvTypeNameInfo so malformed declarations can be skipped safely. func recvTypeName(typ ast.Expr) string { name, _, ok := recvTypeNameInfo(typ) if !ok { @@ -421,6 +423,8 @@ func recvTypeNameInfo(typ ast.Expr) (name string, pointer bool, ok bool) { // fullName: // - func: pkg.name // - method: pkg.(T).name, pkg.(*T).name +// astFuncName asserts the post-typecheck declaration invariant. Syntax preload +// uses astFuncNameOK so malformed declarations can be skipped safely. func astFuncName(pkgPath string, fn *ast.FuncDecl) (fullName, inPkgName string) { fullName, inPkgName, ok := astFuncNameOK(pkgPath, fn) if !ok { diff --git a/cl/import_coverage_test.go b/cl/import_coverage_test.go index 7d040b3813..27f84e45bf 100644 --- a/cl/import_coverage_test.go +++ b/cl/import_coverage_test.go @@ -272,6 +272,69 @@ func TestAstAndTypesFuncNameCoverage(t *testing.T) { } } +func TestReceiverNameHelpersRejectMalformedSyntax(t *testing.T) { + invalidBase := &ast.IndexExpr{ + X: &ast.SelectorExpr{X: &ast.Ident{Name: "pkg"}, Sel: &ast.Ident{Name: "T"}}, + Index: &ast.Ident{Name: "P"}, + } + if name, pointer, ok := recvTypeNameInfo(invalidBase); ok || name != "" || pointer { + t.Fatalf("recvTypeNameInfo(invalid indexed base) = (%q, %v, %v), want empty false false", name, pointer, ok) + } + if name, pointer, ok := recvTypeNameInfo(&ast.ArrayType{Elt: &ast.Ident{Name: "int"}}); ok || name != "" || pointer { + t.Fatalf("recvTypeNameInfo(array) = (%q, %v, %v), want empty false false", name, pointer, ok) + } + indexed := &ast.IndexListExpr{ + X: &ast.ParenExpr{X: &ast.Ident{Name: "G"}}, + Indices: []ast.Expr{&ast.Ident{Name: "P"}, &ast.Ident{Name: "Q"}}, + } + if name, pointer, ok := recvTypeNameInfo(indexed); !ok || name != "G" || pointer { + t.Fatalf("recvTypeNameInfo(index list) = (%q, %v, %v), want G false true", name, pointer, ok) + } + + invalidDecls := []struct { + name string + fn *ast.FuncDecl + }{ + {name: "nil declaration"}, + {name: "nil name", fn: &ast.FuncDecl{}}, + {name: "empty receiver list", fn: &ast.FuncDecl{ + Name: &ast.Ident{Name: "M"}, Recv: &ast.FieldList{}, + }}, + {name: "nil receiver field", fn: &ast.FuncDecl{ + Name: &ast.Ident{Name: "M"}, Recv: &ast.FieldList{List: []*ast.Field{nil}}, + }}, + {name: "invalid receiver type", fn: &ast.FuncDecl{ + Name: &ast.Ident{Name: "M"}, + Recv: &ast.FieldList{List: []*ast.Field{{Type: &ast.ArrayType{ + Elt: &ast.Ident{Name: "int"}, + }}}}, + }}, + } + for _, tt := range invalidDecls { + t.Run(tt.name, func(t *testing.T) { + if full, inPkg, ok := astFuncNameOK("example.com/p", tt.fn); ok || full != "" || inPkg != "" { + t.Fatalf("astFuncNameOK = (%q, %q, %v), want empty empty false", full, inPkg, ok) + } + }) + } + + expectPanic := func(t *testing.T, call func()) { + t.Helper() + defer func() { + if recover() == nil { + t.Fatal("call did not panic") + } + }() + call() + } + t.Run("receiver invariant wrapper", func(t *testing.T) { + expectPanic(t, func() { recvTypeName(invalidBase) }) + }) + t.Run("function invariant wrapper", func(t *testing.T) { + expectPanic(t, func() { astFuncName("example.com/p", nil) }) + }) +} + func TestParsePkgSyntaxCollectsLinknames(t *testing.T) { cases := []struct { name string