diff --git a/internal/ast/diagnostic.go b/internal/ast/diagnostic.go index 8eadefd4b20..9e454902b46 100644 --- a/internal/ast/diagnostic.go +++ b/internal/ast/diagnostic.go @@ -163,29 +163,69 @@ func NewCompilerDiagnostic(message *diagnostics.Message, args ...any) *Diagnosti type DiagnosticsCollection struct { mu sync.Mutex count int - fileDiagnostics map[string][]*Diagnostic - fileDiagnosticsSorted collections.Set[string] + fileDiagnostics map[*SourceFile][]*Diagnostic + fileDiagnosticsSorted collections.Set[*SourceFile] nonFileDiagnostics []*Diagnostic nonFileDiagnosticsSorted bool + diagnosticIndex map[diagnosticLocationKey]*Diagnostic + diagnosticCollisions map[diagnosticLocationKey][]*Diagnostic } -func (c *DiagnosticsCollection) Add(diagnostic *Diagnostic) { +func (c *DiagnosticsCollection) Add(diagnostic *Diagnostic) *Diagnostic { c.mu.Lock() defer c.mu.Unlock() + key := getDiagnosticLocationKey(diagnostic) + if existing := c.diagnosticIndex[key]; existing != nil { + if EqualDiagnostics(existing, diagnostic) { + return existing + } + for _, collision := range c.diagnosticCollisions[key] { + if EqualDiagnostics(collision, diagnostic) { + return collision + } + } + } + if c.diagnosticIndex == nil { + c.diagnosticIndex = make(map[diagnosticLocationKey]*Diagnostic) + } + if c.diagnosticIndex[key] == nil { + c.diagnosticIndex[key] = diagnostic + } else { + if c.diagnosticCollisions == nil { + c.diagnosticCollisions = make(map[diagnosticLocationKey][]*Diagnostic) + } + c.diagnosticCollisions[key] = append(c.diagnosticCollisions[key], diagnostic) + } + c.count++ if diagnostic.File() != nil { - fileName := diagnostic.File().FileName() + file := diagnostic.File() if c.fileDiagnostics == nil { - c.fileDiagnostics = make(map[string][]*Diagnostic) + c.fileDiagnostics = make(map[*SourceFile][]*Diagnostic) } - c.fileDiagnostics[fileName] = append(c.fileDiagnostics[fileName], diagnostic) - c.fileDiagnosticsSorted.Delete(fileName) + c.fileDiagnostics[file] = append(c.fileDiagnostics[file], diagnostic) + c.fileDiagnosticsSorted.Delete(file) } else { c.nonFileDiagnostics = append(c.nonFileDiagnostics, diagnostic) c.nonFileDiagnosticsSorted = false } + return diagnostic +} + +type diagnosticLocationKey struct { + file *SourceFile + loc core.TextRange + code int32 +} + +func getDiagnosticLocationKey(diagnostic *Diagnostic) diagnosticLocationKey { + return diagnosticLocationKey{ + file: diagnostic.File(), + loc: diagnostic.Loc(), + code: diagnostic.Code(), + } } func (c *DiagnosticsCollection) Lookup(diagnostic *Diagnostic) *Diagnostic { @@ -194,7 +234,7 @@ func (c *DiagnosticsCollection) Lookup(diagnostic *Diagnostic) *Diagnostic { var diagnostics []*Diagnostic if diagnostic.File() != nil { - diagnostics = c.getDiagnosticsForFileLocked(diagnostic.File().FileName()) + diagnostics = c.getDiagnosticsForFileLocked(diagnostic.File()) } else { diagnostics = c.getGlobalDiagnosticsLocked() } @@ -219,19 +259,19 @@ func (c *DiagnosticsCollection) getGlobalDiagnosticsLocked() []*Diagnostic { return slices.Clone(c.nonFileDiagnostics) } -func (c *DiagnosticsCollection) GetDiagnosticsForFile(fileName string) []*Diagnostic { +func (c *DiagnosticsCollection) GetDiagnosticsForFile(file *SourceFile) []*Diagnostic { c.mu.Lock() defer c.mu.Unlock() - return c.getDiagnosticsForFileLocked(fileName) + return c.getDiagnosticsForFileLocked(file) } -func (c *DiagnosticsCollection) getDiagnosticsForFileLocked(fileName string) []*Diagnostic { - if !c.fileDiagnosticsSorted.Has(fileName) { - slices.SortStableFunc(c.fileDiagnostics[fileName], CompareDiagnostics) - c.fileDiagnosticsSorted.Add(fileName) +func (c *DiagnosticsCollection) getDiagnosticsForFileLocked(file *SourceFile) []*Diagnostic { + if !c.fileDiagnosticsSorted.Has(file) { + slices.SortStableFunc(c.fileDiagnostics[file], CompareDiagnostics) + c.fileDiagnosticsSorted.Add(file) } - return slices.Clone(c.fileDiagnostics[fileName]) + return slices.Clone(c.fileDiagnostics[file]) } func (c *DiagnosticsCollection) GetDiagnostics() []*Diagnostic { @@ -269,10 +309,18 @@ func EqualDiagnosticsNoRelatedInfo(d1, d2 *Diagnostic) bool { return getDiagnosticPath(d1) == getDiagnosticPath(d2) && d1.Loc() == d2.Loc() && d1.Code() == d2.Code() && + getDiagnosticMessageIdentity(d1) == getDiagnosticMessageIdentity(d2) && slices.Equal(d1.MessageArgs(), d2.MessageArgs()) && slices.EqualFunc(d1.MessageChain(), d2.MessageChain(), equalMessageChain) } +func getDiagnosticMessageIdentity(diagnostic *Diagnostic) string { + if diagnostic.message != nil && diagnostic.Code() == -1 { + return diagnostic.message.String() + } + return string(diagnostic.MessageKey()) +} + func equalMessageChain(c1, c2 *Diagnostic) bool { if c1 == c2 { return true @@ -346,6 +394,10 @@ func CompareDiagnostics(d1, d2 *Diagnostic) int { if c != 0 { return c } + c = strings.Compare(getDiagnosticMessageIdentity(d1), getDiagnosticMessageIdentity(d2)) + if c != 0 { + return c + } c = slices.Compare(d1.MessageArgs(), d2.MessageArgs()) if c != 0 { return c diff --git a/internal/ast/diagnostic_test.go b/internal/ast/diagnostic_test.go new file mode 100644 index 00000000000..35c6cfe663d --- /dev/null +++ b/internal/ast/diagnostic_test.go @@ -0,0 +1,54 @@ +package ast + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/diagnostics" +) + +func TestDiagnosticsCollectionDeduplicatesExactDiagnosticsOnAdd(t *testing.T) { + t.Parallel() + + var collection DiagnosticsCollection + first := NewCompilerDiagnostic(diagnostics.Cannot_find_name_0, "x"). + AddRelatedInfo(NewCompilerDiagnostic(diagnostics.X_0_is_declared_here, "first")) + second := NewCompilerDiagnostic(diagnostics.Cannot_find_name_0, "x"). + AddRelatedInfo(NewCompilerDiagnostic(diagnostics.X_0_is_declared_here, "first")) + different := NewCompilerDiagnostic(diagnostics.Cannot_find_name_0, "x"). + AddRelatedInfo(NewCompilerDiagnostic(diagnostics.X_0_is_declared_here, "second")) + + if got := collection.Add(first); got != first { + t.Fatalf("first Add() returned %p, want %p", got, first) + } + canonical := collection.Add(second) + if canonical != first { + t.Fatalf("second Add() returned %p, want canonical %p", canonical, first) + } + if got := collection.Add(different); got != different { + t.Fatalf("different Add() returned %p, want %p", got, different) + } + + canonical.AddRelatedInfo(NewCompilerDiagnostic(diagnostics.X_0_is_declared_here, "third")) + collected := collection.GetGlobalDiagnostics() + if len(collected) != 2 { + t.Fatalf("GetGlobalDiagnostics() returned %d diagnostics, want 2", len(collected)) + } + if got := len(first.RelatedInformation()); got != 2 { + t.Fatalf("canonical diagnostic has %d related diagnostics, want 2", got) + } +} + +func TestDiagnosticsCollectionPreservesDistinctAdHocMessages(t *testing.T) { + t.Parallel() + + var collection DiagnosticsCollection + first := NewCompilerDiagnostic(diagnostics.NewAdHocMessage("first")) + second := NewCompilerDiagnostic(diagnostics.NewAdHocMessage("second")) + + collection.Add(first) + collection.Add(second) + collected := collection.GetGlobalDiagnostics() + if len(collected) != 2 { + t.Fatalf("GetGlobalDiagnostics() returned %d diagnostics, want 2", len(collected)) + } +} diff --git a/internal/checker/checker.go b/internal/checker/checker.go index 6037261cf7c..def9459042f 100644 --- a/internal/checker/checker.go +++ b/internal/checker/checker.go @@ -2497,7 +2497,7 @@ func (c *Checker) checkDeferredNodes(context *ast.SourceFile) { } c.checkDeferredNode(node) } - links.deferredNodes.Clear() + links.deferredNodes = collections.OrderedSet[*ast.Node]{} } func (c *Checker) checkDeferredNode(node *ast.Node) { @@ -2532,6 +2532,8 @@ func (c *Checker) checkDeferredNode(node *ast.Node) { if ast.IsInstanceOfExpression(node) { c.resolveUntypedCall(node) } + case ast.KindObjectLiteralExpression, ast.KindJsxAttributes: + c.checkContextualDeprecations(node) } c.currentNode = saveCurrentNode } @@ -8762,7 +8764,7 @@ func (c *Checker) resolveDecorator(node *ast.Node, candidatesOutArray *[]*Signat headMessage := c.getDiagnosticHeadMessageForDecoratorResolution(node) if len(callSignatures) == 0 { diag := ast.NewDiagnosticChain(c.invocationErrorDetails(node.Expression(), apparentType, SignatureKindCall), headMessage) - c.addDiagnostic(diag) + diag = c.addDiagnostic(diag) c.invocationErrorRecovery(apparentType, SignatureKindCall, diag) return c.resolveErrorCall(node) } @@ -9998,7 +10000,7 @@ func (c *Checker) invocationError(errorTarget *ast.Node, apparentType *Type, kin if relatedInformation != nil { diagnostic.AddRelatedInfo(relatedInformation) } - c.addDiagnostic(diagnostic) + diagnostic = c.addDiagnostic(diagnostic) c.invocationErrorRecovery(apparentType, kind, diagnostic) } @@ -13152,6 +13154,7 @@ func (c *Checker) checkObjectLiteral(node *ast.Node, checkMode CheckMode) *Type // is nothing to check here. return result //nolint:customlint // expando object literal has no property children to check } + c.checkNodeDeferred(node) inDestructuringPattern := ast.IsAssignmentTarget(node) // Grammar checking c.checkGrammarObjectLiteralExpression(node.AsObjectLiteralExpression(), inDestructuringPattern) @@ -13268,9 +13271,6 @@ func (c *Checker) checkObjectLiteral(node *ast.Node, checkMode CheckMode) *Type if allPropertiesTable != nil { allPropertiesTable[prop.Name] = prop } - if ast.IsIdentifier(memberDecl.Name()) { - c.checkDeprecatedProperty(memberDecl.Name(), contextualType) - } if contextualType != nil && checkMode&CheckModeInferential != 0 && checkMode&CheckModeSkipContextSensitive == 0 && (ast.IsPropertyAssignment(memberDecl) || ast.IsMethodDeclaration(memberDecl)) && c.isContextSensitive(memberDecl) { inferenceContext := c.getInferenceContext(node) // In CheckMode.Inferential we should always have an inference context @@ -13355,8 +13355,20 @@ func (c *Checker) checkObjectLiteral(node *ast.Node, checkMode CheckMode) *Type return createObjectLiteralType() } -func (c *Checker) checkDeprecatedProperty(name *ast.IdentifierNode, contextualType *Type) { - if contextualType == nil || name == nil { +func (c *Checker) checkContextualDeprecations(node *ast.Node) { + contextualType := c.getApparentTypeOfContextualType(node, ContextFlagsNone) + for _, property := range node.Properties() { + if c.isCanceled() { + return + } + if property.Name() != nil && !ast.IsComputedPropertyName(property.Name()) { + c.checkDeprecatedProperty(property.Name(), contextualType) + } + } +} + +func (c *Checker) checkDeprecatedProperty(name *ast.Node, contextualType *Type) { + if contextualType == nil { return } prop := c.getPropertyOfType(contextualType, name.Text()) @@ -13963,7 +13975,7 @@ func (c *Checker) getDiagnostics(ctx context.Context, sourceFile *ast.SourceFile if c.wasCanceled { return nil } - return collection.GetDiagnosticsForFile(sourceFile.FileName()) + return collection.GetDiagnosticsForFile(sourceFile) } func (c *Checker) GetGlobalDiagnostics() []*ast.Diagnostic { @@ -13982,24 +13994,24 @@ func (c *Checker) produceDeferredDiagnostics() { c.deferredDiagnosticCallbacks = nil } -func (c *Checker) addDiagnostic(diagnostic *ast.Diagnostic) { +func (c *Checker) addDiagnostic(diagnostic *ast.Diagnostic) *ast.Diagnostic { // Discard diagnostics created while at the maximum number of recursive TypeToString invocations. if c.serializationLevel < maxSerializationLevel { - c.diagnostics.Add(diagnostic) + return c.diagnostics.Add(diagnostic) } + return diagnostic } -func (c *Checker) addSuggestionDiagnostic(diagnostic *ast.Diagnostic) { +func (c *Checker) addSuggestionDiagnostic(diagnostic *ast.Diagnostic) *ast.Diagnostic { // Discard diagnostics created while at the maximum number of recursive TypeToString invocations. if c.serializationLevel < maxSerializationLevel { - c.suggestionDiagnostics.Add(diagnostic) + return c.suggestionDiagnostics.Add(diagnostic) } + return diagnostic } func (c *Checker) error(location *ast.Node, message *diagnostics.Message, args ...any) *ast.Diagnostic { - diagnostic := NewDiagnosticForNode(location, message, args...) - c.addDiagnostic(diagnostic) - return diagnostic + return c.addDiagnostic(NewDiagnosticForNode(location, message, args...)) } func (c *Checker) errorSkippedOnNoEmit(location *ast.Node, message *diagnostics.Message, args ...any) *ast.Diagnostic { @@ -14047,8 +14059,7 @@ func (c *Checker) addDeprecatedSuggestionWorker(declarations []*ast.Node, diagno break } } - c.addSuggestionDiagnostic(diagnostic) - return diagnostic + return c.addSuggestionDiagnostic(diagnostic) } func (c *Checker) isDeprecatedSymbol(symbol *ast.Symbol) bool { @@ -14268,13 +14279,7 @@ func getAdjustedNodeForError(node *ast.Node) *ast.Node { } func (c *Checker) lookupOrIssueError(location *ast.Node, message *diagnostics.Message, args ...any) *ast.Diagnostic { - diagnostic := NewDiagnosticForNode(location, message, args...) - existing := c.diagnostics.Lookup(diagnostic) - if existing != nil { - return existing - } - c.addDiagnostic(diagnostic) - return diagnostic + return c.addDiagnostic(NewDiagnosticForNode(location, message, args...)) } func getFirstDeclaration(symbol *ast.Symbol) *ast.Node { diff --git a/internal/checker/grammarchecks.go b/internal/checker/grammarchecks.go index afd95d72ecb..8ebf5826a3e 100644 --- a/internal/checker/grammarchecks.go +++ b/internal/checker/grammarchecks.go @@ -82,7 +82,7 @@ func (c *Checker) checkGrammarRegularExpressionLiteral(node *ast.RegularExpressi lastError.AddRelatedInfo(err) } else if lastError == nil || start != lastError.Pos() { lastError = ast.NewDiagnostic(sourceFile, core.NewTextRange(start, start+length), message, args...) - c.addDiagnostic(lastError) + lastError = c.addDiagnostic(lastError) } }) c.regExpScanner.SetText(sourceFile.Text()) diff --git a/internal/checker/jsx.go b/internal/checker/jsx.go index aa2f98c6136..a0c458bd7cf 100644 --- a/internal/checker/jsx.go +++ b/internal/checker/jsx.go @@ -124,6 +124,7 @@ func (c *Checker) checkJsxFragment(node *ast.Node) *Type { } func (c *Checker) checkJsxAttributes(node *ast.Node, checkMode CheckMode) *Type { + c.checkNodeDeferred(node) return c.createJsxAttributesTypeFromAttributesProperty(node.Parent, checkMode) } @@ -756,9 +757,6 @@ func (c *Checker) createJsxAttributesTypeFromAttributesProperty(openingLikeEleme if attributeDecl.Name().Text() == jsxChildrenPropertyName { explicitlySpecifyChildrenAttribute = true } - if ast.IsIdentifier(attributeDecl.Name()) { - c.checkDeprecatedProperty(attributeDecl.Name(), contextualType) - } if contextualType != nil && checkMode&CheckModeInferential != 0 && checkMode&CheckModeSkipContextSensitive == 0 && c.isContextSensitive(attributeDecl) { inferenceContext := c.getInferenceContext(attributes) debug.Assert(inferenceContext != nil) diff --git a/internal/compiler/program.go b/internal/compiler/program.go index de4d515f202..2f25848c723 100644 --- a/internal/compiler/program.go +++ b/internal/compiler/program.go @@ -706,7 +706,7 @@ func (p *Program) GetIncludeProcessorDiagnostics(sourceFile *ast.SourceFile) []* if p.SkipTypeChecking(sourceFile, false) { return nil } - filtered, _ := p.getDiagnosticsWithPrecedingDirectives(sourceFile, p.includeProcessor.getDiagnostics(p).GetDiagnosticsForFile(sourceFile.FileName())) + filtered, _ := p.getDiagnosticsWithPrecedingDirectives(sourceFile, p.includeProcessor.getDiagnostics(p).GetDiagnosticsForFile(sourceFile)) return filtered } diff --git a/internal/fourslash/tests/deprecatedContextualPropertyOverload_test.go b/internal/fourslash/tests/deprecatedContextualPropertyOverload_test.go new file mode 100644 index 00000000000..63d6a08285b --- /dev/null +++ b/internal/fourslash/tests/deprecatedContextualPropertyOverload_test.go @@ -0,0 +1,129 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/lsp/lsproto" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestDeprecatedContextualPropertyOverload(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + + const content = `interface DeprecatedOptions { + kind: "deprecated"; + /** @deprecated */ + value: number; +} +interface CurrentOptions { + kind: "current"; + value: number; +} +declare function select(options: DeprecatedOptions): void; +declare function select(options: CurrentOptions): void; + +select({ kind: "current", value: 1 }); + +/** @deprecated */ +declare const deprecatedValue: number; +select({ kind: "current", value: [|deprecatedValue|] }); + +interface DeprecatedContainer { + /** @deprecated */ + value: number; +} +declare const deprecatedContainer: DeprecatedContainer; +select({ kind: "current", value: deprecatedContainer.[|value|] }); + +/** @deprecated */ +declare function deprecatedCall(): number; +select({ kind: "current", value: [|deprecatedCall|]() }); + +interface DeprecatedAccessorOptions { + /** @deprecated */ + value: number; +} +declare function accessor(options: DeprecatedAccessorOptions): void; +accessor({ get [|value|]() { return 1; } }); +accessor({ set [|value|](_value: number) {} }); + +interface DeprecatedNamedOptions { + /** @deprecated */ + "string-name": number; + /** @deprecated */ + 1: number; +} +declare function named(options: DeprecatedNamedOptions): void; +named({ [|"string-name"|]: 1, [|1|]: 1 }); + +interface FirstDeprecatedOptions { + kind: "first"; + /** @deprecated */ + value: number; +} +interface SecondDeprecatedOptions { + kind: "second"; + /** @deprecated */ + value: number; +} +declare function selectDeprecated(options: FirstDeprecatedOptions): void; +declare function selectDeprecated(options: SecondDeprecatedOptions): void; + +selectDeprecated({ kind: "second", [|value|]: 1 });` + + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + defer done() + + f.VerifySuggestionDiagnostics(t, []*lsproto.Diagnostic{ + { + Code: &lsproto.IntegerOrString{Integer: new(int32(6385))}, + Message: lsproto.StringOrMarkupContent{String: new("'deprecatedValue' is deprecated.")}, + Tags: &[]lsproto.DiagnosticTag{lsproto.DiagnosticTagDeprecated}, + Range: f.Ranges()[0].LSRange, + }, + { + Code: &lsproto.IntegerOrString{Integer: new(int32(6385))}, + Message: lsproto.StringOrMarkupContent{String: new("'value' is deprecated.")}, + Tags: &[]lsproto.DiagnosticTag{lsproto.DiagnosticTagDeprecated}, + Range: f.Ranges()[1].LSRange, + }, + { + Code: &lsproto.IntegerOrString{Integer: new(int32(6387))}, + Message: lsproto.StringOrMarkupContent{String: new("The signature '(): number' of 'deprecatedCall' is deprecated.")}, + Tags: &[]lsproto.DiagnosticTag{lsproto.DiagnosticTagDeprecated}, + Range: f.Ranges()[2].LSRange, + }, + { + Code: &lsproto.IntegerOrString{Integer: new(int32(6385))}, + Message: lsproto.StringOrMarkupContent{String: new("'value' is deprecated.")}, + Tags: &[]lsproto.DiagnosticTag{lsproto.DiagnosticTagDeprecated}, + Range: f.Ranges()[3].LSRange, + }, + { + Code: &lsproto.IntegerOrString{Integer: new(int32(6385))}, + Message: lsproto.StringOrMarkupContent{String: new("'value' is deprecated.")}, + Tags: &[]lsproto.DiagnosticTag{lsproto.DiagnosticTagDeprecated}, + Range: f.Ranges()[4].LSRange, + }, + { + Code: &lsproto.IntegerOrString{Integer: new(int32(6385))}, + Message: lsproto.StringOrMarkupContent{String: new("'string-name' is deprecated.")}, + Tags: &[]lsproto.DiagnosticTag{lsproto.DiagnosticTagDeprecated}, + Range: f.Ranges()[5].LSRange, + }, + { + Code: &lsproto.IntegerOrString{Integer: new(int32(6385))}, + Message: lsproto.StringOrMarkupContent{String: new("'1' is deprecated.")}, + Tags: &[]lsproto.DiagnosticTag{lsproto.DiagnosticTagDeprecated}, + Range: f.Ranges()[6].LSRange, + }, + { + Code: &lsproto.IntegerOrString{Integer: new(int32(6385))}, + Message: lsproto.StringOrMarkupContent{String: new("'value' is deprecated.")}, + Tags: &[]lsproto.DiagnosticTag{lsproto.DiagnosticTagDeprecated}, + Range: f.Ranges()[7].LSRange, + }, + }) +}