Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
17 changes: 10 additions & 7 deletions pkg/ast/parseoptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ func GetSourceFileAffectingCompilerOptions(fileName string, options *core.Compil
}

type ExternalModuleIndicatorOptions struct {
jsx bool
force bool
JSX bool
Force bool
}

func GetExternalModuleIndicatorOptions(fileName string, options *core.CompilerOptions, metadata SourceFileMetaData) ExternalModuleIndicatorOptions {
Expand All @@ -43,7 +43,7 @@ func GetExternalModuleIndicatorOptions(fileName string, options *core.CompilerOp
switch options.GetEmitModuleDetectionKind() {
case core.ModuleDetectionKindForce:
// All non-declaration files are modules, declaration files still do the usual isFileProbablyExternalModule
return ExternalModuleIndicatorOptions{force: true}
return ExternalModuleIndicatorOptions{Force: true}
case core.ModuleDetectionKindLegacy:
// Files are modules if they have imports, exports, or import.meta
return ExternalModuleIndicatorOptions{}
Expand All @@ -52,8 +52,8 @@ func GetExternalModuleIndicatorOptions(fileName string, options *core.CompilerOp
// If jsx is react-jsx or react-jsxdev then jsx tags force module-ness
// otherwise, the presence of import or export statments (or import.meta) implies module-ness
return ExternalModuleIndicatorOptions{
jsx: options.Jsx == core.JsxEmitReactJSX || options.Jsx == core.JsxEmitReactJSXDev,
force: isFileForcedToBeModuleByFormat(fileName, options, metadata),
JSX: options.Jsx == core.JsxEmitReactJSX || options.Jsx == core.JsxEmitReactJSXDev,
Force: isFileForcedToBeModuleByFormat(fileName, options, metadata),
}
default:
return ExternalModuleIndicatorOptions{}
Expand Down Expand Up @@ -89,13 +89,13 @@ func getExternalModuleIndicator(file *SourceFile, opts ExternalModuleIndicatorOp
return nil
}

if opts.jsx {
if opts.JSX {
if node := isFileModuleFromUsingJSXTag(file); node != nil {
return node
}
}

if opts.force {
if opts.Force {
return file.AsNode()
}

Expand All @@ -112,6 +112,9 @@ func isFileProbablyExternalModule(sourceFile *SourceFile) *Node {
}

func isAnExternalModuleIndicatorNode(node *Node) bool {
if node.Flags&NodeFlagsReparsed != 0 {
return false
}
return HasSyntacticModifier(node, ModifierFlagsExport) ||
IsImportEqualsDeclaration(node) && IsExternalModuleReference(node.AsImportEqualsDeclaration().ModuleReference) ||
IsImportDeclaration(node) || IsExportAssignment(node) || IsExportDeclaration(node)
Expand Down
9 changes: 9 additions & 0 deletions pkg/ast/utilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -3921,3 +3921,12 @@ func HasContextSensitiveParameters(node *Node) bool {
func IsInfinityOrNaNString(name string) bool {
return name == "Infinity" || name == "-Infinity" || name == "NaN"
}

func GetFirstConstructorWithBody(node *Node) *Node {
for _, member := range node.Members() {
if IsConstructorDeclaration(member) && NodeIsPresent(member.Body()) {
return member
}
}
return nil
}
44 changes: 32 additions & 12 deletions pkg/checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -1518,12 +1518,14 @@ func (c *Checker) onFailedToResolveSymbol(errorLocation *ast.Node, name string,
suggestion := c.getSuggestedSymbolForNonexistentSymbol(errorLocation, name, meaning)
if suggestion != nil && !(suggestion.ValueDeclaration != nil && ast.IsAmbientModule(suggestion.ValueDeclaration) && ast.IsGlobalScopeAugmentation(suggestion.ValueDeclaration)) {
suggestionName := c.symbolToString(suggestion)
message := core.IfElse(meaning == ast.SymbolFlagsNamespace, diagnostics.Cannot_find_namespace_0_Did_you_mean_1, diagnostics.Cannot_find_name_0_Did_you_mean_1)
isUncheckedJS := c.isUncheckedJSSuggestion(errorLocation, suggestion, false /*excludeClasses*/)
message := core.IfElse(meaning == ast.SymbolFlagsNamespace, diagnostics.Cannot_find_namespace_0_Did_you_mean_1,
core.IfElse(isUncheckedJS, diagnostics.Could_not_find_name_0_Did_you_mean_1, diagnostics.Cannot_find_name_0_Did_you_mean_1))
diagnostic := NewDiagnosticForNode(errorLocation, message, name, suggestionName)
if suggestion.ValueDeclaration != nil {
diagnostic.AddRelatedInfo(NewDiagnosticForNode(suggestion.ValueDeclaration, diagnostics.X_0_is_declared_here, suggestionName))
}
c.diagnostics.Add(diagnostic)
c.addErrorOrSuggestion(!isUncheckedJS, diagnostic)
return
}
// And then fall back to unspecified "not found"
Expand Down Expand Up @@ -10873,11 +10875,10 @@ func (c *Checker) checkPropertyAccessExpressionOrQualifiedName(node *ast.Node, l
if c.checkPrivateIdentifierPropertyAccess(leftType, right, lexicallyScopedSymbol) {
return c.errorType
}
// !!!
// containingClass := getContainingClassExcludingClassDecorators(right)
// if containingClass && isPlainJSFile(ast.GetSourceFileOfNode(containingClass), c.compilerOptions.checkJs) {
// c.grammarErrorOnNode(right, diagnostics.Private_field_0_must_be_declared_in_an_enclosing_class, right.Text())
// }
containingClass := getContainingClassExcludingClassDecorators(right)
if containingClass != nil && ast.IsPlainJSFile(ast.GetSourceFileOfNode(containingClass), c.compilerOptions.CheckJs) {
c.grammarErrorOnNode(right, diagnostics.Private_field_0_must_be_declared_in_an_enclosing_class, right.Text())
}
} else {
isSetonlyAccessor := prop.Flags&ast.SymbolFlagsSetAccessor != 0 && prop.Flags&ast.SymbolFlagsGetAccessor == 0
if isSetonlyAccessor && assignmentKind != AssignmentKindDefinite {
Expand All @@ -10904,6 +10905,10 @@ func (c *Checker) checkPropertyAccessExpressionOrQualifiedName(node *ast.Node, l
indexInfo = c.getApplicableIndexInfoForName(apparentType, right.Text())
}
if indexInfo == nil {
isUncheckedJS := c.isUncheckedJSSuggestion(node, leftType.symbol, true /*excludeClasses*/)
if !isUncheckedJS && c.isJSLiteralType(leftType) {
return c.anyType
}
if leftType.symbol == c.globalThisSymbol {
globalSymbol := c.globalThisSymbol.Exports[right.Text()]
if globalSymbol != nil && globalSymbol.Flags&ast.SymbolFlagsBlockScoped != 0 {
Expand All @@ -10914,7 +10919,7 @@ func (c *Checker) checkPropertyAccessExpressionOrQualifiedName(node *ast.Node, l
return c.anyType
}
if right.Text() != "" && !c.checkAndReportErrorForExtendingInterface(node) {
c.reportNonexistentProperty(right, core.IfElse(isThisTypeParameter(leftType), apparentType, leftType))
c.reportNonexistentProperty(right, core.IfElse(isThisTypeParameter(leftType), apparentType, leftType), isUncheckedJS)
}
return c.errorType
}
Expand Down Expand Up @@ -11091,7 +11096,7 @@ func (c *Checker) checkPrivateIdentifierPropertyAccess(leftType *Type, right *as
return false
}

func (c *Checker) reportNonexistentProperty(propNode *ast.Node, containingType *Type) {
func (c *Checker) reportNonexistentProperty(propNode *ast.Node, containingType *Type, isUncheckedJS bool) {
if ast.IsJSDocNameReferenceContext(propNode) {
return
}
Expand Down Expand Up @@ -11123,7 +11128,8 @@ func (c *Checker) reportNonexistentProperty(propNode *ast.Node, containingType *
suggestion := c.getSuggestedSymbolForNonexistentProperty(propNode, containingType)
if suggestion != nil {
suggestedName := ast.SymbolName(suggestion)
diagnostic = NewDiagnosticChainForNode(diagnostic, propNode, diagnostics.Property_0_does_not_exist_on_type_1_Did_you_mean_2, missingProperty, container, suggestedName)
message := core.IfElse(isUncheckedJS, diagnostics.Property_0_may_not_exist_on_type_1_Did_you_mean_2, diagnostics.Property_0_does_not_exist_on_type_1_Did_you_mean_2)
diagnostic = NewDiagnosticChainForNode(diagnostic, propNode, message, missingProperty, container, suggestedName)
if suggestion.ValueDeclaration != nil {
diagnostic.AddRelatedInfo(NewDiagnosticForNode(suggestion.ValueDeclaration, diagnostics.X_0_is_declared_here, suggestedName))
}
Expand All @@ -11140,7 +11146,7 @@ func (c *Checker) reportNonexistentProperty(propNode *ast.Node, containingType *
}
}
}
c.diagnostics.Add(diagnostic)
c.addErrorOrSuggestion(!isUncheckedJS || diagnostic.Code() != diagnostics.Property_0_may_not_exist_on_type_1_Did_you_mean_2.Code(), diagnostic)
}

func (c *Checker) getSuggestedLibForNonExistentProperty(missingProperty string, containingType *Type) string {
Expand Down Expand Up @@ -12609,7 +12615,8 @@ func (c *Checker) checkInExpression(left *ast.Expression, right *ast.Expression,
// Unlike in 'checkPrivateIdentifierExpression' we now have access to the RHS type
// which provides us with the opportunity to emit more detailed errors
if c.symbolNodeLinks.Get(left).resolvedSymbol == nil && ast.GetContainingClass(left) != nil {
c.reportNonexistentProperty(left, rightType)
isUncheckedJS := c.isUncheckedJSSuggestion(left, rightType.symbol, true /*excludeClasses*/)
c.reportNonexistentProperty(left, rightType, isUncheckedJS)
}
} else {
// The type of the left operand must be assignable to string, number, or symbol.
Expand Down Expand Up @@ -12714,6 +12721,9 @@ func (c *Checker) checkObjectLiteral(node *ast.Node, checkMode CheckMode) *Type
}
result := c.newAnonymousType(node.Symbol(), propertiesTable, nil, nil, indexInfos)
result.objectFlags |= objectFlags | ObjectFlagsObjectLiteral | ObjectFlagsContainsObjectOrArrayLiteral
if contextualType == nil && ast.IsInJSFile(node) && !ast.IsInJsonFile(node) {
result.objectFlags |= ObjectFlagsJSLiteral
}
if patternWithComputedProperties {
result.objectFlags |= ObjectFlagsObjectLiteralPatternWithComputedProperties
}
Expand Down Expand Up @@ -17598,6 +17608,10 @@ func (c *Checker) widenTypeForVariableLikeDeclaration(t *Type, declaration *ast.
}

func (c *Checker) reportImplicitAny(declaration *ast.Node, t *Type, wideningKind WideningKind) {
if ast.IsInJSFile(declaration) && !ast.IsCheckJSEnabledForFile(ast.GetSourceFileOfNode(declaration), c.compilerOptions) {
// Only report implicit any errors/suggestions in TS and ts-check JS files
return
}
typeAsString := c.TypeToString(c.getWidenedType(t))
var diagnostic *diagnostics.Message
switch declaration.Kind {
Expand Down Expand Up @@ -26321,6 +26335,9 @@ func (c *Checker) getPropertyTypeForIndexType(originalObjectType *Type, objectTy
if indexType.flags&TypeFlagsNever != 0 {
return c.neverType
}
if c.isJSLiteralType(objectType) {
return c.anyType
}
if accessExpression != nil && !isConstEnumObjectType(objectType) {
if isObjectLiteralType(objectType) {
if c.noImplicitAny && indexType.flags&(TypeFlagsStringLiteral|TypeFlagsNumberLiteral) != 0 {
Expand Down Expand Up @@ -26378,6 +26395,9 @@ func (c *Checker) getPropertyTypeForIndexType(originalObjectType *Type, objectTy
if accessFlags&AccessFlagsAllowMissing != 0 && isObjectLiteralType(objectType) {
return c.undefinedType
}
if c.isJSLiteralType(objectType) {
return c.anyType
}
if accessNode != nil {
indexNode := getIndexNodeForAccessExpression(accessNode)
if indexNode.Kind != ast.KindBigIntLiteral && indexType.flags&(TypeFlagsStringLiteral|TypeFlagsNumberLiteral) != 0 {
Expand Down
89 changes: 89 additions & 0 deletions pkg/checker/utilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -1825,3 +1825,92 @@ func nodeStartsNewLexicalEnvironment(node *ast.Node) bool {
}
return false
}

// Determines whether a did-you-mean error should be a suggestion in an unchecked JS file.
// Only applies to unchecked JS files without checkJS, // @ts-check or // @ts-nocheck
// It does not suggest when the suggestion:
// - Is from a global file that is different from the reference file, or
// - (optionally) Is a class, or is a this.x property access expression
func (c *Checker) isUncheckedJSSuggestion(node *ast.Node, suggestion *ast.Symbol, excludeClasses bool) bool {
file := ast.GetSourceFileOfNode(node)
if file != nil {
if c.compilerOptions.CheckJs.IsUnknown() && file.CheckJsDirective == nil && (file.ScriptKind == core.ScriptKindJS || file.ScriptKind == core.ScriptKindJSX) {
var declarationFile *ast.SourceFile
if suggestion != nil {
if firstDeclaration := core.FirstOrNil(suggestion.Declarations); firstDeclaration != nil {
declarationFile = ast.GetSourceFileOfNode(firstDeclaration)
}
}
suggestionHasNoExtendsOrDecorators := suggestion == nil ||
suggestion.ValueDeclaration == nil ||
!ast.IsClassLike(suggestion.ValueDeclaration) ||
len(ast.GetExtendsHeritageClauseElements(suggestion.ValueDeclaration)) != 0 ||
classOrConstructorParameterIsDecorated(suggestion.ValueDeclaration)
return !(file != declarationFile && declarationFile != nil && ast.IsGlobalSourceFile(declarationFile.AsNode())) &&
!(excludeClasses && suggestion != nil && suggestion.Flags&ast.SymbolFlagsClass != 0 && suggestionHasNoExtendsOrDecorators) &&
!(node != nil && excludeClasses && ast.IsPropertyAccessExpression(node) && node.Expression().Kind == ast.KindThisKeyword && suggestionHasNoExtendsOrDecorators)
}
}
return false
}

func classOrConstructorParameterIsDecorated(node *ast.Node) bool {
if nodeIsDecorated(node, nil, nil) {
return true
}
constructor := ast.GetFirstConstructorWithBody(node)
return constructor != nil && childIsDecorated(constructor, node)
}

func nodeIsDecorated(node *ast.Node, parent *ast.Node, grandparent *ast.Node) bool {
return ast.HasDecorators(node) && nodeCanBeDecorated(false, node, parent, grandparent)
}

func nodeOrChildIsDecorated(node *ast.Node, parent *ast.Node, grandparent *ast.Node) bool {
return nodeIsDecorated(node, parent, grandparent) || childIsDecorated(node, parent)
}

func childIsDecorated(node *ast.Node, parent *ast.Node) bool {
switch node.Kind {
case ast.KindClassDeclaration, ast.KindClassExpression:
return core.Some(node.Members(), func(m *ast.Node) bool {
return nodeOrChildIsDecorated(m, node, parent)
})
case ast.KindMethodDeclaration,
ast.KindSetAccessor,
ast.KindConstructor:
return core.Some(node.Parameters(), func(p *ast.Node) bool {
return nodeIsDecorated(p, node, parent)
})
default:
return false
}
}

// Returns if a type is or consists of a JSLiteral object type
// In addition to objects which are directly literals,
// * unions where every element is a jsliteral
// * intersections where at least one element is a jsliteral
// * and instantiable types constrained to a jsliteral
// Should all count as literals and not print errors on access or assignment of possibly existing properties.
// This mirrors the behavior of the index signature propagation, to which this behaves similarly (but doesn't affect assignability or inference).
func (c *Checker) isJSLiteralType(t *Type) bool {
if c.noImplicitAny {
return false
// Flag is meaningless under `noImplicitAny` mode
}
if t.objectFlags&ObjectFlagsJSLiteral != 0 {
return true
}
if t.flags&TypeFlagsUnion != 0 {
return core.Every(t.AsUnionType().types, c.isJSLiteralType)
}
if t.flags&TypeFlagsIntersection != 0 {
return core.Some(t.AsIntersectionType().types, c.isJSLiteralType)
}
if t.flags&TypeFlagsInstantiable != 0 {
constraint := c.getResolvedBaseConstraint(t, nil)
return constraint != t && c.isJSLiteralType(constraint)
}
return false
}
7 changes: 3 additions & 4 deletions pkg/core/compileroptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,12 +232,11 @@ func (options *CompilerOptions) GetEmitModuleDetectionKind() ModuleDetectionKind
if options.ModuleDetection != ModuleDetectionKindNone {
return options.ModuleDetection
}
switch options.GetEmitModuleKind() {
case ModuleKindNode16, ModuleKindNode20, ModuleKindNodeNext:
moduleKind := options.GetEmitModuleKind()
if ModuleKindNode16 <= moduleKind && moduleKind <= ModuleKindNodeNext {
return ModuleDetectionKindForce
default:
return ModuleDetectionKindAuto
}
return ModuleDetectionKindAuto
}

func (options *CompilerOptions) GetResolvePackageJsonExports() bool {
Expand Down
7 changes: 5 additions & 2 deletions pkg/format/indent.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,11 @@ func getIndentationForNodeWorker(
// }, { itself contributes nothing.
// prop: 1 L3 - The indentation of the second object literal is best understood by
// }) looking at the relationship between the list and *first* list item.
listLine, _ := getStartLineAndCharacterForNode(firstListChild, sourceFile)
listIndentsChild := firstListChild != nil && listLine > containingListOrParentStartLine
var listIndentsChild bool
if firstListChild != nil {
listLine, _ := getStartLineAndCharacterForNode(firstListChild, sourceFile)
listIndentsChild = listLine > containingListOrParentStartLine
}
actualIndentation := getActualIndentationForListItem(current, sourceFile, options, listIndentsChild)
if actualIndentation != -1 {
return actualIndentation + indentationDelta
Expand Down
63 changes: 63 additions & 0 deletions pkg/ls/format_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,66 @@ func TestGetFormattingEditsAfterKeystroke_SimpleStatement(t *testing.T) {
// Should return nil or empty edits, not panic
_ = edits
}

// Test for issue: Crash in range formatting when requested on a line that is different from the containing function
// This reproduces the panic when formatting a range inside a function body
func TestGetFormattingEditsForRange_FunctionBody(t *testing.T) {
t.Parallel()

testCases := []struct {
name string
text string
startPos int
endPos int
}{
{
name: "return statement in function",
text: "function foo() {\n return (1 + 2);\n}",
startPos: 21, // Start of "return"
endPos: 38, // End of ");"
},
{
name: "function with newline after keyword",
text: "function\nf() {\n}",
startPos: 9, // After "function\n"
endPos: 13, // Inside or after function
},
{
name: "empty function body",
text: "function f() {\n \n}",
startPos: 15, // Inside body
endPos: 17, // Inside body
},
{
name: "after function closing brace",
text: "function f() {\n}",
startPos: 15, // After closing brace
endPos: 15,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{
FileName: "/test.ts",
Path: "/test.ts",
}, tc.text, core.ScriptKindTS)

langService := &LanguageService{}
ctx := context.Background()
options := format.GetDefaultFormatCodeSettings("\n")

// This should not panic
edits := langService.getFormattingEditsForRange(
ctx,
sourceFile,
options,
core.NewTextRange(tc.startPos, tc.endPos),
)

// Should not panic
_ = edits // Just ensuring no panic
})
}
}
Loading