Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 32 additions & 14 deletions internal/ast/diagnostic.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,20 @@ import (

"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/diagnostics"
"github.com/microsoft/typescript-go/internal/locale"
)

// Diagnostic

type Diagnostic struct {
file *SourceFile
loc core.TextRange
code int32
category diagnostics.Category
message string
file *SourceFile
loc core.TextRange
code int32
category diagnostics.Category
// Original message; may be nil.
message *diagnostics.Message
messageKey diagnostics.Key
messageArgs []string
messageChain []*Diagnostic
relatedInformation []*Diagnostic
reportsUnnecessary bool
Expand All @@ -31,7 +35,8 @@ func (d *Diagnostic) Len() int { return d.loc.Len() }
func (d *Diagnostic) Loc() core.TextRange { return d.loc }
func (d *Diagnostic) Code() int32 { return d.code }
func (d *Diagnostic) Category() diagnostics.Category { return d.category }
func (d *Diagnostic) Message() string { return d.message }
func (d *Diagnostic) MessageKey() diagnostics.Key { return d.messageKey }
func (d *Diagnostic) MessageArgs() []string { return d.messageArgs }
func (d *Diagnostic) MessageChain() []*Diagnostic { return d.messageChain }
func (d *Diagnostic) RelatedInformation() []*Diagnostic { return d.relatedInformation }
func (d *Diagnostic) ReportsUnnecessary() bool { return d.reportsUnnecessary }
Expand Down Expand Up @@ -72,12 +77,22 @@ func (d *Diagnostic) Clone() *Diagnostic {
return &result
}

func NewDiagnosticWith(
func (d *Diagnostic) Localize(locale locale.Locale) string {
return diagnostics.Localize(locale, d.message, d.messageKey, d.messageArgs...)
}

// For debugging only.
func (d *Diagnostic) String() string {
return diagnostics.Localize(locale.Default, d.message, d.messageKey, d.messageArgs...)
}

func NewDiagnosticFromSerialized(
file *SourceFile,
loc core.TextRange,
code int32,
category diagnostics.Category,
message string,
messageKey diagnostics.Key,
messageArgs []string,
messageChain []*Diagnostic,
relatedInformation []*Diagnostic,
reportsUnnecessary bool,
Expand All @@ -89,7 +104,8 @@ func NewDiagnosticWith(
loc: loc,
code: code,
category: category,
message: message,
messageKey: messageKey,
messageArgs: messageArgs,
messageChain: messageChain,
relatedInformation: relatedInformation,
reportsUnnecessary: reportsUnnecessary,
Expand All @@ -104,7 +120,9 @@ func NewDiagnostic(file *SourceFile, loc core.TextRange, message *diagnostics.Me
loc: loc,
code: message.Code(),
category: message.Category(),
message: message.Format(args...),
message: message,
messageKey: message.Key(),
messageArgs: diagnostics.StringifyArgs(args),
reportsUnnecessary: message.ReportsUnnecessary(),
reportsDeprecated: message.ReportsDeprecated(),
}
Expand Down Expand Up @@ -185,13 +203,13 @@ func EqualDiagnosticsNoRelatedInfo(d1, d2 *Diagnostic) bool {
return getDiagnosticPath(d1) == getDiagnosticPath(d2) &&
d1.Loc() == d2.Loc() &&
d1.Code() == d2.Code() &&
d1.Message() == d2.Message() &&
slices.Equal(d1.MessageArgs(), d2.MessageArgs()) &&
slices.EqualFunc(d1.MessageChain(), d2.MessageChain(), equalMessageChain)
}

func equalMessageChain(c1, c2 *Diagnostic) bool {
return c1.Code() == c2.Code() &&
c1.Message() == c2.Message() &&
slices.Equal(c1.MessageArgs(), c2.MessageArgs()) &&
slices.EqualFunc(c1.MessageChain(), c2.MessageChain(), equalMessageChain)
}

Expand All @@ -211,7 +229,7 @@ func compareMessageChainSize(c1, c2 []*Diagnostic) int {

func compareMessageChainContent(c1, c2 []*Diagnostic) int {
for i := range c1 {
c := strings.Compare(c1[i].Message(), c2[i].Message())
c := slices.Compare(c1[i].MessageArgs(), c2[i].MessageArgs())
if c != 0 {
return c
}
Expand Down Expand Up @@ -256,7 +274,7 @@ func CompareDiagnostics(d1, d2 *Diagnostic) int {
if c != 0 {
return c
}
c = strings.Compare(d1.Message(), d2.Message())
c = slices.Compare(d1.MessageArgs(), d2.MessageArgs())
if c != 0 {
return c
}
Expand Down
3 changes: 2 additions & 1 deletion internal/bundled/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (

"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/locale"
"github.com/microsoft/typescript-go/internal/parser"
"github.com/microsoft/typescript-go/internal/repo"
"github.com/microsoft/typescript-go/internal/tspath"
Expand Down Expand Up @@ -150,7 +151,7 @@ func readLibs() []lib {

if len(diags) > 0 {
for _, diag := range diags {
log.Printf("%s", diag.Message())
log.Printf("%s", diag.Localize(locale.Default))
}
log.Fatalf("failed to parse libs.json")
}
Expand Down
7 changes: 4 additions & 3 deletions internal/checker/checker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/microsoft/typescript-go/internal/checker"
"github.com/microsoft/typescript-go/internal/compiler"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/locale"
"github.com/microsoft/typescript-go/internal/repo"
"github.com/microsoft/typescript-go/internal/tsoptions"
"github.com/microsoft/typescript-go/internal/tspath"
Expand Down Expand Up @@ -36,7 +37,7 @@ foo.bar;`
fs = bundled.WrapFS(fs)

cd := "/"
host := compiler.NewCompilerHost(cd, fs, bundled.LibPath(), nil, nil)
host := compiler.NewCompilerHost(cd, fs, locale.Default, bundled.LibPath(), nil, nil)

parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile("/tsconfig.json", &core.CompilerOptions{}, host, nil)
assert.Equal(t, len(errors), 0, "Expected no errors in parsed command line")
Expand Down Expand Up @@ -70,7 +71,7 @@ func TestCheckSrcCompiler(t *testing.T) {

rootPath := tspath.CombinePaths(tspath.NormalizeSlashes(repo.TypeScriptSubmodulePath), "src", "compiler")

host := compiler.NewCompilerHost(rootPath, fs, bundled.LibPath(), nil, nil)
host := compiler.NewCompilerHost(rootPath, fs, locale.Default, bundled.LibPath(), nil, nil)
parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile(tspath.CombinePaths(rootPath, "tsconfig.json"), &core.CompilerOptions{}, host, nil)
assert.Equal(t, len(errors), 0, "Expected no errors in parsed command line")
p := compiler.NewProgram(compiler.ProgramOptions{
Expand All @@ -87,7 +88,7 @@ func BenchmarkNewChecker(b *testing.B) {

rootPath := tspath.CombinePaths(tspath.NormalizeSlashes(repo.TypeScriptSubmodulePath), "src", "compiler")

host := compiler.NewCompilerHost(rootPath, fs, bundled.LibPath(), nil, nil)
host := compiler.NewCompilerHost(rootPath, fs, locale.Default, bundled.LibPath(), nil, nil)
parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile(tspath.CombinePaths(rootPath, "tsconfig.json"), &core.CompilerOptions{}, host, nil)
assert.Equal(b, len(errors), 0, "Expected no errors in parsed command line")
p := compiler.NewProgram(compiler.ProgramOptions{
Expand Down
44 changes: 28 additions & 16 deletions internal/checker/jsx.go
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ func (c *Checker) elaborateJsxComponents(node *ast.Node, source *Type, target *T
if !ast.IsJsxSpreadAttribute(prop) && !isHyphenatedJsxName(prop.Name().Text()) {
nameType := c.getStringLiteralType(prop.Name().Text())
if nameType != nil && nameType.flags&TypeFlagsNever == 0 {
reportedError = c.elaborateElement(source, target, relation, prop.Name(), prop.Initializer(), nameType, nil, diagnosticOutput) || reportedError
reportedError = c.elaborateElement(source, target, relation, prop.Name(), prop.Initializer(), nameType, nil, nil, diagnosticOutput) || reportedError
}
}
}
Expand Down Expand Up @@ -326,13 +326,14 @@ func (c *Checker) elaborateJsxComponents(node *ast.Node, source *Type, target *T
nonArrayLikeTargetParts = c.filterType(childrenTargetType, func(t *Type) bool { return !c.isArrayOrTupleLikeType(t) })
}
var invalidTextDiagnostic *diagnostics.Message
getInvalidTextualChildDiagnostic := func() *diagnostics.Message {
var invalidTextDiagnosticArgs []any
getInvalidTextualChildDiagnostic := func() (*diagnostics.Message, []any) {
if invalidTextDiagnostic == nil {
tagNameText := scanner.GetTextOfNode(node.Parent.TagName())
diagnostic := diagnostics.X_0_components_don_t_accept_text_as_child_elements_Text_in_JSX_has_the_type_string_but_the_expected_type_of_1_is_2
invalidTextDiagnostic = diagnostics.FormatMessage(diagnostic, tagNameText, childrenPropName, c.TypeToString(childrenTargetType))
invalidTextDiagnostic = diagnostics.X_0_components_don_t_accept_text_as_child_elements_Text_in_JSX_has_the_type_string_but_the_expected_type_of_1_is_2
invalidTextDiagnosticArgs = []any{tagNameText, childrenPropName, c.TypeToString(childrenTargetType)}
}
return invalidTextDiagnostic
return invalidTextDiagnostic, invalidTextDiagnosticArgs
}
if moreThanOneRealChildren {
if arrayLikeTargetParts != c.neverType {
Expand All @@ -350,7 +351,7 @@ func (c *Checker) elaborateJsxComponents(node *ast.Node, source *Type, target *T
child := validChildren[0]
e := c.getElaborationElementForJsxChild(child, childrenNameType, getInvalidTextualChildDiagnostic)
if e.errorNode != nil {
reportedError = c.elaborateElement(source, target, relation, e.errorNode, e.innerExpression, e.nameType, e.errorMessage, diagnosticOutput) || reportedError
reportedError = c.elaborateElement(source, target, relation, e.errorNode, e.innerExpression, e.nameType, nil, e.createDiagnostic, diagnosticOutput) || reportedError
}
} else if !c.isTypeRelatedTo(c.getIndexedAccessType(source, childrenNameType), childrenTargetType, relation) {
// arity mismatch
Expand All @@ -364,13 +365,13 @@ func (c *Checker) elaborateJsxComponents(node *ast.Node, source *Type, target *T
}

type JsxElaborationElement struct {
errorNode *ast.Node
innerExpression *ast.Node
nameType *Type
errorMessage *diagnostics.Message
errorNode *ast.Node
innerExpression *ast.Node
nameType *Type
createDiagnostic func(prop *ast.Node) *ast.Diagnostic // Optional: creates a custom diagnostic for this element
}

func (c *Checker) generateJsxChildren(node *ast.Node, getInvalidTextDiagnostic func() *diagnostics.Message) iter.Seq[JsxElaborationElement] {
func (c *Checker) generateJsxChildren(node *ast.Node, getInvalidTextDiagnostic func() (*diagnostics.Message, []any)) iter.Seq[JsxElaborationElement] {
return func(yield func(JsxElaborationElement) bool) {
memberOffset := 0
for i, child := range node.Children().Nodes {
Expand All @@ -387,7 +388,7 @@ func (c *Checker) generateJsxChildren(node *ast.Node, getInvalidTextDiagnostic f
}
}

func (c *Checker) getElaborationElementForJsxChild(child *ast.Node, nameType *Type, getInvalidTextDiagnostic func() *diagnostics.Message) JsxElaborationElement {
func (c *Checker) getElaborationElementForJsxChild(child *ast.Node, nameType *Type, getInvalidTextDiagnostic func() (*diagnostics.Message, []any)) JsxElaborationElement {
switch child.Kind {
case ast.KindJsxExpression:
// child is of the type of the expression
Expand All @@ -398,7 +399,15 @@ func (c *Checker) getElaborationElementForJsxChild(child *ast.Node, nameType *Ty
return JsxElaborationElement{}
}
// child is a string
return JsxElaborationElement{errorNode: child, innerExpression: nil, nameType: nameType, errorMessage: getInvalidTextDiagnostic()}
return JsxElaborationElement{
errorNode: child,
innerExpression: nil,
nameType: nameType,
createDiagnostic: func(prop *ast.Node) *ast.Diagnostic {
errorMessage, errorArgs := getInvalidTextDiagnostic()
return NewDiagnosticForNode(prop, errorMessage, errorArgs...)
},
}
case ast.KindJsxElement, ast.KindJsxSelfClosingElement, ast.KindJsxFragment:
// child is of type JSX.Element
return JsxElaborationElement{errorNode: child, innerExpression: child, nameType: nameType}
Expand Down Expand Up @@ -448,18 +457,21 @@ func (c *Checker) elaborateIterableOrArrayLikeTargetElementwise(iterator iter.Se
if next != nil {
specificSource = c.checkExpressionForMutableLocationWithContextualType(next, sourcePropType)
}
if c.exactOptionalPropertyTypes && c.isExactOptionalPropertyMismatch(specificSource, targetPropType) {
if e.createDiagnostic != nil {
// Use the custom diagnostic factory if provided (e.g., for JSX text children with dynamic error messages)
c.reportDiagnostic(e.createDiagnostic(prop), diagnosticOutput)
} else if c.exactOptionalPropertyTypes && c.isExactOptionalPropertyMismatch(specificSource, targetPropType) {
diag := createDiagnosticForNode(prop, diagnostics.Type_0_is_not_assignable_to_type_1_with_exactOptionalPropertyTypes_Colon_true_Consider_adding_undefined_to_the_type_of_the_target, c.TypeToString(specificSource), c.TypeToString(targetPropType))
c.reportDiagnostic(diag, diagnosticOutput)
} else {
targetIsOptional := propName != ast.InternalSymbolNameMissing && core.OrElse(c.getPropertyOfType(tupleOrArrayLikeTargetParts, propName), c.unknownSymbol).Flags&ast.SymbolFlagsOptional != 0
sourceIsOptional := propName != ast.InternalSymbolNameMissing && core.OrElse(c.getPropertyOfType(source, propName), c.unknownSymbol).Flags&ast.SymbolFlagsOptional != 0
targetPropType = c.removeMissingType(targetPropType, targetIsOptional)
sourcePropType = c.removeMissingType(sourcePropType, targetIsOptional && sourceIsOptional)
result := c.checkTypeRelatedToEx(specificSource, targetPropType, relation, prop, e.errorMessage, diagnosticOutput)
result := c.checkTypeRelatedToEx(specificSource, targetPropType, relation, prop, nil, diagnosticOutput)
if result && specificSource != sourcePropType {
// If for whatever reason the expression type doesn't yield an error, make sure we still issue an error on the sourcePropType
c.checkTypeRelatedToEx(sourcePropType, targetPropType, relation, prop, e.errorMessage, diagnosticOutput)
c.checkTypeRelatedToEx(sourcePropType, targetPropType, relation, prop, nil, diagnosticOutput)
}
}
}
Expand Down
13 changes: 8 additions & 5 deletions internal/checker/relater.go
Original file line number Diff line number Diff line change
Expand Up @@ -503,10 +503,10 @@ func (c *Checker) elaborateObjectLiteral(node *ast.Node, source *Type, target *T
}
switch prop.Kind {
case ast.KindSetAccessor, ast.KindGetAccessor, ast.KindMethodDeclaration, ast.KindShorthandPropertyAssignment:
reportedError = c.elaborateElement(source, target, relation, prop.Name(), nil, nameType, nil, diagnosticOutput) || reportedError
reportedError = c.elaborateElement(source, target, relation, prop.Name(), nil, nameType, nil, nil, diagnosticOutput) || reportedError
case ast.KindPropertyAssignment:
message := core.IfElse(ast.IsComputedNonLiteralName(prop.Name()), diagnostics.Type_of_computed_property_s_value_is_0_which_is_not_assignable_to_type_1, nil)
reportedError = c.elaborateElement(source, target, relation, prop.Name(), prop.Initializer(), nameType, message, diagnosticOutput) || reportedError
reportedError = c.elaborateElement(source, target, relation, prop.Name(), prop.Initializer(), nameType, message, nil, diagnosticOutput) || reportedError
}
}
return reportedError
Expand All @@ -531,12 +531,12 @@ func (c *Checker) elaborateArrayLiteral(node *ast.Node, source *Type, target *Ty
}
nameType := c.getNumberLiteralType(jsnum.Number(i))
checkNode := c.getEffectiveCheckNode(element)
reportedError = c.elaborateElement(source, target, relation, checkNode, checkNode, nameType, nil, diagnosticOutput) || reportedError
reportedError = c.elaborateElement(source, target, relation, checkNode, checkNode, nameType, nil, nil, diagnosticOutput) || reportedError
}
return reportedError
}

func (c *Checker) elaborateElement(source *Type, target *Type, relation *Relation, prop *ast.Node, next *ast.Node, nameType *Type, errorMessage *diagnostics.Message, diagnosticOutput *[]*ast.Diagnostic) bool {
func (c *Checker) elaborateElement(source *Type, target *Type, relation *Relation, prop *ast.Node, next *ast.Node, nameType *Type, errorMessage *diagnostics.Message, diagnosticFactory func(prop *ast.Node) *ast.Diagnostic, diagnosticOutput *[]*ast.Diagnostic) bool {
targetPropType := c.getBestMatchIndexedAccessTypeOrUndefined(source, target, nameType)
if targetPropType == nil || targetPropType.flags&TypeFlagsIndexedAccess != 0 {
// Don't elaborate on indexes on generic variables
Expand All @@ -557,7 +557,10 @@ func (c *Checker) elaborateElement(source *Type, target *Type, relation *Relatio
if next != nil {
specificSource = c.checkExpressionForMutableLocationWithContextualType(next, sourcePropType)
}
if c.exactOptionalPropertyTypes && c.isExactOptionalPropertyMismatch(specificSource, targetPropType) {
if diagnosticFactory != nil {
// Use the custom diagnostic factory if provided (e.g., for JSX text children with dynamic error messages)
diags = append(diags, diagnosticFactory(prop))
} else if c.exactOptionalPropertyTypes && c.isExactOptionalPropertyMismatch(specificSource, targetPropType) {
diags = append(diags, createDiagnosticForNode(prop, diagnostics.Type_0_is_not_assignable_to_type_1_with_exactOptionalPropertyTypes_Colon_true_Consider_adding_undefined_to_the_type_of_the_target, c.TypeToString(specificSource), c.TypeToString(targetPropType)))
} else {
propName := c.getPropertyNameFromIndex(nameType, nil /*accessNode*/)
Expand Down
12 changes: 11 additions & 1 deletion internal/compiler/host.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package compiler
import (
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/locale"
"github.com/microsoft/typescript-go/internal/parser"
"github.com/microsoft/typescript-go/internal/tsoptions"
"github.com/microsoft/typescript-go/internal/tspath"
Expand All @@ -14,6 +15,7 @@ type CompilerHost interface {
FS() vfs.FS
DefaultLibraryPath() string
GetCurrentDirectory() string
Locale() locale.Locale
Trace(msg string)
GetSourceFile(opts ast.SourceFileParseOptions) *ast.SourceFile
GetResolvedProjectReference(fileName string, path tspath.Path) *tsoptions.ParsedCommandLine
Expand All @@ -24,6 +26,7 @@ var _ CompilerHost = (*compilerHost)(nil)
type compilerHost struct {
currentDirectory string
fs vfs.FS
locale locale.Locale
defaultLibraryPath string
extendedConfigCache tsoptions.ExtendedConfigCache
trace func(msg string)
Expand All @@ -32,16 +35,18 @@ type compilerHost struct {
func NewCachedFSCompilerHost(
currentDirectory string,
fs vfs.FS,
locale locale.Locale,
defaultLibraryPath string,
extendedConfigCache tsoptions.ExtendedConfigCache,
trace func(msg string),
) CompilerHost {
return NewCompilerHost(currentDirectory, cachedvfs.From(fs), defaultLibraryPath, extendedConfigCache, trace)
return NewCompilerHost(currentDirectory, cachedvfs.From(fs), locale, defaultLibraryPath, extendedConfigCache, trace)
}

func NewCompilerHost(
currentDirectory string,
fs vfs.FS,
locale locale.Locale,
defaultLibraryPath string,
extendedConfigCache tsoptions.ExtendedConfigCache,
trace func(msg string),
Expand All @@ -52,6 +57,7 @@ func NewCompilerHost(
return &compilerHost{
currentDirectory: currentDirectory,
fs: fs,
locale: locale,
defaultLibraryPath: defaultLibraryPath,
extendedConfigCache: extendedConfigCache,
trace: trace,
Expand All @@ -70,6 +76,10 @@ func (h *compilerHost) GetCurrentDirectory() string {
return h.currentDirectory
}

func (h *compilerHost) Locale() locale.Locale {
return h.locale
}

func (h *compilerHost) Trace(msg string) {
h.trace(msg)
}
Expand Down
4 changes: 2 additions & 2 deletions internal/compiler/program.go
Original file line number Diff line number Diff line change
Expand Up @@ -1565,10 +1565,10 @@ func (p *Program) ExplainFiles(w io.Writer) {
for _, file := range p.GetSourceFiles() {
fmt.Fprintln(w, toRelativeFileName(file.FileName()))
for _, reason := range p.includeProcessor.fileIncludeReasons[file.Path()] {
fmt.Fprintln(w, " ", reason.toDiagnostic(p, true).Message())
fmt.Fprintln(w, " ", reason.toDiagnostic(p, true).Localize(p.Host().Locale()))
}
for _, diag := range p.includeProcessor.explainRedirectAndImpliedFormat(p, file, toRelativeFileName) {
fmt.Fprintln(w, " ", diag.Message())
fmt.Fprintln(w, " ", diag.Localize(p.Host().Locale()))
}
}
}
Expand Down
Loading