From 87ca65ebe4779519f31becb48ebda0cb10477659 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Thu, 30 Jul 2026 06:31:28 +0800 Subject: [PATCH 1/7] 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 2/7] 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 3/7] 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 4/7] 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 5/7] 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 6/7] 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 27c5a389d05dfc573ca21b945bb005f622126da5 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Thu, 30 Jul 2026 11:44:49 +0800 Subject: [PATCH 7/7] test/goroot: match vareq recovery diagnostics --- test/goroot/runner_test.go | 14 +++++- test/goroot/runner_unit_test.go | 82 +++++++++++++++++++++++++++++++++ test/goroot/xfail.yaml | 4 -- 3 files changed, 94 insertions(+), 6 deletions(-) diff --git a/test/goroot/runner_test.go b/test/goroot/runner_test.go index 6ce5a6d7ed..08ed5bea44 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} @@ -1742,6 +1742,16 @@ func parserRecoverySecondaryGroups(primary, source string) [][]string { } source = parserRecoverySourceCode(source) switch primary { + // GOROOT/test/syntax/vareq.go + case "syntax error: unexpected { at end of statement": + if source == `var x map[string]string{"a":"b"}` { + return [][]string{ + {"expected ';', found '{'"}, + {"expected ';', found 'EOF'"}, + {"expected '}', found 'EOF'"}, + {"declared and not used: x"}, + } + } // GOROOT/test/fixedbugs/issue11610.go case "invalid character U+003F '?'": if source == "var?" { diff --git a/test/goroot/runner_unit_test.go b/test/goroot/runner_unit_test.go index deab81b0ce..85a93ed451 100644 --- a/test/goroot/runner_unit_test.go +++ b/test/goroot/runner_unit_test.go @@ -868,6 +868,88 @@ var? // ERROR "invalid character U\+003F '\?'|invalid character 0x3f in input fi } } +func TestCheckExpectedErrorsScopesVareqRecovery(t *testing.T) { + const ( + source = `package main +func main() { + var x map[string]string{"a":"b"} // ERROR "unexpected { at end of statement|expected ';' or '}' or newline"` + primary = "syntax error: unexpected { at end of statement" + ) + secondaries := []string{ + "expected ';', found '{'", + "expected ';', found 'EOF'", + "expected '}', found 'EOF'", + "declared and not used: x", + } + check := func(t *testing.T, source string, includePrimary bool, secondaryLine int, extras ...string) error { + t.Helper() + file := filepath.Join(t.TempDir(), "case.go") + if err := os.WriteFile(file, []byte(source), 0o644); err != nil { + t.Fatal(err) + } + var output []string + if includePrimary { + output = append(output, file+":3: "+primary) + } + for _, secondary := range secondaries { + output = append(output, fmt.Sprintf("%s:%d: %s", file, secondaryLine, secondary)) + } + for _, extra := range extras { + output = append(output, fmt.Sprintf("%s:%d: %s", file, secondaryLine, extra)) + } + return checkExpectedErrors(strings.Join(output, "\n"), file, "case.go") + } + + t.Run("exact source primary and line", func(t *testing.T) { + if err := check(t, source, true, 3); err != nil { + t.Fatal(err) + } + }) + t.Run("wrong source", func(t *testing.T) { + wrong := strings.Replace(source, `"a":"b"`, `"a":"c"`, 1) + if err := check(t, wrong, true, 3); err == nil || !strings.Contains(err.Error(), secondaries[0]) { + t.Fatalf("err=%v, want recovery diagnostics to remain", err) + } + }) + t.Run("missing primary", func(t *testing.T) { + if err := check(t, source, false, 3); err == nil || !strings.Contains(err.Error(), "no match") { + t.Fatalf("err=%v, want missing primary to fail", err) + } + }) + t.Run("wrong line", func(t *testing.T) { + if err := check(t, source, true, 4); err == nil || !strings.Contains(err.Error(), secondaries[0]) { + t.Fatalf("err=%v, want wrong-line recovery diagnostics to remain", err) + } + }) + t.Run("unrelated type diagnostic", func(t *testing.T) { + const extra = "undefined: still_reported" + if err := check(t, source, true, 3, extra); err == nil || !strings.Contains(err.Error(), extra) { + t.Fatalf("err=%v, want unrelated diagnostic to remain", err) + } + }) + t.Run("duplicate secondary", func(t *testing.T) { + // The complete pipeline deduplicates exact diagnostics before pairing, + // so exercise the one-use recovery matcher directly. + file := filepath.Join(t.TempDir(), "case.go") + pairs := make([]parserRecoveryPair, 0, len(secondaries)) + sourceLine := strings.Split(source, "\n")[2] + for _, group := range parserRecoverySecondaryGroups(primary, sourceLine) { + pairs = append(pairs, parserRecoveryPair{ + file: canonicalDiagnosticPath(file), line: 3, secondaries: group, + }) + } + duplicate := file + ":3: declared and not used: x" + got := discardPairedParserDiagnostics( + []string{duplicate, duplicate}, + newDiagnosticPathResolver([]diagnosticSource{{full: file, short: "case.go"}}), + pairs, + ) + if want := []string{duplicate}; !reflect.DeepEqual(got, want) { + t.Fatalf("duplicate recovery diagnostics=%v, want %v", got, want) + } + }) +} + func TestAdditionalParserRecoveryDiagnosticsFailOpen(t *testing.T) { t.Run("one ERROR authorizes one recovery group", func(t *testing.T) { file := filepath.Join(t.TempDir(), "case.go") diff --git a/test/goroot/xfail.yaml b/test/goroot/xfail.yaml index 88b5361a27..1e53075998 100644 --- a/test/goroot/xfail.yaml +++ b/test/goroot/xfail.yaml @@ -2309,10 +2309,6 @@ xfails: directive: errorcheck case: nilptr5.go reason: gc-specific -d=nil optimization diagnostics are not implemented by llgo - - version: go1.26 - directive: errorcheck - case: syntax/vareq.go - reason: llgo parser recovery emits additional diagnostics after the expected syntax error - version: go1.26 directive: compile case: typeparam/issue50993.go