From cb87a73cfa474635e56bb5fc7a9e16c923f1eae5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Nov 2026 16:34:56 +0000 Subject: [PATCH 1/7] Initial plan From 1f1df10ea129ba4e13b243fcc422b4abd100666a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Nov 2026 16:40:42 +0000 Subject: [PATCH 2/7] Add diagnostic formatting argument count lint rule and tests Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com> --- tools/customlint/diagnosticargs.go | 142 ++++++++++++++++++ tools/customlint/plugin.go | 1 + .../testdata/diagnosticargs/diagnosticargs.go | 31 ++++ .../diagnosticargs/diagnosticargs.go.golden | 48 ++++++ .../internal/diagnostics/diagnostics.go | 14 ++ .../diagnostics/diagnostics.go.golden | 15 ++ 6 files changed, 251 insertions(+) create mode 100644 tools/customlint/diagnosticargs.go create mode 100644 tools/customlint/testdata/diagnosticargs/diagnosticargs.go create mode 100644 tools/customlint/testdata/diagnosticargs/diagnosticargs.go.golden create mode 100644 tools/customlint/testdata/internal/diagnostics/diagnostics.go create mode 100644 tools/customlint/testdata/internal/diagnostics/diagnostics.go.golden diff --git a/tools/customlint/diagnosticargs.go b/tools/customlint/diagnosticargs.go new file mode 100644 index 0000000000000..5269295e60d92 --- /dev/null +++ b/tools/customlint/diagnosticargs.go @@ -0,0 +1,142 @@ +package customlint + +import ( + "go/ast" + "go/constant" + "go/token" + "go/types" + "regexp" + "strconv" + "strings" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/inspector" +) + +var diagnosticArgsAnalyzer = &analysis.Analyzer{ + Name: "diagnosticargs", + Doc: "checks diagnostic message formatting argument counts", + Requires: []*analysis.Analyzer{inspect.Analyzer}, + FactTypes: []analysis.Fact{new(diagnosticArgumentCount)}, + Run: runDiagnosticArgs, +} + +type diagnosticArgumentCount struct { + Count int +} + +func (*diagnosticArgumentCount) AFact() {} + +var diagnosticPlaceholder = regexp.MustCompile(`\{(\d+)\}`) + +func isDiagnosticMessage(t types.Type) bool { + pointer, ok := types.Unalias(t).(*types.Pointer) + if !ok { + return false + } + named, ok := types.Unalias(pointer.Elem()).(*types.Named) + return ok && named.Obj().Name() == "Message" && named.Obj().Pkg() != nil && + strings.HasSuffix(named.Obj().Pkg().Path(), "/internal/diagnostics") +} + +func runDiagnosticArgs(pass *analysis.Pass) (any, error) { + counts := make(map[types.Object]diagnosticArgumentCount) + if strings.HasSuffix(pass.Pkg.Path(), "/internal/diagnostics") { + for _, file := range pass.Files { + for _, decl := range file.Decls { + gen, ok := decl.(*ast.GenDecl) + if !ok || gen.Tok != token.VAR { + continue + } + for _, spec := range gen.Specs { + value := spec.(*ast.ValueSpec) + for i, expr := range value.Values { + unary, ok := expr.(*ast.UnaryExpr) + if !ok || unary.Op != token.AND || !isDiagnosticMessage(pass.TypesInfo.TypeOf(expr)) { + continue + } + literal, ok := unary.X.(*ast.CompositeLit) + if !ok { + continue + } + for _, element := range literal.Elts { + field, ok := element.(*ast.KeyValueExpr) + if !ok { + continue + } + key, ok := field.Key.(*ast.Ident) + if !ok || key.Name != "text" { + continue + } + text := pass.TypesInfo.Types[field.Value].Value + if text == nil || text.Kind() != constant.String { + continue + } + count := diagnosticArgumentCount{} + for _, match := range diagnosticPlaceholder.FindAllStringSubmatch(constant.StringVal(text), -1) { + index, err := strconv.Atoi(match[1]) + if err == nil { + count.Count = max(count.Count, index+1) + } + } + object := pass.TypesInfo.Defs[value.Names[i]] + counts[object] = count + pass.ExportObjectFact(object, &count) + } + } + } + } + } + } + + check := func(call *ast.CallExpr, message ast.Expr, actual int) { + var object types.Object + switch expr := ast.Unparen(message).(type) { + case *ast.Ident: + object = pass.TypesInfo.Uses[expr] + case *ast.SelectorExpr: + object = pass.TypesInfo.Uses[expr.Sel] + } + if object == nil { + return + } + count, ok := counts[object] + if !ok && !pass.ImportObjectFact(object, &count) { + return + } + if actual != count.Count { + pass.ReportRangef(call, "diagnostic %s expects %d formatting arguments, got %d", object.Name(), count.Count, actual) + } + } + + in := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + in.Preorder([]ast.Node{(*ast.CallExpr)(nil)}, func(node ast.Node) { + call := node.(*ast.CallExpr) + if call.Ellipsis.IsValid() { + return + } + signature, ok := pass.TypesInfo.TypeOf(call.Fun).(*types.Signature) + if !ok || !signature.Variadic() { + return + } + fixed := signature.Params().Len() - 1 + if len(call.Args) < fixed { + return + } + for i := range fixed { + if isDiagnosticMessage(signature.Params().At(i).Type()) { + check(call, call.Args[i], len(call.Args)-fixed) + return + } + } + if selector, ok := call.Fun.(*ast.SelectorExpr); ok { + selection := pass.TypesInfo.Selections[selector] + if selection != nil && selection.Kind() == types.MethodVal && + isDiagnosticMessage(selection.Recv()) && selector.Sel.Name == "Localize" { + check(call, selector.X, len(call.Args)-fixed) + } + } + }) + return nil, nil +} diff --git a/tools/customlint/plugin.go b/tools/customlint/plugin.go index 476b68e7a8901..410512a76e6b1 100644 --- a/tools/customlint/plugin.go +++ b/tools/customlint/plugin.go @@ -19,6 +19,7 @@ func (f *plugin) BuildAnalyzers() ([]*analysis.Analyzer, error) { checkChildrenAnalyzer, cleanupAnalyzer, caseBodyAnalyzer, + diagnosticArgsAnalyzer, forbidParentAccessAnalyzer, protectedFieldAnalyzer, shadowAnalyzer, diff --git a/tools/customlint/testdata/diagnosticargs/diagnosticargs.go b/tools/customlint/testdata/diagnosticargs/diagnosticargs.go new file mode 100644 index 0000000000000..e2ad018ae2730 --- /dev/null +++ b/tools/customlint/testdata/diagnosticargs/diagnosticargs.go @@ -0,0 +1,31 @@ +package diagnosticargs + +import d "testdata/internal/diagnostics" + +func diagnostic(message *d.Message, args ...any) {} + +func nodeDiagnostic(node int, message *d.Message, args ...any) {} + +func calls(args []any, message *d.Message) { + diagnostic(d.Circularity) + diagnostic(d.Circularity, "a -> b -> a") + diagnostic(d.Plain) + diagnostic(d.Plain, "extra") + diagnostic(d.Multiple, "one") + diagnostic(d.Multiple, "one", "two") + diagnostic(d.Multiple, "one", "two", "three") + diagnostic(d.Sparse, "one", "two") + diagnostic(d.Sparse, "one", "two", "three") + nodeDiagnostic(0, d.Circularity) + nodeDiagnostic(0, d.Circularity, "cycle") + d.Circularity.Localize() + d.Circularity.Localize("cycle") + (*d.Message).Localize(d.Circularity) + (*d.Message).Localize(d.Circularity, "cycle") + diagnostic(d.Circularity, args...) + diagnostic(message) + local := d.Circularity + diagnostic(local) + unrelated := func(value any, args ...any) {} + unrelated(d.Circularity) +} diff --git a/tools/customlint/testdata/diagnosticargs/diagnosticargs.go.golden b/tools/customlint/testdata/diagnosticargs/diagnosticargs.go.golden new file mode 100644 index 0000000000000..ee4c98a79b9b8 --- /dev/null +++ b/tools/customlint/testdata/diagnosticargs/diagnosticargs.go.golden @@ -0,0 +1,48 @@ + package diagnosticargs + + import d "testdata/internal/diagnostics" + + func diagnostic(message *d.Message, args ...any) {} + + func nodeDiagnostic(node int, message *d.Message, args ...any) {} + + func calls(args []any, message *d.Message) { + diagnostic(d.Circularity) + ~~~~~~~~~~~~~~~~~~~~~~~~~ +!!! diagnosticargs: diagnostic Circularity expects 1 formatting arguments, got 0 + diagnostic(d.Circularity, "a -> b -> a") + diagnostic(d.Plain) + diagnostic(d.Plain, "extra") + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +!!! diagnosticargs: diagnostic Plain expects 0 formatting arguments, got 1 + diagnostic(d.Multiple, "one") + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +!!! diagnosticargs: diagnostic Multiple expects 2 formatting arguments, got 1 + diagnostic(d.Multiple, "one", "two") + diagnostic(d.Multiple, "one", "two", "three") + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +!!! diagnosticargs: diagnostic Multiple expects 2 formatting arguments, got 3 + diagnostic(d.Sparse, "one", "two") + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +!!! diagnosticargs: diagnostic Sparse expects 3 formatting arguments, got 2 + diagnostic(d.Sparse, "one", "two", "three") + nodeDiagnostic(0, d.Circularity) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +!!! diagnosticargs: diagnostic Circularity expects 1 formatting arguments, got 0 + nodeDiagnostic(0, d.Circularity, "cycle") + d.Circularity.Localize() + ~~~~~~~~~~~~~~~~~~~~~~~~ +!!! diagnosticargs: diagnostic Circularity expects 1 formatting arguments, got 0 + d.Circularity.Localize("cycle") + (*d.Message).Localize(d.Circularity) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +!!! diagnosticargs: diagnostic Circularity expects 1 formatting arguments, got 0 + (*d.Message).Localize(d.Circularity, "cycle") + diagnostic(d.Circularity, args...) + diagnostic(message) + local := d.Circularity + diagnostic(local) + unrelated := func(value any, args ...any) {} + unrelated(d.Circularity) + } + diff --git a/tools/customlint/testdata/internal/diagnostics/diagnostics.go b/tools/customlint/testdata/internal/diagnostics/diagnostics.go new file mode 100644 index 0000000000000..7200ed0ad1ab4 --- /dev/null +++ b/tools/customlint/testdata/internal/diagnostics/diagnostics.go @@ -0,0 +1,14 @@ +package diagnostics + +type Message struct { + text string +} + +var Circularity = &Message{text: "Circularity detected while resolving configuration: {0}"} +var Plain = &Message{text: "No placeholders"} +var Multiple = &Message{text: "{0} and {1} and {0}"} +var Sparse = &Message{text: "{2}"} + +func (m *Message) Localize(args ...any) string { + return m.text +} diff --git a/tools/customlint/testdata/internal/diagnostics/diagnostics.go.golden b/tools/customlint/testdata/internal/diagnostics/diagnostics.go.golden new file mode 100644 index 0000000000000..2ca7474d20851 --- /dev/null +++ b/tools/customlint/testdata/internal/diagnostics/diagnostics.go.golden @@ -0,0 +1,15 @@ + package diagnostics + + type Message struct { + text string + } + + var Circularity = &Message{text: "Circularity detected while resolving configuration: {0}"} + var Plain = &Message{text: "No placeholders"} + var Multiple = &Message{text: "{0} and {1} and {0}"} + var Sparse = &Message{text: "{2}"} + + func (m *Message) Localize(args ...any) string { + return m.text + } + From e673d2aa4634ac8dbdbedf1ece771657e404f2d3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Nov 2026 16:45:39 +0000 Subject: [PATCH 3/7] Exclude known circularity diagnostic bug from new lint rule Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com> --- .golangci.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.golangci.yml b/.golangci.yml index 72b7797fa8072..9a3efb876c780 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -93,6 +93,11 @@ linters: exclusions: rules: + # Tracked separately in #65091; keep the circularity fix out of the lint rule change. + - path: 'internal/tsoptions/tsconfigparsing\.go$' + linters: + - customlint + text: '^diagnosticargs: diagnostic Circularity_detected_while_resolving_configuration_Colon_0 expects 1 formatting arguments, got 0$' - path: internal/fourslash/tests/ linters: - misspell From c9445868f05f9d13e44f5bc705df768573fd0b26 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Nov 2026 16:50:21 +0000 Subject: [PATCH 4/7] Limit diagnostic lint to missing variadic arguments and standard wrappers Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com> --- .golangci.yml | 7 ++++++- tools/customlint/diagnosticargs.go | 14 ++++++------- .../testdata/diagnosticargs/diagnosticargs.go | 5 +++++ .../diagnosticargs/diagnosticargs.go.golden | 21 ++++++++++--------- 4 files changed, 28 insertions(+), 19 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 9a3efb876c780..ca82589d835f9 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -97,7 +97,12 @@ linters: - path: 'internal/tsoptions/tsconfigparsing\.go$' linters: - customlint - text: '^diagnosticargs: diagnostic Circularity_detected_while_resolving_configuration_Colon_0 expects 1 formatting arguments, got 0$' + text: '^diagnosticargs: diagnostic Circularity_detected_while_resolving_configuration_Colon_0 requires at least 1 formatting arguments, got 0$' + # Existing missing arguments; do not change checker behavior in the lint rule change. + - path: 'internal/checker/checker\.go$' + linters: + - customlint + text: '^diagnosticargs: diagnostic Type_0_does_not_satisfy_the_constraint_1 requires at least 2 formatting arguments, got 0$' - path: internal/fourslash/tests/ linters: - misspell diff --git a/tools/customlint/diagnosticargs.go b/tools/customlint/diagnosticargs.go index 5269295e60d92..cc2e94df9e315 100644 --- a/tools/customlint/diagnosticargs.go +++ b/tools/customlint/diagnosticargs.go @@ -16,7 +16,7 @@ import ( var diagnosticArgsAnalyzer = &analysis.Analyzer{ Name: "diagnosticargs", - Doc: "checks diagnostic message formatting argument counts", + Doc: "checks diagnostic messages have enough formatting arguments", Requires: []*analysis.Analyzer{inspect.Analyzer}, FactTypes: []analysis.Fact{new(diagnosticArgumentCount)}, Run: runDiagnosticArgs, @@ -105,8 +105,8 @@ func runDiagnosticArgs(pass *analysis.Pass) (any, error) { if !ok && !pass.ImportObjectFact(object, &count) { return } - if actual != count.Count { - pass.ReportRangef(call, "diagnostic %s expects %d formatting arguments, got %d", object.Name(), count.Count, actual) + if actual < count.Count { + pass.ReportRangef(call, "diagnostic %s requires at least %d formatting arguments, got %d", object.Name(), count.Count, actual) } } @@ -124,11 +124,9 @@ func runDiagnosticArgs(pass *analysis.Pass) (any, error) { if len(call.Args) < fixed { return } - for i := range fixed { - if isDiagnosticMessage(signature.Params().At(i).Type()) { - check(call, call.Args[i], len(call.Args)-fixed) - return - } + if fixed > 0 && isDiagnosticMessage(signature.Params().At(fixed-1).Type()) { + check(call, call.Args[fixed-1], len(call.Args)-fixed) + return } if selector, ok := call.Fun.(*ast.SelectorExpr); ok { selection := pass.TypesInfo.Selections[selector] diff --git a/tools/customlint/testdata/diagnosticargs/diagnosticargs.go b/tools/customlint/testdata/diagnosticargs/diagnosticargs.go index e2ad018ae2730..ad426d373f76c 100644 --- a/tools/customlint/testdata/diagnosticargs/diagnosticargs.go +++ b/tools/customlint/testdata/diagnosticargs/diagnosticargs.go @@ -6,6 +6,10 @@ func diagnostic(message *d.Message, args ...any) {} func nodeDiagnostic(node int, message *d.Message, args ...any) {} +func optionDiagnostic(message *d.Message, option string, args ...any) { + diagnostic(message, append([]any{option}, args...)...) +} + func calls(args []any, message *d.Message) { diagnostic(d.Circularity) diagnostic(d.Circularity, "a -> b -> a") @@ -18,6 +22,7 @@ func calls(args []any, message *d.Message) { diagnostic(d.Sparse, "one", "two", "three") nodeDiagnostic(0, d.Circularity) nodeDiagnostic(0, d.Circularity, "cycle") + optionDiagnostic(d.Circularity, "cycle") d.Circularity.Localize() d.Circularity.Localize("cycle") (*d.Message).Localize(d.Circularity) diff --git a/tools/customlint/testdata/diagnosticargs/diagnosticargs.go.golden b/tools/customlint/testdata/diagnosticargs/diagnosticargs.go.golden index ee4c98a79b9b8..b53cfe57f4ffe 100644 --- a/tools/customlint/testdata/diagnosticargs/diagnosticargs.go.golden +++ b/tools/customlint/testdata/diagnosticargs/diagnosticargs.go.golden @@ -6,37 +6,38 @@ func nodeDiagnostic(node int, message *d.Message, args ...any) {} + func optionDiagnostic(message *d.Message, option string, args ...any) { + diagnostic(message, append([]any{option}, args...)...) + } + func calls(args []any, message *d.Message) { diagnostic(d.Circularity) ~~~~~~~~~~~~~~~~~~~~~~~~~ -!!! diagnosticargs: diagnostic Circularity expects 1 formatting arguments, got 0 +!!! diagnosticargs: diagnostic Circularity requires at least 1 formatting arguments, got 0 diagnostic(d.Circularity, "a -> b -> a") diagnostic(d.Plain) diagnostic(d.Plain, "extra") - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -!!! diagnosticargs: diagnostic Plain expects 0 formatting arguments, got 1 diagnostic(d.Multiple, "one") ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -!!! diagnosticargs: diagnostic Multiple expects 2 formatting arguments, got 1 +!!! diagnosticargs: diagnostic Multiple requires at least 2 formatting arguments, got 1 diagnostic(d.Multiple, "one", "two") diagnostic(d.Multiple, "one", "two", "three") - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -!!! diagnosticargs: diagnostic Multiple expects 2 formatting arguments, got 3 diagnostic(d.Sparse, "one", "two") ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -!!! diagnosticargs: diagnostic Sparse expects 3 formatting arguments, got 2 +!!! diagnosticargs: diagnostic Sparse requires at least 3 formatting arguments, got 2 diagnostic(d.Sparse, "one", "two", "three") nodeDiagnostic(0, d.Circularity) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -!!! diagnosticargs: diagnostic Circularity expects 1 formatting arguments, got 0 +!!! diagnosticargs: diagnostic Circularity requires at least 1 formatting arguments, got 0 nodeDiagnostic(0, d.Circularity, "cycle") + optionDiagnostic(d.Circularity, "cycle") d.Circularity.Localize() ~~~~~~~~~~~~~~~~~~~~~~~~ -!!! diagnosticargs: diagnostic Circularity expects 1 formatting arguments, got 0 +!!! diagnosticargs: diagnostic Circularity requires at least 1 formatting arguments, got 0 d.Circularity.Localize("cycle") (*d.Message).Localize(d.Circularity) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -!!! diagnosticargs: diagnostic Circularity expects 1 formatting arguments, got 0 +!!! diagnosticargs: diagnostic Circularity requires at least 1 formatting arguments, got 0 (*d.Message).Localize(d.Circularity, "cycle") diagnostic(d.Circularity, args...) diagnostic(message) From fba6668da07a707b7dacc2050f9985b9e6ee8588 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Mon, 2 Nov 2026 10:48:12 -0800 Subject: [PATCH 5/7] Remove exclusion so we can see failure --- .golangci.yml | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index ca82589d835f9..72b7797fa8072 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -93,16 +93,6 @@ linters: exclusions: rules: - # Tracked separately in #65091; keep the circularity fix out of the lint rule change. - - path: 'internal/tsoptions/tsconfigparsing\.go$' - linters: - - customlint - text: '^diagnosticargs: diagnostic Circularity_detected_while_resolving_configuration_Colon_0 requires at least 1 formatting arguments, got 0$' - # Existing missing arguments; do not change checker behavior in the lint rule change. - - path: 'internal/checker/checker\.go$' - linters: - - customlint - text: '^diagnosticargs: diagnostic Type_0_does_not_satisfy_the_constraint_1 requires at least 2 formatting arguments, got 0$' - path: internal/fourslash/tests/ linters: - misspell From e9f7e8c61a84c095f6542a00a807fc94738f57ee Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Nov 2026 19:06:48 +0000 Subject: [PATCH 6/7] Supply type and constraint arguments in checker diagnostic chain Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com> --- tsc/internal/checker/checker.go | 5 +++-- tsc/internal/checker/checker_test.go | 27 +++++++++++++++++++++++++++ tsc/internal/checker/export_test.go | 11 +++++++++++ 3 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 tsc/internal/checker/export_test.go diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index 4d3e417842f77..016bd3b4fb0e1 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -9470,11 +9470,12 @@ func (c *Checker) checkTypeArguments(signature *Signature, typeArgumentNodes []* errorNode = typeArgumentNodes[i] } var diags []*ast.Diagnostic - if !c.checkTypeAssignableToEx(typeArgument, c.getTypeWithThisArgument(c.instantiateType(constraint, mapper), typeArgument, false), errorNode, typeArgumentHeadMessage, &diags) { + instantiatedConstraint := c.getTypeWithThisArgument(c.instantiateType(constraint, mapper), typeArgument, false) + if !c.checkTypeAssignableToEx(typeArgument, instantiatedConstraint, errorNode, typeArgumentHeadMessage, &diags) { if len(diags) != 0 { diagnostic := diags[0] if headMessage != nil { - diagnostic = ast.NewDiagnosticChain(diagnostic, diagnostics.Type_0_does_not_satisfy_the_constraint_1) + diagnostic = ast.NewDiagnosticChain(diagnostic, diagnostics.Type_0_does_not_satisfy_the_constraint_1, c.typeToString(typeArgument, nil), c.typeToString(instantiatedConstraint, nil)) } c.addDiagnostic(diagnostic) } diff --git a/tsc/internal/checker/checker_test.go b/tsc/internal/checker/checker_test.go index 42ae368d35924..dab2e6dd936db 100644 --- a/tsc/internal/checker/checker_test.go +++ b/tsc/internal/checker/checker_test.go @@ -11,6 +11,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/checker" "github.com/microsoft/TypeScript/tsc/internal/compiler" "github.com/microsoft/TypeScript/tsc/internal/core" + "github.com/microsoft/TypeScript/tsc/internal/diagnostics" "github.com/microsoft/TypeScript/tsc/internal/repo" "github.com/microsoft/TypeScript/tsc/internal/tsoptions" "github.com/microsoft/TypeScript/tsc/internal/tspath" @@ -62,6 +63,32 @@ foo.bar;` } } +func TestTypeArgumentConstraintDiagnosticWithHeadMessage(t *testing.T) { + t.Parallel() + + fs := bundled.WrapFS(vfstest.FromMap(map[string]string{ + "/foo.ts": `declare function f(): void; +f();`, + "/tsconfig.json": `{"files":["foo.ts"]}`, + }, tspath.CaseInsensitive)) + host := compiler.NewCompilerHost(fs, bundled.LibPath(), nil, nil, nil) + parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile("/tsconfig.json", &core.CompilerOptions{}, nil, tsoptions.NewDefaultParseConfigHost(fs), nil) + assert.Equal(t, len(errors), 0) + p := compiler.NewProgram(compiler.ProgramOptions{Config: parsed, Host: host}) + c, done := p.GetTypeChecker(t.Context()) + defer done() + file := p.GetSourceFile("/foo.ts") + call := file.Statements.Nodes[1].Expression() + signature := c.GetSignaturesOfType(c.GetTypeAtLocation(call.Expression()), checker.SignatureKindCall)[0] + diags := c.CheckTypeArgumentsForTest(signature, call.TypeArguments(), diagnostics.Unable_to_resolve_signature_of_class_decorator_when_called_as_an_expression) + assert.Equal(t, len(diags), 1) + assert.Equal(t, diags[0].Code(), diagnostics.Type_0_does_not_satisfy_the_constraint_1.Code()) + assert.DeepEqual(t, diags[0].MessageArgs(), []string{"number", "string"}) + assert.Assert(t, strings.Contains(diags[0].String(), "Type 'number' does not satisfy the constraint 'string'.")) + assert.Equal(t, len(diags[0].MessageChain()), 1) + assert.Equal(t, diags[0].MessageChain()[0].Code(), diagnostics.Unable_to_resolve_signature_of_class_decorator_when_called_as_an_expression.Code()) +} + func TestGetTypeAtLocationOfTypeOnlyImportClause(t *testing.T) { t.Parallel() diff --git a/tsc/internal/checker/export_test.go b/tsc/internal/checker/export_test.go new file mode 100644 index 0000000000000..3ffad49b60696 --- /dev/null +++ b/tsc/internal/checker/export_test.go @@ -0,0 +1,11 @@ +package checker + +import ( + "github.com/microsoft/TypeScript/tsc/internal/ast" + "github.com/microsoft/TypeScript/tsc/internal/diagnostics" +) + +func (c *Checker) CheckTypeArgumentsForTest(signature *Signature, typeArgumentNodes []*ast.Node, headMessage *diagnostics.Message) []*ast.Diagnostic { + c.checkTypeArguments(signature, typeArgumentNodes, true, headMessage) + return c.diagnostics.GetDiagnostics() +} From 91eb495f4d3c7359593fe0e4f19f0a69bd0d639c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Nov 2026 19:47:24 +0000 Subject: [PATCH 7/7] Replace checker unit test with compiler diagnostic baselines Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com> --- tsc/internal/checker/checker_test.go | 27 ----------------- tsc/internal/checker/export_test.go | 11 ------- ...ypeArgumentConstraintDiagnostic.errors.txt | 25 ++++++++++++++++ .../typeArgumentConstraintDiagnostic.symbols | 30 +++++++++++++++++++ .../typeArgumentConstraintDiagnostic.types | 26 ++++++++++++++++ .../typeArgumentConstraintDiagnostic.ts | 11 +++++++ 6 files changed, 92 insertions(+), 38 deletions(-) delete mode 100644 tsc/internal/checker/export_test.go create mode 100644 tsc/testdata/baselines/reference/compiler/typeArgumentConstraintDiagnostic.errors.txt create mode 100644 tsc/testdata/baselines/reference/compiler/typeArgumentConstraintDiagnostic.symbols create mode 100644 tsc/testdata/baselines/reference/compiler/typeArgumentConstraintDiagnostic.types create mode 100644 tsc/testdata/tests/cases/compiler/typeArgumentConstraintDiagnostic.ts diff --git a/tsc/internal/checker/checker_test.go b/tsc/internal/checker/checker_test.go index dab2e6dd936db..42ae368d35924 100644 --- a/tsc/internal/checker/checker_test.go +++ b/tsc/internal/checker/checker_test.go @@ -11,7 +11,6 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/checker" "github.com/microsoft/TypeScript/tsc/internal/compiler" "github.com/microsoft/TypeScript/tsc/internal/core" - "github.com/microsoft/TypeScript/tsc/internal/diagnostics" "github.com/microsoft/TypeScript/tsc/internal/repo" "github.com/microsoft/TypeScript/tsc/internal/tsoptions" "github.com/microsoft/TypeScript/tsc/internal/tspath" @@ -63,32 +62,6 @@ foo.bar;` } } -func TestTypeArgumentConstraintDiagnosticWithHeadMessage(t *testing.T) { - t.Parallel() - - fs := bundled.WrapFS(vfstest.FromMap(map[string]string{ - "/foo.ts": `declare function f(): void; -f();`, - "/tsconfig.json": `{"files":["foo.ts"]}`, - }, tspath.CaseInsensitive)) - host := compiler.NewCompilerHost(fs, bundled.LibPath(), nil, nil, nil) - parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile("/tsconfig.json", &core.CompilerOptions{}, nil, tsoptions.NewDefaultParseConfigHost(fs), nil) - assert.Equal(t, len(errors), 0) - p := compiler.NewProgram(compiler.ProgramOptions{Config: parsed, Host: host}) - c, done := p.GetTypeChecker(t.Context()) - defer done() - file := p.GetSourceFile("/foo.ts") - call := file.Statements.Nodes[1].Expression() - signature := c.GetSignaturesOfType(c.GetTypeAtLocation(call.Expression()), checker.SignatureKindCall)[0] - diags := c.CheckTypeArgumentsForTest(signature, call.TypeArguments(), diagnostics.Unable_to_resolve_signature_of_class_decorator_when_called_as_an_expression) - assert.Equal(t, len(diags), 1) - assert.Equal(t, diags[0].Code(), diagnostics.Type_0_does_not_satisfy_the_constraint_1.Code()) - assert.DeepEqual(t, diags[0].MessageArgs(), []string{"number", "string"}) - assert.Assert(t, strings.Contains(diags[0].String(), "Type 'number' does not satisfy the constraint 'string'.")) - assert.Equal(t, len(diags[0].MessageChain()), 1) - assert.Equal(t, diags[0].MessageChain()[0].Code(), diagnostics.Unable_to_resolve_signature_of_class_decorator_when_called_as_an_expression.Code()) -} - func TestGetTypeAtLocationOfTypeOnlyImportClause(t *testing.T) { t.Parallel() diff --git a/tsc/internal/checker/export_test.go b/tsc/internal/checker/export_test.go deleted file mode 100644 index 3ffad49b60696..0000000000000 --- a/tsc/internal/checker/export_test.go +++ /dev/null @@ -1,11 +0,0 @@ -package checker - -import ( - "github.com/microsoft/TypeScript/tsc/internal/ast" - "github.com/microsoft/TypeScript/tsc/internal/diagnostics" -) - -func (c *Checker) CheckTypeArgumentsForTest(signature *Signature, typeArgumentNodes []*ast.Node, headMessage *diagnostics.Message) []*ast.Diagnostic { - c.checkTypeArguments(signature, typeArgumentNodes, true, headMessage) - return c.diagnostics.GetDiagnostics() -} diff --git a/tsc/testdata/baselines/reference/compiler/typeArgumentConstraintDiagnostic.errors.txt b/tsc/testdata/baselines/reference/compiler/typeArgumentConstraintDiagnostic.errors.txt new file mode 100644 index 0000000000000..7b9165f06bdfb --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/typeArgumentConstraintDiagnostic.errors.txt @@ -0,0 +1,25 @@ +typeArgumentConstraintDiagnostic.ts(2,13): error TS2344: Type 'number' does not satisfy the constraint 'string'. +typeArgumentConstraintDiagnostic.ts(5,11): error TS2344: Type 'number' does not satisfy the constraint 'string'. +typeArgumentConstraintDiagnostic.ts(8,17): error TS2344: Type '{ value: number; }' does not satisfy the constraint '{ value: string; }'. + Types of property 'value' are incompatible. + Type 'number' is not assignable to type 'string'. + + +==== typeArgumentConstraintDiagnostic.ts (3 errors) ==== + declare function constrained(): void; + constrained(); + ~~~~~~ +!!! error TS2344: Type 'number' does not satisfy the constraint 'string'. + + declare function dependent(): void; + dependent(); + ~~~~~~ +!!! error TS2344: Type 'number' does not satisfy the constraint 'string'. + + declare function dependentObject(): void; + dependentObject<{ value: number }, { value: string }>(); + ~~~~~~~~~~~~~~~~~ +!!! error TS2344: Type '{ value: number; }' does not satisfy the constraint '{ value: string; }'. +!!! error TS2344: Types of property 'value' are incompatible. +!!! error TS2344: Type 'number' is not assignable to type 'string'. + \ No newline at end of file diff --git a/tsc/testdata/baselines/reference/compiler/typeArgumentConstraintDiagnostic.symbols b/tsc/testdata/baselines/reference/compiler/typeArgumentConstraintDiagnostic.symbols new file mode 100644 index 0000000000000..da43243b4dd55 --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/typeArgumentConstraintDiagnostic.symbols @@ -0,0 +1,30 @@ +//// [tests/cases/compiler/typeArgumentConstraintDiagnostic.ts] //// + +=== typeArgumentConstraintDiagnostic.ts === +declare function constrained(): void; +>constrained : Symbol(constrained, Decl(typeArgumentConstraintDiagnostic.ts, 0, 0)) +>T : Symbol(T, Decl(typeArgumentConstraintDiagnostic.ts, 0, 29)) + +constrained(); +>constrained : Symbol(constrained, Decl(typeArgumentConstraintDiagnostic.ts, 0, 0)) + +declare function dependent(): void; +>dependent : Symbol(dependent, Decl(typeArgumentConstraintDiagnostic.ts, 1, 22)) +>T : Symbol(T, Decl(typeArgumentConstraintDiagnostic.ts, 3, 27)) +>U : Symbol(U, Decl(typeArgumentConstraintDiagnostic.ts, 3, 39)) +>U : Symbol(U, Decl(typeArgumentConstraintDiagnostic.ts, 3, 39)) + +dependent(); +>dependent : Symbol(dependent, Decl(typeArgumentConstraintDiagnostic.ts, 1, 22)) + +declare function dependentObject(): void; +>dependentObject : Symbol(dependentObject, Decl(typeArgumentConstraintDiagnostic.ts, 4, 28)) +>T : Symbol(T, Decl(typeArgumentConstraintDiagnostic.ts, 6, 33)) +>U : Symbol(U, Decl(typeArgumentConstraintDiagnostic.ts, 6, 45)) +>U : Symbol(U, Decl(typeArgumentConstraintDiagnostic.ts, 6, 45)) + +dependentObject<{ value: number }, { value: string }>(); +>dependentObject : Symbol(dependentObject, Decl(typeArgumentConstraintDiagnostic.ts, 4, 28)) +>value : Symbol(value, Decl(typeArgumentConstraintDiagnostic.ts, 7, 17)) +>value : Symbol(value, Decl(typeArgumentConstraintDiagnostic.ts, 7, 36)) + diff --git a/tsc/testdata/baselines/reference/compiler/typeArgumentConstraintDiagnostic.types b/tsc/testdata/baselines/reference/compiler/typeArgumentConstraintDiagnostic.types new file mode 100644 index 0000000000000..ecb87cb5fd7b4 --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/typeArgumentConstraintDiagnostic.types @@ -0,0 +1,26 @@ +//// [tests/cases/compiler/typeArgumentConstraintDiagnostic.ts] //// + +=== typeArgumentConstraintDiagnostic.ts === +declare function constrained(): void; +>constrained : () => void + +constrained(); +>constrained() : void +>constrained : () => void + +declare function dependent(): void; +>dependent : () => void + +dependent(); +>dependent() : void +>dependent : () => void + +declare function dependentObject(): void; +>dependentObject : () => void + +dependentObject<{ value: number }, { value: string }>(); +>dependentObject<{ value: number }, { value: string }>() : void +>dependentObject : () => void +>value : number +>value : string + diff --git a/tsc/testdata/tests/cases/compiler/typeArgumentConstraintDiagnostic.ts b/tsc/testdata/tests/cases/compiler/typeArgumentConstraintDiagnostic.ts new file mode 100644 index 0000000000000..fcb2cb3044ce6 --- /dev/null +++ b/tsc/testdata/tests/cases/compiler/typeArgumentConstraintDiagnostic.ts @@ -0,0 +1,11 @@ +// @strict: true +// @noEmit: true + +declare function constrained(): void; +constrained(); + +declare function dependent(): void; +dependent(); + +declare function dependentObject(): void; +dependentObject<{ value: number }, { value: string }>();