vendor tsgo

This commit is contained in:
2026-07-09 16:50:43 -04:00
parent c06ea2e5a4
commit 98978e4930
5804 changed files with 1556156 additions and 101 deletions

View File

@@ -0,0 +1,104 @@
package lsutil
import (
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/astnav"
"github.com/microsoft/typescript-go/internal/scanner"
)
func PositionIsASICandidate(pos int, context *ast.Node, file *ast.SourceFile) bool {
contextAncestor := ast.FindAncestorOrQuit(context, func(ancestor *ast.Node) ast.FindAncestorResult {
if ancestor.End() != pos {
return ast.FindAncestorQuit
}
return ast.ToFindAncestorResult(SyntaxMayBeASICandidate(ancestor.Kind))
})
return contextAncestor != nil && NodeIsASICandidate(contextAncestor, file)
}
func SyntaxMayBeASICandidate(kind ast.Kind) bool {
return SyntaxRequiresTrailingCommaOrSemicolonOrASI(kind) ||
SyntaxRequiresTrailingFunctionBlockOrSemicolonOrASI(kind) ||
SyntaxRequiresTrailingModuleBlockOrSemicolonOrASI(kind) ||
SyntaxRequiresTrailingSemicolonOrASI(kind)
}
func SyntaxRequiresTrailingCommaOrSemicolonOrASI(kind ast.Kind) bool {
return kind == ast.KindCallSignature ||
kind == ast.KindConstructSignature ||
kind == ast.KindIndexSignature ||
kind == ast.KindPropertySignature ||
kind == ast.KindMethodSignature
}
func SyntaxRequiresTrailingFunctionBlockOrSemicolonOrASI(kind ast.Kind) bool {
return kind == ast.KindFunctionDeclaration ||
kind == ast.KindConstructor ||
kind == ast.KindMethodDeclaration ||
kind == ast.KindGetAccessor ||
kind == ast.KindSetAccessor
}
func SyntaxRequiresTrailingModuleBlockOrSemicolonOrASI(kind ast.Kind) bool {
return kind == ast.KindModuleDeclaration
}
func SyntaxRequiresTrailingSemicolonOrASI(kind ast.Kind) bool {
return kind == ast.KindVariableStatement ||
kind == ast.KindExpressionStatement ||
kind == ast.KindDoStatement ||
kind == ast.KindContinueStatement ||
kind == ast.KindBreakStatement ||
kind == ast.KindReturnStatement ||
kind == ast.KindThrowStatement ||
kind == ast.KindDebuggerStatement ||
kind == ast.KindPropertyDeclaration ||
kind == ast.KindTypeAliasDeclaration ||
kind == ast.KindImportDeclaration ||
kind == ast.KindImportEqualsDeclaration ||
kind == ast.KindExportDeclaration ||
kind == ast.KindNamespaceExportDeclaration ||
kind == ast.KindExportAssignment
}
func NodeIsASICandidate(node *ast.Node, file *ast.SourceFile) bool {
lastToken := GetLastToken(node, file)
if lastToken != nil && lastToken.Kind == ast.KindSemicolonToken {
return false
}
if SyntaxRequiresTrailingCommaOrSemicolonOrASI(node.Kind) {
if lastToken != nil && lastToken.Kind == ast.KindCommaToken {
return false
}
} else if SyntaxRequiresTrailingModuleBlockOrSemicolonOrASI(node.Kind) {
lastChild := GetLastChild(node, file)
if lastChild != nil && ast.IsModuleBlock(lastChild) {
return false
}
} else if SyntaxRequiresTrailingFunctionBlockOrSemicolonOrASI(node.Kind) {
lastChild := GetLastChild(node, file)
if lastChild != nil && ast.IsFunctionBlock(lastChild) {
return false
}
} else if !SyntaxRequiresTrailingSemicolonOrASI(node.Kind) {
return false
}
// See comment in parser's `parseDoStatement`
if node.Kind == ast.KindDoStatement {
return true
}
topNode := ast.FindAncestor(node, func(ancestor *ast.Node) bool { return ancestor.Parent == nil })
nextToken := astnav.FindNextToken(node, topNode, file)
if nextToken == nil || nextToken.Kind == ast.KindCloseBraceToken {
return true
}
startLine := scanner.GetECMALineOfPosition(file, node.End())
endLine := scanner.GetECMALineOfPosition(file, astnav.GetStartOfNode(nextToken, file, false /*includeJSDoc*/))
return startLine != endLine
}

View File

@@ -0,0 +1,130 @@
package lsutil
import (
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/astnav"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/scanner"
)
// Replaces last(node.getChildren(sourceFile))
func GetLastChild(node *ast.Node, sourceFile *ast.SourceFile) *ast.Node {
lastChildNode := GetLastVisitedChild(node, sourceFile)
if ast.IsJSDocSingleCommentNode(node) && lastChildNode == nil {
return nil
}
var tokenStartPos int
if lastChildNode != nil {
tokenStartPos = lastChildNode.End()
} else {
tokenStartPos = node.Pos()
}
var lastToken *ast.Node
scanner := scanner.GetScannerForSourceFile(sourceFile, tokenStartPos)
for startPos := tokenStartPos; startPos < node.End(); {
tokenKind := scanner.Token()
tokenFullStart := scanner.TokenFullStart()
tokenEnd := scanner.TokenEnd()
lastToken = sourceFile.GetOrCreateToken(tokenKind, tokenFullStart, tokenEnd, node, scanner.TokenFlags())
startPos = tokenEnd
scanner.Scan()
}
return core.IfElse(lastToken != nil, lastToken, lastChildNode)
}
func GetLastToken(node *ast.Node, sourceFile *ast.SourceFile) *ast.Node {
if node == nil {
return nil
}
if ast.IsTokenKind(node.Kind) || ast.IsIdentifier(node) {
return nil
}
AssertHasRealPosition(node)
lastChild := GetLastChild(node, sourceFile)
if lastChild == nil {
return nil
}
if lastChild.Kind < ast.KindFirstNode {
return lastChild
} else {
return GetLastToken(lastChild, sourceFile)
}
}
// Gets the last visited child of the given node.
// NOTE: This doesn't include unvisited tokens; for this, use `getLastChild` or `getLastToken`.
func GetLastVisitedChild(node *ast.Node, sourceFile *ast.SourceFile) *ast.Node {
var lastChild *ast.Node
visitNode := func(n *ast.Node, _ *ast.NodeVisitor) *ast.Node {
if n != nil && n.Flags&ast.NodeFlagsReparsed == 0 {
lastChild = n
}
return n
}
visitNodeList := func(nodeList *ast.NodeList, _ *ast.NodeVisitor) *ast.NodeList {
if nodeList != nil && len(nodeList.Nodes) > 0 {
for i := len(nodeList.Nodes) - 1; i >= 0; i-- {
if nodeList.Nodes[i].Flags&ast.NodeFlagsReparsed == 0 {
lastChild = nodeList.Nodes[i]
break
}
}
}
return nodeList
}
astnav.VisitEachChildAndJSDoc(node, sourceFile, visitNode, visitNodeList)
return lastChild
}
func GetFirstToken(node *ast.Node, sourceFile *ast.SourceFile) *ast.Node {
if ast.IsIdentifier(node) || ast.IsTokenKind(node.Kind) {
return nil
}
AssertHasRealPosition(node)
var firstChild *ast.Node
node.ForEachChild(func(n *ast.Node) bool {
if n == nil || node.Flags&ast.NodeFlagsReparsed != 0 {
return false
}
firstChild = n
return true
})
var tokenEndPosition int
if firstChild != nil {
tokenEndPosition = firstChild.Pos()
} else {
tokenEndPosition = node.End()
}
scanner := scanner.GetScannerForSourceFile(sourceFile, node.Pos())
var firstToken *ast.Node
if node.Pos() < tokenEndPosition {
tokenKind := scanner.Token()
tokenFullStart := scanner.TokenFullStart()
tokenEnd := scanner.TokenEnd()
firstToken = sourceFile.GetOrCreateToken(tokenKind, tokenFullStart, tokenEnd, node, scanner.TokenFlags())
}
if firstToken != nil {
return firstToken
}
if firstChild == nil {
return nil
}
if firstChild.Kind < ast.KindFirstNode {
return firstChild
}
return GetFirstToken(firstChild, sourceFile)
}
func AssertHasRealPosition(node *ast.Node) {
if ast.PositionIsSynthesized(node.Pos()) || ast.PositionIsSynthesized(node.End()) {
panic("Node must have a real position for this operation.")
}
}

View File

@@ -0,0 +1,196 @@
package lsutil
import (
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/astnav"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/scanner"
)
// PositionBelongsToNode returns true if the position belongs to the node.
// Assumes `candidate.Pos() <= position` holds.
func PositionBelongsToNode(candidate *ast.Node, position int, file *ast.SourceFile) bool {
if candidate.Pos() > position {
panic("Expected candidate.pos <= position")
}
return position < candidate.End() || !IsCompletedNode(candidate, file)
}
func IsCompletedNode(n *ast.Node, sourceFile *ast.SourceFile) bool {
if n == nil || ast.NodeIsMissing(n) {
return false
}
switch n.Kind {
case ast.KindClassDeclaration,
ast.KindInterfaceDeclaration,
ast.KindEnumDeclaration,
ast.KindObjectLiteralExpression,
ast.KindObjectBindingPattern,
ast.KindTypeLiteral,
ast.KindBlock,
ast.KindModuleBlock,
ast.KindCaseBlock,
ast.KindNamedImports,
ast.KindNamedExports:
return nodeEndsWith(n, ast.KindCloseBraceToken, sourceFile)
case ast.KindCatchClause:
return IsCompletedNode(n.AsCatchClause().Block, sourceFile)
case ast.KindNewExpression:
if n.ArgumentList() == nil {
return true
}
fallthrough
case ast.KindCallExpression,
ast.KindParenthesizedExpression,
ast.KindParenthesizedType:
return nodeEndsWith(n, ast.KindCloseParenToken, sourceFile)
case ast.KindFunctionType,
ast.KindConstructorType:
return IsCompletedNode(n.Type(), sourceFile)
case ast.KindConstructor,
ast.KindGetAccessor,
ast.KindSetAccessor,
ast.KindFunctionDeclaration,
ast.KindFunctionExpression,
ast.KindMethodDeclaration,
ast.KindMethodSignature,
ast.KindConstructSignature,
ast.KindCallSignature,
ast.KindArrowFunction:
if n.Body() != nil {
return IsCompletedNode(n.Body(), sourceFile)
}
if n.Type() != nil {
return IsCompletedNode(n.Type(), sourceFile)
}
// Even though type parameters can be unclosed, we can get away with
// having at least a closing paren.
return hasChildOfKind(n, ast.KindCloseParenToken, sourceFile)
case ast.KindModuleDeclaration:
return n.Body() != nil && IsCompletedNode(n.Body(), sourceFile)
case ast.KindIfStatement:
if n.AsIfStatement().ElseStatement != nil {
return IsCompletedNode(n.AsIfStatement().ElseStatement, sourceFile)
}
return IsCompletedNode(n.AsIfStatement().ThenStatement, sourceFile)
case ast.KindExpressionStatement:
return IsCompletedNode(n.Expression(), sourceFile) ||
hasChildOfKind(n, ast.KindSemicolonToken, sourceFile)
case ast.KindArrayLiteralExpression,
ast.KindArrayBindingPattern,
ast.KindElementAccessExpression,
ast.KindComputedPropertyName,
ast.KindTupleType:
return nodeEndsWith(n, ast.KindCloseBracketToken, sourceFile)
case ast.KindIndexSignature:
if n.AsIndexSignatureDeclaration().Type != nil {
return IsCompletedNode(n.AsIndexSignatureDeclaration().Type, sourceFile)
}
return hasChildOfKind(n, ast.KindCloseBracketToken, sourceFile)
case ast.KindCaseClause,
ast.KindDefaultClause:
// there is no such thing as terminator token for CaseClause/DefaultClause so for simplicity always consider them non-completed
return false
case ast.KindForStatement,
ast.KindForInStatement,
ast.KindForOfStatement,
ast.KindWhileStatement:
return IsCompletedNode(n.Statement(), sourceFile)
case ast.KindDoStatement:
// rough approximation: if DoStatement has While keyword - then if node is completed is checking the presence of ')';
if hasChildOfKind(n, ast.KindWhileKeyword, sourceFile) {
return nodeEndsWith(n, ast.KindCloseParenToken, sourceFile)
}
return IsCompletedNode(n.Statement(), sourceFile)
case ast.KindTypeQuery:
return IsCompletedNode(n.AsTypeQueryNode().ExprName, sourceFile)
case ast.KindTypeOfExpression,
ast.KindDeleteExpression,
ast.KindVoidExpression,
ast.KindYieldExpression,
ast.KindSpreadElement:
return IsCompletedNode(n.Expression(), sourceFile)
case ast.KindTaggedTemplateExpression:
return IsCompletedNode(n.AsTaggedTemplateExpression().Template, sourceFile)
case ast.KindTemplateExpression:
if n.AsTemplateExpression().TemplateSpans == nil {
return false
}
lastSpan := core.LastOrNil(n.AsTemplateExpression().TemplateSpans.Nodes)
return IsCompletedNode(lastSpan, sourceFile)
case ast.KindTemplateSpan:
return ast.NodeIsPresent(n.AsTemplateSpan().Literal)
case ast.KindExportDeclaration,
ast.KindImportDeclaration:
return ast.NodeIsPresent(n.ModuleSpecifier())
case ast.KindPrefixUnaryExpression:
return IsCompletedNode(n.AsPrefixUnaryExpression().Operand, sourceFile)
case ast.KindBinaryExpression:
return IsCompletedNode(n.AsBinaryExpression().Right, sourceFile)
case ast.KindConditionalExpression:
return IsCompletedNode(n.AsConditionalExpression().WhenFalse, sourceFile)
default:
return true
}
}
// Checks if node ends with 'expectedLastToken'.
// If child at position 'length - 1' is 'SemicolonToken' it is skipped and 'expectedLastToken' is compared with child at position 'length - 2'.
func nodeEndsWith(n *ast.Node, expectedLastToken ast.Kind, sourceFile *ast.SourceFile) bool {
lastChildNode := GetLastVisitedChild(n, sourceFile)
var lastNodeAndTokens []*ast.Node
var tokenStartPos int
if lastChildNode != nil {
lastNodeAndTokens = []*ast.Node{lastChildNode}
tokenStartPos = lastChildNode.End()
} else {
tokenStartPos = n.Pos()
}
scanner := scanner.GetScannerForSourceFile(sourceFile, tokenStartPos)
for startPos := tokenStartPos; startPos < n.End(); {
tokenKind := scanner.Token()
tokenFullStart := scanner.TokenFullStart()
tokenEnd := scanner.TokenEnd()
token := sourceFile.GetOrCreateToken(tokenKind, tokenFullStart, tokenEnd, n, scanner.TokenFlags())
lastNodeAndTokens = append(lastNodeAndTokens, token)
startPos = tokenEnd
scanner.Scan()
}
if len(lastNodeAndTokens) == 0 {
return false
}
lastChild := lastNodeAndTokens[len(lastNodeAndTokens)-1]
if lastChild.Kind == expectedLastToken {
return true
} else if lastChild.Kind == ast.KindSemicolonToken && len(lastNodeAndTokens) > 1 {
return lastNodeAndTokens[len(lastNodeAndTokens)-2].Kind == expectedLastToken
}
return false
}
func hasChildOfKind(containingNode *ast.Node, kind ast.Kind, sourceFile *ast.SourceFile) bool {
return astnav.FindChildOfKind(containingNode, kind, sourceFile) != nil
}

View File

@@ -0,0 +1,141 @@
package lsutil
import (
"strings"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
"github.com/microsoft/typescript-go/internal/printer"
)
type IndentStyle int
const (
IndentStyleNone IndentStyle = iota
IndentStyleBlock
IndentStyleSmart
)
func parseIndentStyle(v any) IndentStyle {
switch s := v.(type) {
case string:
switch strings.ToLower(s) {
case "none":
return IndentStyleNone
case "block":
return IndentStyleBlock
case "smart":
return IndentStyleSmart
}
case float64:
return IndentStyle(int(s))
case int:
return IndentStyle(s)
}
return IndentStyleSmart
}
type SemicolonPreference string
const (
SemicolonPreferenceIgnore SemicolonPreference = "ignore"
SemicolonPreferenceInsert SemicolonPreference = "insert"
SemicolonPreferenceRemove SemicolonPreference = "remove"
)
func parseSemicolonPreference(v any) SemicolonPreference {
if s, ok := v.(string); ok {
switch strings.ToLower(s) {
case "ignore":
return SemicolonPreferenceIgnore
case "insert":
return SemicolonPreferenceInsert
case "remove":
return SemicolonPreferenceRemove
}
}
return SemicolonPreferenceIgnore
}
type EditorSettings struct {
BaseIndentSize int `raw:"baseIndentSize" config:"format.baseIndentSize"`
IndentSize int `raw:"indentSize" config:"format.indentSize"`
TabSize int `raw:"tabSize" config:"format.tabSize"`
NewLineCharacter string `raw:"newLineCharacter" config:"format.newLineCharacter"`
ConvertTabsToSpaces core.Tristate `raw:"convertTabsToSpaces" config:"format.convertTabsToSpaces"`
IndentStyle IndentStyle `raw:"indentStyle" config:"format.indentStyle"`
TrimTrailingWhitespace core.Tristate `raw:"trimTrailingWhitespace" config:"format.trimTrailingWhitespace"`
}
type FormatCodeSettings struct {
EditorSettings
InsertSpaceAfterCommaDelimiter core.Tristate `raw:"insertSpaceAfterCommaDelimiter" config:"format.insertSpaceAfterCommaDelimiter"`
InsertSpaceAfterSemicolonInForStatements core.Tristate `raw:"insertSpaceAfterSemicolonInForStatements" config:"format.insertSpaceAfterSemicolonInForStatements"`
InsertSpaceBeforeAndAfterBinaryOperators core.Tristate `raw:"insertSpaceBeforeAndAfterBinaryOperators" config:"format.insertSpaceBeforeAndAfterBinaryOperators"`
InsertSpaceAfterConstructor core.Tristate `raw:"insertSpaceAfterConstructor" config:"format.insertSpaceAfterConstructor"`
InsertSpaceAfterKeywordsInControlFlowStatements core.Tristate `raw:"insertSpaceAfterKeywordsInControlFlowStatements" config:"format.insertSpaceAfterKeywordsInControlFlowStatements"`
InsertSpaceAfterFunctionKeywordForAnonymousFunctions core.Tristate `raw:"insertSpaceAfterFunctionKeywordForAnonymousFunctions" config:"format.insertSpaceAfterFunctionKeywordForAnonymousFunctions"`
InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis core.Tristate `raw:"insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis" config:"format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis"`
InsertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets core.Tristate `raw:"insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets" config:"format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets"`
InsertSpaceAfterOpeningAndBeforeClosingNonemptyBraces core.Tristate `raw:"insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces" config:"format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces"`
InsertSpaceAfterOpeningAndBeforeClosingEmptyBraces core.Tristate `raw:"insertSpaceAfterOpeningAndBeforeClosingEmptyBraces" config:"format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces"`
InsertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces core.Tristate `raw:"insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces" config:"format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces"`
InsertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces core.Tristate `raw:"insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces" config:"format.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces"`
InsertSpaceAfterTypeAssertion core.Tristate `raw:"insertSpaceAfterTypeAssertion" config:"format.insertSpaceAfterTypeAssertion"`
InsertSpaceBeforeFunctionParenthesis core.Tristate `raw:"insertSpaceBeforeFunctionParenthesis" config:"format.insertSpaceBeforeFunctionParenthesis"`
PlaceOpenBraceOnNewLineForFunctions core.Tristate `raw:"placeOpenBraceOnNewLineForFunctions" config:"format.placeOpenBraceOnNewLineForFunctions"`
PlaceOpenBraceOnNewLineForControlBlocks core.Tristate `raw:"placeOpenBraceOnNewLineForControlBlocks" config:"format.placeOpenBraceOnNewLineForControlBlocks"`
InsertSpaceBeforeTypeAnnotation core.Tristate `raw:"insertSpaceBeforeTypeAnnotation" config:"format.insertSpaceBeforeTypeAnnotation"`
IndentMultiLineObjectLiteralBeginningOnBlankLine core.Tristate `raw:"indentMultiLineObjectLiteralBeginningOnBlankLine" config:"format.indentMultiLineObjectLiteralBeginningOnBlankLine"`
Semicolons SemicolonPreference `raw:"semicolons" config:"format.semicolons"`
IndentSwitchCase core.Tristate `raw:"indentSwitchCase" config:"format.indentSwitchCase"`
}
func FromLSFormatOptions(f FormatCodeSettings, opt *lsproto.FormattingOptions) FormatCodeSettings {
updatedSettings := f
updatedSettings.TabSize = int(opt.TabSize)
updatedSettings.IndentSize = int(opt.TabSize)
updatedSettings.ConvertTabsToSpaces = core.BoolToTristate(opt.InsertSpaces)
if opt.TrimTrailingWhitespace != nil {
updatedSettings.TrimTrailingWhitespace = core.BoolToTristate(*opt.TrimTrailingWhitespace)
}
return updatedSettings
}
func (settings FormatCodeSettings) ToLSFormatOptions() *lsproto.FormattingOptions {
trimTrailingWhitespace := settings.TrimTrailingWhitespace.IsTrue()
return &lsproto.FormattingOptions{
TabSize: uint32(settings.TabSize),
InsertSpaces: settings.ConvertTabsToSpaces.IsTrue(),
TrimTrailingWhitespace: &trimTrailingWhitespace,
}
}
func GetDefaultFormatCodeSettings() FormatCodeSettings {
return FormatCodeSettings{
EditorSettings: EditorSettings{
IndentSize: printer.GetDefaultIndentSize(),
TabSize: printer.GetDefaultIndentSize(),
NewLineCharacter: "\n",
ConvertTabsToSpaces: core.TSTrue,
IndentStyle: IndentStyleSmart,
TrimTrailingWhitespace: core.TSTrue,
},
InsertSpaceAfterConstructor: core.TSFalse,
InsertSpaceAfterCommaDelimiter: core.TSTrue,
InsertSpaceAfterSemicolonInForStatements: core.TSTrue,
InsertSpaceBeforeAndAfterBinaryOperators: core.TSTrue,
InsertSpaceAfterKeywordsInControlFlowStatements: core.TSTrue,
InsertSpaceAfterFunctionKeywordForAnonymousFunctions: core.TSFalse,
InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: core.TSFalse,
InsertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: core.TSFalse,
InsertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: core.TSTrue,
InsertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: core.TSFalse,
InsertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces: core.TSFalse,
InsertSpaceBeforeFunctionParenthesis: core.TSFalse,
PlaceOpenBraceOnNewLineForFunctions: core.TSFalse,
PlaceOpenBraceOnNewLineForControlBlocks: core.TSFalse,
Semicolons: SemicolonPreferenceIgnore,
IndentSwitchCase: core.TSTrue,
}
}

View File

@@ -0,0 +1,695 @@
package lsutil
import (
"cmp"
"math"
"strings"
"unicode"
"unicode/utf8"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/stringutil"
"github.com/microsoft/typescript-go/internal/tspath"
"golang.org/x/text/unicode/norm"
)
// FilterImportDeclarations filters out non-import declarations from a list of statements.
func FilterImportDeclarations(statements []*ast.Statement) []*ast.Statement {
return core.Filter(statements, func(stmt *ast.Statement) bool {
return stmt.Kind == ast.KindImportDeclaration
})
}
// GetDetectionLists returns the lists of comparers and type orders to test for organize imports detection.
func GetDetectionLists(preferences UserPreferences) (comparersToTest []func(a, b string) int, typeOrdersToTest []OrganizeImportsTypeOrder) {
if preferences.OrganizeImportsSort != OrganizeImportsSortAuto {
comparersToTest = []func(a, b string) int{getOrganizeImportsPresetStringComparer(preferences.OrganizeImportsSort)}
} else if !preferences.OrganizeImportsIgnoreCase.IsUnknown() {
comparersToTest = []func(a, b string) int{getOrganizeImportsStringComparer(preferences, preferences.OrganizeImportsIgnoreCase.IsTrue())}
} else {
comparersToTest = []func(a, b string) int{
getOrganizeImportsStringComparer(preferences, true),
getOrganizeImportsStringComparer(preferences, false),
}
}
if preferences.OrganizeImportsTypeOrder != OrganizeImportsTypeOrderAuto {
typeOrdersToTest = []OrganizeImportsTypeOrder{preferences.OrganizeImportsTypeOrder}
} else {
typeOrdersToTest = []OrganizeImportsTypeOrder{
OrganizeImportsTypeOrderLast,
OrganizeImportsTypeOrderInline,
OrganizeImportsTypeOrderFirst,
}
}
return comparersToTest, typeOrdersToTest
}
func ResolveOrganizeImportsSort(preferences UserPreferences) OrganizeImportsSort {
if preferences.OrganizeImportsSort != OrganizeImportsSortAuto {
return preferences.OrganizeImportsSort
}
if preferences.OrganizeImportsCollation == OrganizeImportsCollationUnicode {
switch preferences.OrganizeImportsIgnoreCase {
case core.TSTrue:
return OrganizeImportsSortNaturalIgnoreCase
case core.TSFalse:
return OrganizeImportsSortNatural
default:
return OrganizeImportsSortAuto
}
}
switch preferences.OrganizeImportsIgnoreCase {
case core.TSTrue:
return OrganizeImportsSortOrdinalIgnoreCase
case core.TSFalse:
return OrganizeImportsSortOrdinal
default:
return OrganizeImportsSortAuto
}
}
func getOrganizeImportsOrdinalStringComparer(ignoreCase bool) func(a, b string) int {
if ignoreCase {
return stringutil.CompareStringsCaseInsensitiveEslintCompatible
}
return stringutil.CompareStringsCaseSensitive
}
func getOrganizeImportsNaturalStringComparer(caseSensitive bool) func(a, b string) int {
return func(a, b string) int {
return compareOrganizeImportsNaturalStrings(a, b, caseSensitive)
}
}
func getOrganizeImportsUnicodeStringComparer(ignoreCase bool, preferences UserPreferences) func(a, b string) int {
caseFirst := preferences.OrganizeImportsCaseFirst
numeric := preferences.OrganizeImportsNumericCollation.IsTrue()
accents := !preferences.OrganizeImportsAccentCollation.IsFalse()
return func(a, b string) int {
return compareOrganizeImportsUnicodeStrings(a, b, ignoreCase, caseFirst, numeric, accents)
}
}
func compareOrganizeImportsNaturalStrings(a string, b string, caseSensitive bool) int {
if cmp := compareStringsNumeric(naturalCollationKey(a), naturalCollationKey(b)); cmp != 0 {
return cmp
}
if caseSensitive {
if cmp := compareOrganizeImportsCaseUpperFirst(a, b); cmp != 0 {
return cmp
}
}
return strings.Compare(a, b)
}
func compareOrganizeImportsUnicodeStrings(a string, b string, ignoreCase bool, caseFirst OrganizeImportsCaseFirst, numeric bool, accents bool) int {
if cmp := compareOrganizeImportsUnicodeKeys(naturalCollationKey(a), naturalCollationKey(b), numeric); cmp != 0 {
return cmp
}
if accents {
if cmp := compareOrganizeImportsUnicodeKeys(strings.ToLower(a), strings.ToLower(b), numeric); cmp != 0 {
return cmp
}
}
if !ignoreCase {
if cmp := compareOrganizeImportsCase(a, b, caseFirst); cmp != 0 {
return cmp
}
}
return strings.Compare(a, b)
}
func naturalCollationKey(s string) string {
return strings.ToLower(removeDiacritics(s))
}
func removeDiacritics(s string) string {
return strings.Map(func(r rune) rune {
if unicode.Is(unicode.Mn, r) {
return -1
}
return r
}, norm.NFD.String(s))
}
func compareOrganizeImportsUnicodeKeys(a string, b string, numeric bool) int {
if numeric {
return compareStringsNumeric(a, b)
}
return strings.Compare(a, b)
}
func compareStringsNumeric(a string, b string) int {
for len(a) > 0 && len(b) > 0 {
if isASCIIDigit(a[0]) && isASCIIDigit(b[0]) {
aRunEnd := asciiDigitRunEnd(a)
bRunEnd := asciiDigitRunEnd(b)
if cmp := compareNumericText(a[:aRunEnd], b[:bRunEnd]); cmp != 0 {
return cmp
}
a = a[aRunEnd:]
b = b[bRunEnd:]
continue
}
aRune, aSize := utf8.DecodeRuneInString(a)
bRune, bSize := utf8.DecodeRuneInString(b)
if aRune != bRune {
return cmp.Compare(aRune, bRune)
}
a = a[aSize:]
b = b[bSize:]
}
return cmp.Compare(len(a), len(b))
}
func isASCIIDigit(ch byte) bool {
return ch >= '0' && ch <= '9'
}
func asciiDigitRunEnd(s string) int {
i := 0
for i < len(s) && isASCIIDigit(s[i]) {
i++
}
return i
}
func compareNumericText(a string, b string) int {
aDigits := strings.TrimLeft(a, "0")
bDigits := strings.TrimLeft(b, "0")
if aDigits == "" {
aDigits = "0"
}
if bDigits == "" {
bDigits = "0"
}
if len(aDigits) != len(bDigits) {
return cmp.Compare(len(aDigits), len(bDigits))
}
if cmp := strings.Compare(aDigits, bDigits); cmp != 0 {
return cmp
}
return strings.Compare(a, b)
}
func compareOrganizeImportsCaseUpperFirst(a string, b string) int {
return compareOrganizeImportsCase(a, b, OrganizeImportsCaseFirstUpper)
}
func compareOrganizeImportsCase(a string, b string, caseFirst OrganizeImportsCaseFirst) int {
aRunes := []rune(a)
bRunes := []rune(b)
minLen := min(len(aRunes), len(bRunes))
for i := range minLen {
aUpper := unicode.IsUpper(aRunes[i])
bUpper := unicode.IsUpper(bRunes[i])
if aUpper != bUpper {
switch caseFirst {
case OrganizeImportsCaseFirstUpper:
if aUpper {
return -1
}
return 1
case OrganizeImportsCaseFirstLower:
if !aUpper {
return -1
}
return 1
default:
if aUpper {
return 1
}
return -1
}
}
}
return cmp.Compare(len(aRunes), len(bRunes))
}
func getOrganizeImportsPresetStringComparer(sort OrganizeImportsSort) func(a, b string) int {
switch sort {
case OrganizeImportsSortOrdinalIgnoreCase:
return getOrganizeImportsOrdinalStringComparer(true)
case OrganizeImportsSortNatural:
return getOrganizeImportsNaturalStringComparer(true)
case OrganizeImportsSortNaturalIgnoreCase:
return getOrganizeImportsNaturalStringComparer(false)
default:
return getOrganizeImportsOrdinalStringComparer(false)
}
}
func getOrganizeImportsStringComparer(preferences UserPreferences, ignoreCase bool) func(a, b string) int {
if preferences.OrganizeImportsSort != OrganizeImportsSortAuto {
return getOrganizeImportsPresetStringComparer(preferences.OrganizeImportsSort)
}
if preferences.OrganizeImportsCollation == OrganizeImportsCollationUnicode {
return getOrganizeImportsUnicodeStringComparer(ignoreCase, preferences)
}
return getOrganizeImportsOrdinalStringComparer(ignoreCase)
}
func getModuleSpecifierExpression(declaration *ast.Statement) *ast.Expression {
switch declaration.Kind {
case ast.KindImportEqualsDeclaration:
importEquals := declaration.AsImportEqualsDeclaration()
if importEquals.ModuleReference.Kind == ast.KindExternalModuleReference {
return importEquals.ModuleReference.Expression()
}
return nil
case ast.KindImportDeclaration:
return declaration.ModuleSpecifier()
case ast.KindVariableStatement:
declarations := declaration.AsVariableStatement().DeclarationList.AsVariableDeclarationList().Declarations.Nodes
if len(declarations) > 0 {
initializer := declarations[0].Initializer()
if initializer != nil && initializer.Kind == ast.KindCallExpression {
callExpr := initializer.AsCallExpression()
if len(callExpr.Arguments.Nodes) > 0 {
return callExpr.Arguments.Nodes[0]
}
}
}
return nil
default:
return nil
}
}
// GetExternalModuleName returns the module name from a module specifier expression.
func GetExternalModuleName(specifier *ast.Expression) string {
if specifier != nil && ast.IsStringLiteralLike(specifier.AsNode()) {
return specifier.Text()
}
return ""
}
// CompareModuleSpecifiers compares two module specifiers using the given comparer.
func CompareModuleSpecifiers(m1 *ast.Expression, m2 *ast.Expression, comparer func(a, b string) int) int {
name1 := GetExternalModuleName(m1)
name2 := GetExternalModuleName(m2)
if cmp := core.CompareBooleans(name1 == "", name2 == ""); cmp != 0 {
return cmp
}
if cmp := core.CompareBooleans(tspath.IsExternalModuleNameRelative(name1), tspath.IsExternalModuleNameRelative(name2)); cmp != 0 {
return cmp
}
return comparer(name1, name2)
}
func compareImportKind(s1 *ast.Statement, s2 *ast.Statement) int {
return cmp.Compare(getImportKindOrder(s1), getImportKindOrder(s2))
}
// getImportKindOrder returns the sort order for different import kinds:
// 1. Side-effect imports
// 2. Type-only imports
// 3. Namespace imports
// 4. Default imports
// 5. Named imports
// 6. ImportEqualsDeclarations
// 7. Require variable statements
const (
importKindOrderSideEffect = 0
importKindOrderTypeOnly = 1
importKindOrderNamespace = 2
importKindOrderDefault = 3
importKindOrderNamed = 4
importKindOrderImportEquals = 5
importKindOrderRequire = 6
importKindOrderUnknown = 7
)
func getImportKindOrder(s1 *ast.Statement) int {
switch s1.Kind {
case ast.KindImportDeclaration:
importDecl := s1.AsImportDeclaration()
if importDecl.ImportClause == nil {
return importKindOrderSideEffect
}
importClause := importDecl.ImportClause.AsImportClause()
if importClause.IsTypeOnly() {
return importKindOrderTypeOnly
}
if importClause.NamedBindings != nil && importClause.NamedBindings.Kind == ast.KindNamespaceImport {
return importKindOrderNamespace
}
if importClause.Name() != nil {
return importKindOrderDefault
}
return importKindOrderNamed
case ast.KindImportEqualsDeclaration:
return importKindOrderImportEquals
case ast.KindVariableStatement:
return importKindOrderRequire
default:
return importKindOrderUnknown
}
}
// CompareImportsOrRequireStatements compares two import or require statements.
func CompareImportsOrRequireStatements(s1 *ast.Statement, s2 *ast.Statement, comparer func(a, b string) int) int {
if cmp := CompareModuleSpecifiers(getModuleSpecifierExpression(s1), getModuleSpecifierExpression(s2), comparer); cmp != 0 {
return cmp
}
return compareImportKind(s1, s2)
}
func compareImportOrExportSpecifiers(s1 *ast.Node, s2 *ast.Node, comparer func(a, b string) int, preferences UserPreferences) int {
typeOrder := preferences.OrganizeImportsTypeOrder
s1Name := s1.Name().Text()
s2Name := s2.Name().Text()
switch typeOrder {
case OrganizeImportsTypeOrderFirst:
if cmp := core.CompareBooleans(s2.IsTypeOnly(), s1.IsTypeOnly()); cmp != 0 {
return cmp
}
return comparer(s1Name, s2Name)
case OrganizeImportsTypeOrderInline:
return comparer(s1Name, s2Name)
default: // OrganizeImportsTypeOrderLast
if cmp := core.CompareBooleans(s1.IsTypeOnly(), s2.IsTypeOnly()); cmp != 0 {
return cmp
}
return comparer(s1Name, s2Name)
}
}
// GetNamedImportSpecifierComparer returns a comparer function for sorting import specifiers.
func GetNamedImportSpecifierComparer(preferences UserPreferences, comparer func(a, b string) int) func(s1, s2 *ast.Node) int {
if comparer == nil {
ignoreCase := false
if !preferences.OrganizeImportsIgnoreCase.IsUnknown() {
ignoreCase = preferences.OrganizeImportsIgnoreCase.IsTrue()
}
comparer = getOrganizeImportsStringComparer(preferences, ignoreCase)
}
return func(s1, s2 *ast.Node) int {
return compareImportOrExportSpecifiers(s1, s2, comparer, preferences)
}
}
// GetImportSpecifierInsertionIndex returns the index at which to insert a new import specifier.
func GetImportSpecifierInsertionIndex(sortedImports []*ast.Node, newImport *ast.Node, comparer func(s1, s2 *ast.Node) int) int {
return core.FirstResult(core.BinarySearchUniqueFunc(sortedImports, func(mid int, value *ast.Node) int {
return comparer(value, newImport)
}))
}
// GetImportDeclarationInsertIndex returns the index at which to insert a new import declaration.
func GetImportDeclarationInsertIndex(sortedImports []*ast.Statement, newImport *ast.Statement, comparer func(a, b *ast.Statement) int) int {
return core.FirstResult(core.BinarySearchUniqueFunc(sortedImports, func(mid int, value *ast.Statement) int {
return comparer(value, newImport)
}))
}
// GetOrganizeImportsStringComparerWithDetection returns a string comparer based on detecting the order of import statements by the module specifier
func GetOrganizeImportsStringComparerWithDetection(originalImportDecls []*ast.Statement, preferences UserPreferences) (comparer func(a, b string) int, isSorted bool) {
result, sorted := DetectModuleSpecifierCaseBySort([][]*ast.Statement{originalImportDecls}, getComparers(preferences))
return result, sorted
}
func getComparers(preferences UserPreferences) []func(a string, b string) int {
if preferences.OrganizeImportsSort != OrganizeImportsSortAuto || !preferences.OrganizeImportsIgnoreCase.IsUnknown() {
ignoreCase := false
if !preferences.OrganizeImportsIgnoreCase.IsUnknown() {
ignoreCase = preferences.OrganizeImportsIgnoreCase.IsTrue()
}
return []func(a, b string) int{getOrganizeImportsStringComparer(preferences, ignoreCase)}
}
return []func(a, b string) int{
getOrganizeImportsStringComparer(preferences, true),
getOrganizeImportsStringComparer(preferences, false),
}
}
type namedImportSortResult struct {
namedImportComparer func(a, b string) int
typeOrder OrganizeImportsTypeOrder
isSorted bool
}
// DetectNamedImportOrganizationBySort detects the order of named imports throughout the file by considering the named imports in each statement as a group
func DetectNamedImportOrganizationBySort(
originalGroups []*ast.Statement,
comparersToTest []func(a, b string) int,
typesToTest []OrganizeImportsTypeOrder,
) (comparer func(a, b string) int, typeOrder OrganizeImportsTypeOrder, found bool) {
result := detectNamedImportOrganizationBySort(originalGroups, comparersToTest, typesToTest)
if result == nil {
return nil, OrganizeImportsTypeOrderLast, false
}
return result.namedImportComparer, result.typeOrder, true
}
func detectNamedImportOrganizationBySort(
originalGroups []*ast.Statement,
comparersToTest []func(a, b string) int,
typesToTest []OrganizeImportsTypeOrder,
) *namedImportSortResult {
var bothNamedImports bool
var importDeclsWithNamed []*ast.Statement
for _, imp := range originalGroups {
if imp.AsImportDeclaration().ImportClause == nil {
continue
}
clause := imp.AsImportDeclaration().ImportClause.AsImportClause()
if clause.NamedBindings == nil || clause.NamedBindings.Kind != ast.KindNamedImports {
continue
}
namedImports := clause.NamedBindings.AsNamedImports()
if len(namedImports.Elements.Nodes) == 0 {
continue
}
if !bothNamedImports {
hasTypeOnly := false
hasRegular := false
for _, elem := range namedImports.Elements.Nodes {
if elem.IsTypeOnly() {
hasTypeOnly = true
} else {
hasRegular = true
}
}
if hasTypeOnly && hasRegular {
bothNamedImports = true
}
}
importDeclsWithNamed = append(importDeclsWithNamed, imp)
}
if len(importDeclsWithNamed) == 0 {
return nil
}
namedImportsByDecl := make([][]*ast.Statement, 0, len(importDeclsWithNamed))
for _, imp := range importDeclsWithNamed {
clause := imp.AsImportDeclaration().ImportClause.AsImportClause()
namedImports := clause.NamedBindings.AsNamedImports()
namedImportsByDecl = append(namedImportsByDecl, namedImports.Elements.Nodes)
}
if !bothNamedImports || len(typesToTest) == 0 {
namesList := make([][]string, len(namedImportsByDecl))
for i, imports := range namedImportsByDecl {
names := make([]string, len(imports))
for j, imp := range imports {
names[j] = imp.Name().Text()
}
namesList[i] = names
}
sortState := detectCaseSensitivityBySort(namesList, comparersToTest)
typeOrder := OrganizeImportsTypeOrderLast
if len(typesToTest) == 1 {
typeOrder = typesToTest[0]
}
return &namedImportSortResult{
namedImportComparer: sortState.comparer,
typeOrder: typeOrder,
isSorted: sortState.isSorted,
}
}
bestDiff := map[OrganizeImportsTypeOrder]int{
OrganizeImportsTypeOrderFirst: math.MaxInt,
OrganizeImportsTypeOrderLast: math.MaxInt,
OrganizeImportsTypeOrderInline: math.MaxInt,
}
bestComparer := map[OrganizeImportsTypeOrder]func(a, b string) int{
OrganizeImportsTypeOrderFirst: comparersToTest[0],
OrganizeImportsTypeOrderLast: comparersToTest[0],
OrganizeImportsTypeOrderInline: comparersToTest[0],
}
for _, curComparer := range comparersToTest {
currDiff := map[OrganizeImportsTypeOrder]int{
OrganizeImportsTypeOrderFirst: 0,
OrganizeImportsTypeOrderLast: 0,
OrganizeImportsTypeOrderInline: 0,
}
for _, importDecl := range namedImportsByDecl {
for _, typeOrder := range typesToTest {
prefs := UserPreferences{OrganizeImportsTypeOrder: typeOrder}
diff := measureSortedness(importDecl, func(n1, n2 *ast.Node) int {
return compareImportOrExportSpecifiers(n1, n2, curComparer, prefs)
})
currDiff[typeOrder] = currDiff[typeOrder] + diff
}
}
for _, typeOrder := range typesToTest {
if currDiff[typeOrder] < bestDiff[typeOrder] {
bestDiff[typeOrder] = currDiff[typeOrder]
bestComparer[typeOrder] = curComparer
}
}
}
for _, bestTypeOrder := range typesToTest {
isBest := true
for _, testTypeOrder := range typesToTest {
if bestDiff[testTypeOrder] < bestDiff[bestTypeOrder] {
isBest = false
break
}
}
if isBest {
return &namedImportSortResult{
namedImportComparer: bestComparer[bestTypeOrder],
typeOrder: bestTypeOrder,
isSorted: bestDiff[bestTypeOrder] == 0,
}
}
}
return &namedImportSortResult{
namedImportComparer: bestComparer[OrganizeImportsTypeOrderLast],
typeOrder: OrganizeImportsTypeOrderLast,
isSorted: bestDiff[OrganizeImportsTypeOrderLast] == 0,
}
}
type caseSensitivityDetectionResult struct {
comparer func(a, b string) int
isSorted bool
}
// DetectModuleSpecifierCaseBySort detects the order of module specifiers based on import statements throughout the module/file
func DetectModuleSpecifierCaseBySort(importDeclsByGroup [][]*ast.Statement, comparersToTest []func(a, b string) int) (comparer func(a, b string) int, isSorted bool) {
moduleSpecifiersByGroup := make([][]string, 0, len(importDeclsByGroup))
for _, importGroup := range importDeclsByGroup {
moduleNames := make([]string, 0, len(importGroup))
for _, decl := range importGroup {
if expr := getModuleSpecifierExpression(decl); expr != nil {
moduleNames = append(moduleNames, GetExternalModuleName(expr))
} else {
moduleNames = append(moduleNames, "")
}
}
moduleSpecifiersByGroup = append(moduleSpecifiersByGroup, moduleNames)
}
result := detectCaseSensitivityBySort(moduleSpecifiersByGroup, comparersToTest)
return result.comparer, result.isSorted
}
func detectCaseSensitivityBySort(originalGroups [][]string, comparersToTest []func(a, b string) int) caseSensitivityDetectionResult {
var bestComparer func(a, b string) int
bestDiff := math.MaxInt
for _, curComparer := range comparersToTest {
diffOfCurrentComparer := 0
for _, listToSort := range originalGroups {
if len(listToSort) <= 1 {
continue
}
diff := measureSortedness(listToSort, curComparer)
diffOfCurrentComparer += diff
}
if diffOfCurrentComparer < bestDiff {
bestDiff = diffOfCurrentComparer
bestComparer = curComparer
}
}
if bestComparer == nil && len(comparersToTest) > 0 {
bestComparer = comparersToTest[0]
}
return caseSensitivityDetectionResult{
comparer: bestComparer,
isSorted: bestDiff == 0,
}
}
func measureSortedness[T any](arr []T, comparer func(a, b T) int) int {
i := 0
for j := range len(arr) - 1 {
if comparer(arr[j], arr[j+1]) > 0 {
i++
}
}
return i
}
// GetNamedImportSpecifierComparerWithDetection returns a specifier comparer based on detecting the existing sort order within a single import statement
func GetNamedImportSpecifierComparerWithDetection(importDecl *ast.Node, sourceFile *ast.SourceFile, preferences UserPreferences) (specifierComparer func(s1, s2 *ast.Node) int, isSorted core.Tristate) {
comparersToTest, typeOrdersToTest := GetDetectionLists(preferences)
var importStmt *ast.Statement
if importDecl.Kind == ast.KindImportDeclaration {
importStmt = importDecl
}
specifierComparer = GetNamedImportSpecifierComparer(preferences, comparersToTest[0])
isSorted = core.TSUnknown
if (ResolveOrganizeImportsSort(preferences) == OrganizeImportsSortAuto || preferences.OrganizeImportsTypeOrder == OrganizeImportsTypeOrderAuto) && importStmt != nil {
detectFromDecl := detectNamedImportOrganizationBySort([]*ast.Statement{importStmt}, comparersToTest, typeOrdersToTest)
if detectFromDecl != nil {
isSorted = core.BoolToTristate(detectFromDecl.isSorted)
specifierComparer = GetNamedImportSpecifierComparer(
UserPreferences{OrganizeImportsTypeOrder: detectFromDecl.typeOrder},
detectFromDecl.namedImportComparer,
)
} else if sourceFile != nil {
allImports := FilterImportDeclarations(sourceFile.Statements.Nodes)
detectFromFile := detectNamedImportOrganizationBySort(allImports, comparersToTest, typeOrdersToTest)
if detectFromFile != nil {
isSorted = core.BoolToTristate(detectFromFile.isSorted)
specifierComparer = GetNamedImportSpecifierComparer(
UserPreferences{OrganizeImportsTypeOrder: detectFromFile.typeOrder},
detectFromFile.namedImportComparer,
)
}
}
}
return specifierComparer, isSorted
}

View File

@@ -0,0 +1,438 @@
package lsutil
import (
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/checker"
"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/core"
)
type ScriptElementKind int
const (
ScriptElementKindUnknown ScriptElementKind = iota
ScriptElementKindWarning
// predefined type (void) or keyword (class)
ScriptElementKindKeyword
// top level script node
ScriptElementKindScriptElement
// module foo {}
ScriptElementKindModuleElement
// class X {}
ScriptElementKindClassElement
// var x = class X {}
ScriptElementKindLocalClassElement
// interface Y {}
ScriptElementKindInterfaceElement
// type T = ...
ScriptElementKindTypeElement
// enum E {}
ScriptElementKindEnumElement
ScriptElementKindEnumMemberElement
// Inside module and script only.
// const v = ...
ScriptElementKindVariableElement
// Inside function.
ScriptElementKindLocalVariableElement
// using foo = ...
ScriptElementKindVariableUsingElement
// await using foo = ...
ScriptElementKindVariableAwaitUsingElement
// Inside module and script only.
// function f() {}
ScriptElementKindFunctionElement
// Inside function.
ScriptElementKindLocalFunctionElement
// class X { [public|private]* foo() {} }
ScriptElementKindMemberFunctionElement
// class X { [public|private]* [get|set] foo:number; }
ScriptElementKindMemberGetAccessorElement
ScriptElementKindMemberSetAccessorElement
// class X { [public|private]* foo:number; }
// interface Y { foo:number; }
ScriptElementKindMemberVariableElement
// class X { [public|private]* accessor foo: number; }
ScriptElementKindMemberAccessorVariableElement
// class X { constructor() { } }
// class X { static { } }
ScriptElementKindConstructorImplementationElement
// interface Y { ():number; }
ScriptElementKindCallSignatureElement
// interface Y { []:number; }
ScriptElementKindIndexSignatureElement
// interface Y { new():Y; }
ScriptElementKindConstructSignatureElement
// function foo(*Y*: string)
ScriptElementKindParameterElement
ScriptElementKindTypeParameterElement
ScriptElementKindPrimitiveType
ScriptElementKindLabel
ScriptElementKindAlias
ScriptElementKindConstElement
ScriptElementKindLetElement
ScriptElementKindDirectory
ScriptElementKindExternalModuleName
// String literal
ScriptElementKindString
// Jsdoc @link: in `{@link C link text}`, the before and after text "{@link " and "}"
ScriptElementKindLink
// Jsdoc @link: in `{@link C link text}`, the entity name "C"
ScriptElementKindLinkName
// Jsdoc @link: in `{@link C link text}`, the link text "link text"
ScriptElementKindLinkText
)
type ScriptElementKindModifier uint32
const (
ScriptElementKindModifierNone ScriptElementKindModifier = 0
ScriptElementKindModifierPublic ScriptElementKindModifier = 1 << iota
ScriptElementKindModifierPrivate
ScriptElementKindModifierProtected
ScriptElementKindModifierExported
ScriptElementKindModifierAmbient
ScriptElementKindModifierStatic
ScriptElementKindModifierAbstract
ScriptElementKindModifierOptional
ScriptElementKindModifierDeprecated
ScriptElementKindModifierDts
ScriptElementKindModifierTs
ScriptElementKindModifierTsx
ScriptElementKindModifierJs
ScriptElementKindModifierJsx
ScriptElementKindModifierJson
ScriptElementKindModifierDmts
ScriptElementKindModifierMts
ScriptElementKindModifierMjs
ScriptElementKindModifierDcts
ScriptElementKindModifierCts
ScriptElementKindModifierCjs
)
var scriptElementKindModifierNames = []struct {
flag ScriptElementKindModifier
name string
}{
{ScriptElementKindModifierPublic, "public"},
{ScriptElementKindModifierPrivate, "private"},
{ScriptElementKindModifierProtected, "protected"},
{ScriptElementKindModifierExported, "export"},
{ScriptElementKindModifierAmbient, "declare"},
{ScriptElementKindModifierStatic, "static"},
{ScriptElementKindModifierAbstract, "abstract"},
{ScriptElementKindModifierOptional, "optional"},
{ScriptElementKindModifierDeprecated, "deprecated"},
{ScriptElementKindModifierDts, ".d.ts"},
{ScriptElementKindModifierTs, ".ts"},
{ScriptElementKindModifierTsx, ".tsx"},
{ScriptElementKindModifierJs, ".js"},
{ScriptElementKindModifierJsx, ".jsx"},
{ScriptElementKindModifierJson, ".json"},
{ScriptElementKindModifierDmts, ".d.mts"},
{ScriptElementKindModifierMts, ".mts"},
{ScriptElementKindModifierMjs, ".mjs"},
{ScriptElementKindModifierDcts, ".d.cts"},
{ScriptElementKindModifierCts, ".cts"},
{ScriptElementKindModifierCjs, ".cjs"},
}
func (m ScriptElementKindModifier) Strings() collections.Set[string] {
result := collections.Set[string]{}
for _, entry := range scriptElementKindModifierNames {
if m&entry.flag != 0 {
result.Add(entry.name)
}
}
return result
}
var FileExtensionKindModifiers = ScriptElementKindModifierDts |
ScriptElementKindModifierTs |
ScriptElementKindModifierTsx |
ScriptElementKindModifierJs |
ScriptElementKindModifierJsx |
ScriptElementKindModifierJson |
ScriptElementKindModifierDmts |
ScriptElementKindModifierMts |
ScriptElementKindModifierMjs |
ScriptElementKindModifierDcts |
ScriptElementKindModifierCts |
ScriptElementKindModifierCjs
func GetSymbolKind(typeChecker *checker.Checker, symbol *ast.Symbol, location *ast.Node) ScriptElementKind {
result := getSymbolKindOfConstructorPropertyMethodAccessorFunctionOrVar(typeChecker, symbol, location)
if result != ScriptElementKindUnknown {
return result
}
flags := symbol.CombinedLocalAndExportSymbolFlags()
if flags&ast.SymbolFlagsClass != 0 {
decl := ast.GetDeclarationOfKind(symbol, ast.KindClassExpression)
if decl != nil {
return ScriptElementKindLocalClassElement
}
return ScriptElementKindClassElement
}
if flags&ast.SymbolFlagsEnum != 0 {
return ScriptElementKindEnumElement
}
if flags&ast.SymbolFlagsTypeAlias != 0 {
return ScriptElementKindTypeElement
}
if flags&ast.SymbolFlagsInterface != 0 {
return ScriptElementKindInterfaceElement
}
if flags&ast.SymbolFlagsTypeParameter != 0 {
return ScriptElementKindTypeParameterElement
}
if flags&ast.SymbolFlagsEnumMember != 0 {
return ScriptElementKindEnumMemberElement
}
if flags&ast.SymbolFlagsAlias != 0 {
return ScriptElementKindAlias
}
if flags&ast.SymbolFlagsModule != 0 {
return ScriptElementKindModuleElement
}
return ScriptElementKindUnknown
}
func getSymbolKindOfConstructorPropertyMethodAccessorFunctionOrVar(typeChecker *checker.Checker, symbol *ast.Symbol, location *ast.Node) ScriptElementKind {
var roots []*ast.Symbol
if typeChecker != nil {
roots = typeChecker.GetRootSymbols(symbol)
} else {
roots = []*ast.Symbol{symbol}
}
// If this is a method from a mapped type, leave as a method so long as it still has a call signature, as opposed to e.g.
// `{ [K in keyof I]: number }`.
if len(roots) == 1 &&
roots[0].Flags&ast.SymbolFlagsMethod != 0 &&
(typeChecker == nil || len(typeChecker.GetCallSignatures(typeChecker.GetNonNullableType(typeChecker.GetTypeOfSymbolAtLocation(symbol, location)))) > 0) {
return ScriptElementKindMemberFunctionElement
}
if typeChecker != nil {
if typeChecker.IsUndefinedSymbol(symbol) {
return ScriptElementKindVariableElement
}
if typeChecker.IsArgumentsSymbol(symbol) {
return ScriptElementKindLocalVariableElement
}
if location.Kind == ast.KindThisKeyword && ast.IsExpression(location) ||
ast.IsThisInTypeQuery(location) {
return ScriptElementKindParameterElement
}
}
flags := symbol.CombinedLocalAndExportSymbolFlags()
if flags&ast.SymbolFlagsVariable != 0 {
if isFirstDeclarationOfSymbolParameter(symbol) {
return ScriptElementKindParameterElement
} else if symbol.ValueDeclaration != nil && ast.IsVarConst(symbol.ValueDeclaration) {
return ScriptElementKindConstElement
} else if symbol.ValueDeclaration != nil && ast.IsVarUsing(symbol.ValueDeclaration) {
return ScriptElementKindVariableUsingElement
} else if symbol.ValueDeclaration != nil && ast.IsVarAwaitUsing(symbol.ValueDeclaration) {
return ScriptElementKindVariableAwaitUsingElement
} else if core.Some(symbol.Declarations, ast.IsLet) {
return ScriptElementKindLetElement
}
if isLocalVariableOrFunction(symbol) {
return ScriptElementKindLocalVariableElement
}
return ScriptElementKindVariableElement
}
if flags&ast.SymbolFlagsFunction != 0 {
if isLocalVariableOrFunction(symbol) {
return ScriptElementKindLocalFunctionElement
}
return ScriptElementKindFunctionElement
}
// FIXME: getter and setter use the same symbol. And it is rare to use only setter without getter, so in most cases the symbol always has getter flag.
// So, even when the location is just on the declaration of setter, this function returns getter.
if flags&ast.SymbolFlagsGetAccessor != 0 {
return ScriptElementKindMemberGetAccessorElement
}
if flags&ast.SymbolFlagsSetAccessor != 0 {
return ScriptElementKindMemberSetAccessorElement
}
if flags&ast.SymbolFlagsMethod != 0 {
return ScriptElementKindMemberFunctionElement
}
if flags&ast.SymbolFlagsConstructor != 0 {
return ScriptElementKindConstructorImplementationElement
}
if flags&ast.SymbolFlagsSignature != 0 {
return ScriptElementKindIndexSignatureElement
}
if flags&ast.SymbolFlagsProperty != 0 {
if typeChecker != nil && flags&ast.SymbolFlagsTransient != 0 && symbol.CheckFlags&ast.CheckFlagsSynthetic != 0 {
// If union property is result of union of non method (property/accessors/variables), it is labeled as property
var unionPropertyKind ScriptElementKind
for _, rootSymbol := range roots {
if rootSymbol.Flags&(ast.SymbolFlagsPropertyOrAccessor|ast.SymbolFlagsVariable) != 0 {
unionPropertyKind = ScriptElementKindMemberVariableElement
break
}
}
if unionPropertyKind == ScriptElementKindUnknown {
// If this was union of all methods,
// make sure it has call signatures before we can label it as method.
typeOfUnionProperty := typeChecker.GetTypeOfSymbolAtLocation(symbol, location)
if len(typeChecker.GetCallSignatures(typeOfUnionProperty)) > 0 {
return ScriptElementKindMemberFunctionElement
}
return ScriptElementKindMemberVariableElement
}
return unionPropertyKind
}
return ScriptElementKindMemberVariableElement
}
return ScriptElementKindUnknown
}
func isFirstDeclarationOfSymbolParameter(symbol *ast.Symbol) bool {
var declaration *ast.Node
if len(symbol.Declarations) > 0 {
declaration = symbol.Declarations[0]
}
result := ast.FindAncestorOrQuit(declaration, func(n *ast.Node) ast.FindAncestorResult {
if ast.IsParameterDeclaration(n) {
return ast.FindAncestorTrue
}
if ast.IsBindingElement(n) || ast.IsObjectBindingPattern(n) || ast.IsArrayBindingPattern(n) {
return ast.FindAncestorFalse
}
return ast.FindAncestorQuit
})
return result != nil
}
func isLocalVariableOrFunction(symbol *ast.Symbol) bool {
if symbol.Parent != nil {
return false // This is exported symbol
}
for _, decl := range symbol.Declarations {
// Function expressions are local
if decl.Kind == ast.KindFunctionExpression {
return true
}
if decl.Kind != ast.KindVariableDeclaration && decl.Kind != ast.KindFunctionDeclaration {
continue
}
// If the parent is not source file or module block, it is a local variable.
parent := decl.Parent
for ; !ast.IsFunctionBlock(parent); parent = parent.Parent {
// Reached source file or module block
if parent.Kind == ast.KindSourceFile || parent.Kind == ast.KindModuleBlock {
break
}
}
if ast.IsFunctionBlock(parent) {
// Parent is in function block.
return true
}
}
return false
}
func GetSymbolModifiers(typeChecker *checker.Checker, symbol *ast.Symbol) ScriptElementKindModifier {
if symbol == nil {
return ScriptElementKindModifierNone
}
modifiers := getNormalizedSymbolModifiers(typeChecker, symbol)
if symbol.Flags&ast.SymbolFlagsAlias != 0 && typeChecker != nil {
resolvedSymbol := typeChecker.GetAliasedSymbol(symbol)
if resolvedSymbol != symbol {
modifiers |= getNormalizedSymbolModifiers(typeChecker, resolvedSymbol)
}
}
if symbol.Flags&ast.SymbolFlagsOptional != 0 {
modifiers |= ScriptElementKindModifierOptional
}
return modifiers
}
func getNormalizedSymbolModifiers(typeChecker *checker.Checker, symbol *ast.Symbol) ScriptElementKindModifier {
var modifierSet ScriptElementKindModifier
if len(symbol.Declarations) > 0 {
declaration := symbol.Declarations[0]
declarations := symbol.Declarations[1:]
// omit deprecated flag if some declarations are not deprecated
var excludeFlags ast.ModifierFlags
if len(declarations) > 0 &&
isDeprecatedDeclaration(typeChecker, declaration) && // !!! include jsdoc node flags
core.Some(declarations, func(d *ast.Node) bool { return !isDeprecatedDeclaration(typeChecker, d) }) {
excludeFlags = ast.ModifierFlagsDeprecated
} else {
excludeFlags = ast.ModifierFlagsNone
}
modifierSet = getNodeModifiers(typeChecker, declaration, excludeFlags)
}
return modifierSet
}
func isDeprecatedDeclaration(typeChecker *checker.Checker, declaration *ast.Node) bool {
if typeChecker != nil {
return typeChecker.IsDeprecatedDeclaration(declaration)
}
return ast.IsDeprecatedDeclaration(declaration)
}
func getNodeModifiers(typeChecker *checker.Checker, node *ast.Node, excludeFlags ast.ModifierFlags) ScriptElementKindModifier {
var result ScriptElementKindModifier
var flags ast.ModifierFlags
if ast.IsDeclaration(node) {
flags = ast.GetCombinedModifierFlags(node)
if isDeprecatedDeclaration(typeChecker, node) {
flags |= ast.ModifierFlagsDeprecated
}
flags &^= excludeFlags
}
if flags&ast.ModifierFlagsPrivate != 0 {
result |= ScriptElementKindModifierPrivate
}
if flags&ast.ModifierFlagsProtected != 0 {
result |= ScriptElementKindModifierProtected
}
if flags&ast.ModifierFlagsPublic != 0 {
result |= ScriptElementKindModifierPublic
}
if flags&ast.ModifierFlagsStatic != 0 {
result |= ScriptElementKindModifierStatic
}
if flags&ast.ModifierFlagsAbstract != 0 {
result |= ScriptElementKindModifierAbstract
}
if flags&ast.ModifierFlagsExport != 0 {
result |= ScriptElementKindModifierExported
}
if flags&ast.ModifierFlagsDeprecated != 0 {
result |= ScriptElementKindModifierDeprecated
}
if flags&ast.ModifierFlagsAmbient != 0 {
result |= ScriptElementKindModifierAmbient
}
if node.Flags&ast.NodeFlagsAmbient != 0 {
result |= ScriptElementKindModifierAmbient
}
if node.Kind == ast.KindExportAssignment {
result |= ScriptElementKindModifierExported
}
return result
}

View File

@@ -0,0 +1,901 @@
package lsutil
import (
"reflect"
"slices"
"strings"
"sync"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/json"
"github.com/microsoft/typescript-go/internal/modulespecifiers"
"github.com/microsoft/typescript-go/internal/vfs/vfsmatch"
)
func NewDefaultUserPreferences() UserPreferences {
return UserPreferences{
FormatCodeSettings: GetDefaultFormatCodeSettings(),
IncludeCompletionsForModuleExports: core.TSTrue,
IncludeCompletionsForImportStatements: core.TSTrue,
EnableAutoClosingTags: core.TSTrue,
EnableJSDocCompletions: core.TSTrue,
GenerateReturnInDocTemplate: core.TSTrue,
AllowRenameOfImportPath: core.TSTrue,
ProvideRefactorNotApplicableReason: core.TSTrue,
EnableFormatting: core.TSTrue,
EnableValidation: core.TSTrue,
DisplayPartsForJSDoc: core.TSTrue,
DisableLineTextInReferences: core.TSTrue,
ReportStyleChecksAsWarnings: core.TSTrue,
ExcludeLibrarySymbolsInNavTo: core.TSTrue,
}
}
// UserPreferences represents TypeScript language service preferences.
//
// Fields are populated using two tags:
// - `raw:"name"` or `raw:"name,invert"` - TypeScript/raw name for unstable section lookup
// - `config:"path.to.setting"` or `config:"path.to.setting,invert"` - VS Code nested config path
//
// At least one tag must be present on each preference field.
// The `,invert` modifier inverts boolean values (e.g., VS Code's "suppress" -> our "include").
type UserPreferences struct {
FormatCodeSettings FormatCodeSettings
QuotePreference QuotePreference `raw:"quotePreference" config:"preferences.quoteStyle"`
LazyConfiguredProjectsFromExternalProject core.Tristate `raw:"lazyConfiguredProjectsFromExternalProject"` // !!!
// A positive integer indicating the maximum length of a hover text before it is truncated.
//
// Default: `500`
MaximumHoverLength int `raw:"maximumHoverLength"` // !!!
// ------- Completions -------
// If enabled, TypeScript will search through all external modules' exports and add them to the completions list.
// This affects lone identifier completions but not completions on the right hand side of `obj.`.
IncludeCompletionsForModuleExports core.Tristate `raw:"includeCompletionsForModuleExports" config:"suggest.autoImports"`
// Enables auto-import-style completions on partially-typed import statements. E.g., allows
// `import write|` to be completed to `import { writeFile } from "fs"`.
IncludeCompletionsForImportStatements core.Tristate `raw:"includeCompletionsForImportStatements" config:"suggest.includeCompletionsForImportStatements"`
// Unless this option is `false`, member completion lists triggered with `.` will include entries
// on potentially-null and potentially-undefined values, with insertion text to replace
// preceding `.` tokens with `?.`.
IncludeAutomaticOptionalChainCompletions core.Tristate `raw:"includeAutomaticOptionalChainCompletions" config:"suggest.includeAutomaticOptionalChainCompletions"`
// If enabled, completions for class members (e.g. methods and properties) will include
// a whole declaration for the member.
// E.g., `class A { f| }` could be completed to `class A { foo(): number {} }`, instead of
// `class A { foo }`.
IncludeCompletionsWithClassMemberSnippets core.Tristate `raw:"includeCompletionsWithClassMemberSnippets" config:"suggest.classMemberSnippets.enabled"` // !!!
// If enabled, object literal methods will have a method declaration completion entry in addition
// to the regular completion entry containing just the method name.
// E.g., `const objectLiteral: T = { f| }` could be completed to `const objectLiteral: T = { foo(): void {} }`,
// in addition to `const objectLiteral: T = { foo }`.
IncludeCompletionsWithObjectLiteralMethodSnippets core.Tristate `raw:"includeCompletionsWithObjectLiteralMethodSnippets" config:"suggest.objectLiteralMethodSnippets.enabled"` // !!!
JsxAttributeCompletionStyle JsxAttributeCompletionStyle `raw:"jsxAttributeCompletionStyle" config:"preferences.jsxAttributeCompletionStyle"`
EnableAutoClosingTags core.Tristate `raw:"autoClosingTags" config:"autoClosingTags.enabled" fallbackConfig:"autoClosingTags"`
EnableJSDocCompletions core.Tristate `raw:"completeJSDocs" config:"suggest.jsdoc.enabled" fallbackConfig:"suggest.completeJSDocs"`
GenerateReturnInDocTemplate core.Tristate `raw:"generateReturnInDocTemplate" config:"suggest.jsdoc.generateReturns"`
// ------- AutoImports --------
ImportModuleSpecifierPreference modulespecifiers.ImportModuleSpecifierPreference `raw:"importModuleSpecifierPreference" config:"preferences.importModuleSpecifier"` // !!!
// Determines whether we import `foo/index.ts` as "foo", "foo/index", or "foo/index.js"
ImportModuleSpecifierEnding modulespecifiers.ImportModuleSpecifierEndingPreference `raw:"importModuleSpecifierEnding" config:"preferences.importModuleSpecifierEnding"` // !!!
AutoImportSpecifierExcludeRegexes []string `raw:"autoImportSpecifierExcludeRegexes" config:"preferences.autoImportSpecifierExcludeRegexes"` // !!!
AutoImportFileExcludePatterns []string `raw:"autoImportFileExcludePatterns" config:"preferences.autoImportFileExcludePatterns"`
AutoImportEntrypointDirectorySearch core.Tristate `raw:"autoImportEntrypointDirectorySearch" config:"preferences.autoImportEntrypointDirectorySearch"`
PreferTypeOnlyAutoImports core.Tristate `raw:"preferTypeOnlyAutoImports" config:"preferences.preferTypeOnlyAutoImports"`
// ------- OrganizeImports -------
// Indicates which deterministic preset should be used to sort imports.
// "auto" detects the existing ordinal case sensitivity where possible.
OrganizeImportsSort OrganizeImportsSort `raw:"organizeImportsSort" config:"preferences.organizeImports.sort"` // !!!
// Indicates whether imports should be organized in a case-insensitive manner.
//
// Default: TSUnknown ("auto" in strada), will perform detection
OrganizeImportsIgnoreCase core.Tristate `raw:"organizeImportsIgnoreCase" config:"preferences.organizeImports.caseSensitivity"` // !!!
// Indicates whether imports should be organized via an "ordinal" (binary) comparison using the numeric value of their
// code points, or via "unicode" natural sorting. This implementation is locale-agnostic and approximates the practical
// import-sorting behavior rather than the full Unicode Collation Algorithm.
//
// Default: Ordinal
OrganizeImportsCollation OrganizeImportsCollation `raw:"organizeImportsCollation" config:"preferences.organizeImports.unicodeCollation"` // !!!
// Indicates the locale to use for "unicode" collation in legacy clients. This is accepted for compatibility, but
// currently ignored because organize-import sorting is deterministic and locale-agnostic.
//
// This preference is ignored if organizeImportsCollation is not `unicode`.
//
// Default: `"en"`
OrganizeImportsLocale string `raw:"organizeImportsLocale" config:"preferences.organizeImports.locale"` // !!!
// Indicates whether numeric collation should be used for digit sequences in strings. When `true`, will collate
// strings such that `a1z < a2z < a100z`. When `false`, will collate strings such that `a1z < a100z < a2z`.
//
// This preference is ignored if organizeImportsCollation is not `unicode`.
//
// Default: `false`
OrganizeImportsNumericCollation core.Tristate `raw:"organizeImportsNumericCollation" config:"preferences.organizeImports.numericCollation"` // !!!
// Indicates whether accents and other diacritic marks are considered unequal for the purpose of sorting.
//
// This preference is ignored if organizeImportsCollation is not `unicode`.
//
// Default: `true`
OrganizeImportsAccentCollation core.Tristate `raw:"organizeImportsAccentCollation" config:"preferences.organizeImports.accentCollation"` // !!!
// Indicates whether upper case or lower case should sort first.
//
// This permission is ignored if:
// - organizeImportsCollation is not `unicode`
// - organizeImportsIgnoreCase is `true`
// - organizeImportsIgnoreCase is `auto` and the auto-detected case sensitivity is case-insensitive.
//
// Default: `false`
OrganizeImportsCaseFirst OrganizeImportsCaseFirst `raw:"organizeImportsCaseFirst" config:"preferences.organizeImports.caseFirst"` // !!!
// Indicates where named type-only imports should sort. "inline" sorts named imports without regard to if the import is type-only.
//
// Default: `auto`, which defaults to `last`
OrganizeImportsTypeOrder OrganizeImportsTypeOrder `raw:"organizeImportsTypeOrder" config:"preferences.organizeImports.typeOrder"` // !!!
// ------- MoveToFile -------
AllowTextChangesInNewFiles core.Tristate `raw:"allowTextChangesInNewFiles"` // !!!
// ------- Rename -------
UseAliasesForRename core.Tristate `raw:"providePrefixAndSuffixTextForRename" config:"preferences.useAliasesForRenames"`
AllowRenameOfImportPath core.Tristate `raw:"allowRenameOfImportPath"`
// ------- CodeFixes/Refactors -------
ProvideRefactorNotApplicableReason core.Tristate `raw:"provideRefactorNotApplicableReason"` // !!!
// ------- InlayHints -------
InlayHints InlayHintsPreferences
// ------- CodeLens -------
CodeLens CodeLensUserPreferences
// ------- Definition -------
PreferGoToSourceDefinition bool `raw:"preferGoToSourceDefinition"`
// ------- Symbols -------
ExcludeLibrarySymbolsInNavTo core.Tristate `raw:"excludeLibrarySymbolsInNavTo" config:"workspaceSymbols.excludeLibrarySymbols"`
// ------- Misc -------
EnableFormatting core.Tristate `raw:"formatEnabled" config:"format.enabled" fallbackConfig:"format.enable"`
EnableValidation core.Tristate `raw:"validateEnabled" config:"validate.enabled" fallbackConfig:"validate.enable"`
DisableSuggestions core.Tristate `raw:"disableSuggestions"` // !!!
DisableLineTextInReferences core.Tristate `raw:"disableLineTextInReferences"` // !!!
DisplayPartsForJSDoc core.Tristate `raw:"displayPartsForJSDoc"` // !!!
ReportStyleChecksAsWarnings core.Tristate `raw:"reportStyleChecksAsWarnings" config:"reportStyleChecksAsWarnings"`
// ------- ATA -------
// DisableAutomaticTypeAcquisition is the deprecated setting from typescript.disableAutomaticTypeAcquisition.
DisableAutomaticTypeAcquisition core.Tristate `raw:"disableAutomaticTypeAcquisition" config:"disableAutomaticTypeAcquisition"`
// AutomaticTypeAcquisitionEnabled is the unified setting from tsserver.automaticTypeAcquisition.enabled under the js/ts section.
// When set, it takes precedence over DisableAutomaticTypeAcquisition.
AutomaticTypeAcquisitionEnabled core.Tristate `raw:"automaticTypeAcquisitionEnabled" config:"tsserver.automaticTypeAcquisition.enabled"`
// TODO: add tsserver.web.typeAcquisition.enabled under the js/ts section for the web variant when web support is implemented.
// ------- Project Configuration -------
// CustomConfigFileName specifies a custom config file name to use before defaulting to tsconfig.json/jsconfig.json.
CustomConfigFileName string `raw:"customConfigFileName" config:"customConfigFileName"`
}
// IsATADisabled returns whether Automatic Type Acquisition is disabled based on user preferences.
// It checks the unified setting (tsserver.automaticTypeAcquisition.enabled) first,
// then falls back to the deprecated setting (disableAutomaticTypeAcquisition).
func (p UserPreferences) IsATADisabled() bool {
if !p.AutomaticTypeAcquisitionEnabled.IsUnknown() {
return !p.AutomaticTypeAcquisitionEnabled.IsTrue()
}
return p.DisableAutomaticTypeAcquisition.IsTrue()
}
type InlayHintsPreferences struct {
IncludeInlayParameterNameHints IncludeInlayParameterNameHints `raw:"includeInlayParameterNameHints" config:"inlayHints.parameterNames.enabled"`
IncludeInlayParameterNameHintsWhenArgumentMatchesName core.Tristate `raw:"includeInlayParameterNameHintsWhenArgumentMatchesName" config:"inlayHints.parameterNames.suppressWhenArgumentMatchesName,invert"`
IncludeInlayFunctionParameterTypeHints core.Tristate `raw:"includeInlayFunctionParameterTypeHints" config:"inlayHints.parameterTypes.enabled"`
IncludeInlayVariableTypeHints core.Tristate `raw:"includeInlayVariableTypeHints" config:"inlayHints.variableTypes.enabled"`
IncludeInlayVariableTypeHintsWhenTypeMatchesName core.Tristate `raw:"includeInlayVariableTypeHintsWhenTypeMatchesName" config:"inlayHints.variableTypes.suppressWhenTypeMatchesName,invert"`
IncludeInlayPropertyDeclarationTypeHints core.Tristate `raw:"includeInlayPropertyDeclarationTypeHints" config:"inlayHints.propertyDeclarationTypes.enabled"`
IncludeInlayFunctionLikeReturnTypeHints core.Tristate `raw:"includeInlayFunctionLikeReturnTypeHints" config:"inlayHints.functionLikeReturnTypes.enabled"`
IncludeInlayEnumMemberValueHints core.Tristate `raw:"includeInlayEnumMemberValueHints" config:"inlayHints.enumMemberValues.enabled"`
}
type CodeLensUserPreferences struct {
ReferencesCodeLensEnabled core.Tristate `raw:"referencesCodeLensEnabled" config:"referencesCodeLens.enabled"`
ImplementationsCodeLensEnabled core.Tristate `raw:"implementationsCodeLensEnabled" config:"implementationsCodeLens.enabled"`
ReferencesCodeLensShowOnAllFunctions core.Tristate `raw:"referencesCodeLensShowOnAllFunctions" config:"referencesCodeLens.showOnAllFunctions"`
ImplementationsCodeLensShowOnInterfaceMethods core.Tristate `raw:"implementationsCodeLensShowOnInterfaceMethods" config:"implementationsCodeLens.showOnInterfaceMethods"`
ImplementationsCodeLensShowOnAllClassMethods core.Tristate `raw:"implementationsCodeLensShowOnAllClassMethods" config:"implementationsCodeLens.showOnAllClassMethods"`
}
// --- Enum Types ---
type QuotePreference string
const (
QuotePreferenceUnknown QuotePreference = ""
QuotePreferenceAuto QuotePreference = "auto"
QuotePreferenceDouble QuotePreference = "double"
QuotePreferenceSingle QuotePreference = "single"
)
type JsxAttributeCompletionStyle string
const (
JsxAttributeCompletionStyleUnknown JsxAttributeCompletionStyle = ""
JsxAttributeCompletionStyleAuto JsxAttributeCompletionStyle = "auto"
JsxAttributeCompletionStyleBraces JsxAttributeCompletionStyle = "braces"
JsxAttributeCompletionStyleNone JsxAttributeCompletionStyle = "none"
)
type IncludeInlayParameterNameHints string
const (
IncludeInlayParameterNameHintsNone IncludeInlayParameterNameHints = ""
IncludeInlayParameterNameHintsAll IncludeInlayParameterNameHints = "all"
IncludeInlayParameterNameHintsLiterals IncludeInlayParameterNameHints = "literals"
)
type OrganizeImportsSort int
const (
OrganizeImportsSortAuto OrganizeImportsSort = iota
OrganizeImportsSortOrdinal
OrganizeImportsSortOrdinalIgnoreCase
OrganizeImportsSortNatural
OrganizeImportsSortNaturalIgnoreCase
)
type OrganizeImportsCollation bool
const (
OrganizeImportsCollationOrdinal OrganizeImportsCollation = false
OrganizeImportsCollationUnicode OrganizeImportsCollation = true
)
type OrganizeImportsCaseFirst int
const (
OrganizeImportsCaseFirstFalse OrganizeImportsCaseFirst = 0
OrganizeImportsCaseFirstLower OrganizeImportsCaseFirst = 1
OrganizeImportsCaseFirstUpper OrganizeImportsCaseFirst = 2
)
type OrganizeImportsTypeOrder int
const (
OrganizeImportsTypeOrderAuto OrganizeImportsTypeOrder = 0
OrganizeImportsTypeOrderLast OrganizeImportsTypeOrder = 1
OrganizeImportsTypeOrderInline OrganizeImportsTypeOrder = 2
OrganizeImportsTypeOrderFirst OrganizeImportsTypeOrder = 3
)
// --- Reflection-based parsing infrastructure ---
// typeParsers maps reflect.Type to a function that parses a value into that type.
var typeParsers = map[reflect.Type]func(any) any{
reflect.TypeFor[core.Tristate](): func(val any) any {
if b, ok := val.(bool); ok {
if b {
return core.TSTrue
}
return core.TSFalse
}
return core.TSUnknown
},
reflect.TypeFor[IndentStyle](): func(val any) any {
return parseIndentStyle(val)
},
reflect.TypeFor[SemicolonPreference](): func(val any) any {
return parseSemicolonPreference(val)
},
reflect.TypeFor[QuotePreference](): func(val any) any {
if s, ok := val.(string); ok {
switch strings.ToLower(s) {
case "auto":
return QuotePreferenceAuto
case "double":
return QuotePreferenceDouble
case "single":
return QuotePreferenceSingle
}
}
return QuotePreferenceUnknown
},
reflect.TypeFor[JsxAttributeCompletionStyle](): func(val any) any {
if s, ok := val.(string); ok {
switch strings.ToLower(s) {
case "braces":
return JsxAttributeCompletionStyleBraces
case "none":
return JsxAttributeCompletionStyleNone
}
}
return JsxAttributeCompletionStyleAuto
},
reflect.TypeFor[IncludeInlayParameterNameHints](): func(val any) any {
if s, ok := val.(string); ok {
switch s {
case "all":
return IncludeInlayParameterNameHintsAll
case "literals":
return IncludeInlayParameterNameHintsLiterals
}
}
return IncludeInlayParameterNameHintsNone
},
reflect.TypeFor[OrganizeImportsSort](): func(val any) any {
if s, ok := val.(string); ok {
switch strings.ToLower(s) {
case "ordinal":
return OrganizeImportsSortOrdinal
case "ordinalignorecase":
return OrganizeImportsSortOrdinalIgnoreCase
case "natural":
return OrganizeImportsSortNatural
case "naturalignorecase":
return OrganizeImportsSortNaturalIgnoreCase
}
}
return OrganizeImportsSortAuto
},
reflect.TypeFor[OrganizeImportsCollation](): func(val any) any {
if s, ok := val.(string); ok && strings.ToLower(s) == "unicode" {
return OrganizeImportsCollationUnicode
}
return OrganizeImportsCollationOrdinal
},
reflect.TypeFor[OrganizeImportsCaseFirst](): func(val any) any {
if s, ok := val.(string); ok {
switch s {
case "lower":
return OrganizeImportsCaseFirstLower
case "upper":
return OrganizeImportsCaseFirstUpper
}
}
return OrganizeImportsCaseFirstFalse
},
reflect.TypeFor[OrganizeImportsTypeOrder](): func(val any) any {
if s, ok := val.(string); ok {
switch s {
case "last":
return OrganizeImportsTypeOrderLast
case "inline":
return OrganizeImportsTypeOrderInline
case "first":
return OrganizeImportsTypeOrderFirst
}
}
return OrganizeImportsTypeOrderAuto
},
reflect.TypeFor[modulespecifiers.ImportModuleSpecifierPreference](): func(val any) any {
if s, ok := val.(string); ok {
switch strings.ToLower(s) {
case "project-relative":
return modulespecifiers.ImportModuleSpecifierPreferenceProjectRelative
case "relative":
return modulespecifiers.ImportModuleSpecifierPreferenceRelative
case "non-relative":
return modulespecifiers.ImportModuleSpecifierPreferenceNonRelative
}
}
return modulespecifiers.ImportModuleSpecifierPreferenceShortest
},
reflect.TypeFor[modulespecifiers.ImportModuleSpecifierEndingPreference](): func(val any) any {
if s, ok := val.(string); ok {
switch strings.ToLower(s) {
case "minimal":
return modulespecifiers.ImportModuleSpecifierEndingPreferenceMinimal
case "index":
return modulespecifiers.ImportModuleSpecifierEndingPreferenceIndex
case "js":
return modulespecifiers.ImportModuleSpecifierEndingPreferenceJs
}
}
return modulespecifiers.ImportModuleSpecifierEndingPreferenceAuto
},
}
// typeSerializers maps reflect.Type to a function that serializes a value of that type.
// For types which do not serialize as-is (tristate, enums, etc).
var typeSerializers = map[reflect.Type]func(any) any{
reflect.TypeFor[core.Tristate](): func(val any) any {
switch val.(core.Tristate) {
case core.TSTrue:
return true
case core.TSFalse:
return false
default:
return nil
}
},
reflect.TypeFor[OrganizeImportsSort](): func(val any) any {
switch val.(OrganizeImportsSort) {
case OrganizeImportsSortOrdinal:
return "ordinal"
case OrganizeImportsSortOrdinalIgnoreCase:
return "ordinalIgnoreCase"
case OrganizeImportsSortNatural:
return "natural"
case OrganizeImportsSortNaturalIgnoreCase:
return "naturalIgnoreCase"
default:
return "auto"
}
},
reflect.TypeFor[OrganizeImportsCollation](): func(val any) any {
if val.(OrganizeImportsCollation) == OrganizeImportsCollationUnicode {
return "unicode"
}
return "ordinal"
},
reflect.TypeFor[OrganizeImportsCaseFirst](): func(val any) any {
switch val.(OrganizeImportsCaseFirst) {
case OrganizeImportsCaseFirstLower:
return "lower"
case OrganizeImportsCaseFirstUpper:
return "upper"
default:
return "default"
}
},
reflect.TypeFor[OrganizeImportsTypeOrder](): func(val any) any {
switch val.(OrganizeImportsTypeOrder) {
case OrganizeImportsTypeOrderLast:
return "last"
case OrganizeImportsTypeOrderInline:
return "inline"
case OrganizeImportsTypeOrderFirst:
return "first"
default:
return "auto"
}
},
// These enums distinguish an unset zero value (e.g. "") from their effective
// default (e.g. "auto"): the parser promotes unset/unknown input to the
// non-zero default. Plain string serialization would therefore write "" for
// an unset field and the parser would read it back as the non-zero default,
// breaking round-tripping. Mirror the core.Tristate serializer above and omit
// the unset value (return nil) so it decodes back to the zero value. (Enums
// whose default already is their zero value, like the OrganizeImports* ones,
// round-trip without this.)
//
// TODO: These three are the only parsers whose fallback is a non-zero value;
// every other parser returns its zero value as the fallback. They should be
// made consistent: change the parser fallback to return the zero value and
// remove this serializer (relying on the default string serialization, which
// already omits ""). The consumer must then treat the zero value as the
// effective default. The two module-specifier enums are safe to convert (all
// read sites already treat the "" zero identically to the promoted default).
reflect.TypeFor[JsxAttributeCompletionStyle](): func(val any) any {
// TODO: make consistent with other enums (see note above). Unlike the
// module-specifier enums, the consumer in completions.go distinguishes
// JsxAttributeCompletionStyleUnknown from ...Auto, so converting this one
// requires updating that consumer to treat the zero value as "auto".
if v := val.(JsxAttributeCompletionStyle); v != JsxAttributeCompletionStyleUnknown {
return string(v)
}
return nil
},
reflect.TypeFor[modulespecifiers.ImportModuleSpecifierPreference](): func(val any) any {
// TODO: make consistent with other enums (see note above): have the parser
// return the zero value (None) as its fallback and drop this serializer.
if v := val.(modulespecifiers.ImportModuleSpecifierPreference); v != "" {
return string(v)
}
return nil
},
reflect.TypeFor[modulespecifiers.ImportModuleSpecifierEndingPreference](): func(val any) any {
// TODO: make consistent with other enums (see note above): have the parser
// return the zero value (None) as its fallback and drop this serializer.
if v := val.(modulespecifiers.ImportModuleSpecifierEndingPreference); v != "" {
return string(v)
}
return nil
},
}
// configPathParsers provides field-specific config value parsers that override the default
// type-based parser when the VS Code config value format differs from the Go field type.
var configPathParsers = map[string]func(any) any{
// VS Code sends caseSensitivity as a string ("auto"/"caseSensitive"/"caseInsensitive"),
// but OrganizeImportsIgnoreCase is a core.Tristate.
"preferences.organizeImports.caseSensitivity": func(val any) any {
if s, ok := val.(string); ok {
switch strings.ToLower(s) {
case "caseinsensitive":
return core.TSTrue
case "casesensitive":
return core.TSFalse
}
}
if b, ok := val.(bool); ok {
if b {
return core.TSTrue
}
return core.TSFalse
}
return core.TSUnknown
},
}
type fieldInfo struct {
rawName string // raw name for unstable section lookup (e.g., "quotePreference")
configPath string // dotted path for config (e.g., "preferences.quoteStyle")
fallbackConfigPaths []configPathInfo
fieldPath []int // index path to field in struct
rawInvert bool // whether to invert boolean values for raw name
configInvert bool // whether to invert boolean values for config path
}
type configPathInfo struct {
path string
invert bool
}
var fieldInfoCache = sync.OnceValue(func() []fieldInfo {
return collectFieldInfos(reflect.TypeFor[UserPreferences](), nil)
})
// unstableNameIndex maps raw names to fieldInfo index for unstable section lookup.
var unstableNameIndex = sync.OnceValue(func() map[string]int {
infos := fieldInfoCache()
index := make(map[string]int, len(infos))
for i, info := range infos {
if info.rawName != "" {
index[info.rawName] = i
}
}
return index
})
func collectFieldInfos(t reflect.Type, indexPath []int) []fieldInfo {
var infos []fieldInfo
for i := range t.NumField() {
field := t.Field(i)
currentPath := append(slices.Clone(indexPath), i)
rawTag := field.Tag.Get("raw")
configTag := field.Tag.Get("config")
fallbackConfigTag := field.Tag.Get("fallbackConfig")
if rawTag == "" && configTag == "" {
// Embedded struct without tags - recurse into it
if field.Type.Kind() == reflect.Struct {
infos = append(infos, collectFieldInfos(field.Type, currentPath)...)
continue
}
panic("raw or config tag required for field " + field.Name)
}
info := fieldInfo{
fieldPath: currentPath,
}
// Parse raw tag: "name" or "name,invert"
if rawTag != "" {
parts := strings.Split(rawTag, ",")
info.rawName = parts[0]
for _, part := range parts[1:] {
if part == "invert" {
info.rawInvert = true
}
}
}
// Parse config tag: "path.to.setting" or "path.to.setting,invert"
if configTag != "" {
configPath := parseConfigPathTag(configTag)
info.configPath = configPath.path
info.configInvert = configPath.invert
}
if fallbackConfigTag != "" {
for tag := range strings.SplitSeq(fallbackConfigTag, ";") {
info.fallbackConfigPaths = append(info.fallbackConfigPaths, parseConfigPathTag(tag))
}
}
infos = append(infos, info)
}
return infos
}
func parseConfigPathTag(tag string) configPathInfo {
parts := strings.Split(tag, ",")
info := configPathInfo{path: parts[0]}
for _, part := range parts[1:] {
if part == "invert" {
info.invert = true
}
}
return info
}
func getNestedValue(config map[string]any, path string) (any, bool) {
parts := strings.Split(path, ".")
current := any(config)
for _, part := range parts {
m, ok := current.(map[string]any)
if !ok {
return nil, false
}
current, ok = m[part]
if !ok {
return nil, false
}
}
return current, true
}
func setNestedValue(config map[string]any, path string, value any) {
parts := strings.Split(path, ".")
current := config
for _, part := range parts[:len(parts)-1] {
next, ok := current[part].(map[string]any)
if !ok {
next = make(map[string]any)
current[part] = next
}
current = next
}
current[parts[len(parts)-1]] = value
}
func setRawFieldsFromConfig(v reflect.Value, infos []fieldInfo, settings map[string]any) {
index := unstableNameIndex()
for name, value := range settings {
if idx, found := index[name]; found {
info := infos[idx]
field := getFieldByPath(v, info.fieldPath)
if info.rawInvert {
if b, ok := value.(bool); ok {
value = !b
}
}
setFieldFromValue(field, value)
}
}
}
func (p UserPreferences) withConfig(config map[string]any) UserPreferences {
v := reflect.ValueOf(&p).Elem()
infos := fieldInfoCache()
// Raw UserPreferences can be provided directly, notably via LSP initializationOptions.
setRawFieldsFromConfig(v, infos, config)
// Process "unstable" section first - allows any field to be set by raw name.
// This mirrors VS Code's behavior: { ...config.get('unstable'), ...stableOptions }
// where stable options are spread after and take precedence.
if unstable, ok := config["unstable"].(map[string]any); ok {
setRawFieldsFromConfig(v, infos, unstable)
}
// Process path-based config (VS Code style nested paths).
// These run after unstable, so stable config values take precedence.
for _, info := range infos {
if info.configPath == "" {
continue
}
configPath := configPathInfo{path: info.configPath, invert: info.configInvert}
val, ok := getNestedValue(config, configPath.path)
if !ok {
for _, fallbackConfigPath := range info.fallbackConfigPaths {
val, ok = getNestedValue(config, fallbackConfigPath.path)
if ok {
configPath = fallbackConfigPath
break
}
}
}
if !ok {
continue
}
field := getFieldByPath(v, info.fieldPath)
if configPath.invert {
if b, ok := val.(bool); ok {
val = !b
}
}
if parser, ok := configPathParsers[configPath.path]; ok {
field.Set(reflect.ValueOf(parser(val)))
continue
}
setFieldFromValue(field, val)
}
// Validate CustomConfigFileName for path traversal
if p.CustomConfigFileName != "" {
name := strings.TrimSpace(p.CustomConfigFileName)
if strings.ContainsAny(name, "/\\") || name == ".." || name == "." {
p.CustomConfigFileName = ""
} else {
p.CustomConfigFileName = name
}
}
return p
}
func getFieldByPath(v reflect.Value, path []int) reflect.Value {
for _, idx := range path {
v = v.Field(idx)
}
return v
}
func setFieldFromValue(field reflect.Value, val any) {
if val == nil {
return
}
// Check custom parsers first (for types like Tristate, enums, etc.)
if parser, ok := typeParsers[field.Type()]; ok {
field.Set(reflect.ValueOf(parser(val)))
return
}
switch field.Kind() {
case reflect.Bool:
if b, ok := val.(bool); ok {
field.SetBool(b)
}
case reflect.Int:
switch v := val.(type) {
case int:
field.SetInt(int64(v))
case float64:
field.SetInt(int64(v))
}
case reflect.String:
if s, ok := val.(string); ok {
field.SetString(s)
}
case reflect.Slice:
if arr, ok := val.([]any); ok {
result := reflect.MakeSlice(field.Type(), 0, len(arr))
for _, item := range arr {
if s, ok := item.(string); ok {
result = reflect.Append(result, reflect.ValueOf(s))
}
}
field.Set(result)
}
}
}
func (p *UserPreferences) MarshalJSONTo(enc *json.Encoder) error {
config := make(map[string]any)
v := reflect.ValueOf(p).Elem()
for _, info := range fieldInfoCache() {
field := getFieldByPath(v, info.fieldPath)
val := serializeField(field)
if val == nil {
continue
}
// Prefer config path if available, otherwise use unstable section
if info.configPath != "" {
if info.configInvert {
if b, ok := val.(bool); ok {
val = !b
}
}
setNestedValue(config, info.configPath, val)
} else if info.rawName != "" {
if info.rawInvert {
if b, ok := val.(bool); ok {
val = !b
}
}
setNestedValue(config, "unstable."+info.rawName, val)
}
}
return json.MarshalEncode(enc, config, json.Deterministic(true))
}
func serializeField(field reflect.Value) any {
// Check custom serializers first (for types like Tristate, enums, etc.)
if serializer, ok := typeSerializers[field.Type()]; ok {
return serializer(field.Interface())
}
switch field.Kind() {
case reflect.Bool:
return field.Bool()
case reflect.Int:
// Zero means "unset" for these preference fields. Omit it so a partial
// config does not clobber defaults with zeros when round-tripped through
// withConfig.
i := field.Int()
if i == 0 {
return nil
}
return int(i)
case reflect.String:
// Zero ("") means "unset"; omit it for the same reason as int above.
s := field.String()
if s == "" {
return nil
}
return s
case reflect.Slice:
if field.IsNil() {
return nil
}
result := make([]string, field.Len())
for i := range field.Len() {
result[i] = field.Index(i).String()
}
return result
default:
return field.Interface()
}
}
func (p *UserPreferences) UnmarshalJSONFrom(dec *json.Decoder) error {
var config map[string]any
if err := json.UnmarshalDecode(dec, &config); err != nil {
return err
}
// Start with defaults, then overlay parsed values
*p = NewDefaultUserPreferences().withConfig(config)
return nil
}
// --- Helper methods ---
func (p UserPreferences) ModuleSpecifierPreferences() modulespecifiers.UserPreferences {
return modulespecifiers.UserPreferences{
ImportModuleSpecifierPreference: p.ImportModuleSpecifierPreference,
ImportModuleSpecifierEnding: p.ImportModuleSpecifierEnding,
AutoImportSpecifierExcludeRegexes: p.AutoImportSpecifierExcludeRegexes,
}
}
func (p UserPreferences) ParsedAutoImportFileExcludePatterns(useCaseSensitiveFileNames bool) *vfsmatch.SpecMatcher {
return vfsmatch.NewSpecMatcher(p.AutoImportFileExcludePatterns, "", vfsmatch.UsageExclude, useCaseSensitiveFileNames)
}
func (p UserPreferences) IsModuleSpecifierExcluded(moduleSpecifier string) bool {
return modulespecifiers.IsExcludedByRegex(moduleSpecifier, p.AutoImportSpecifierExcludeRegexes)
}
func ParseUserPreferences(items map[string]any) UserPreferences {
prefs := NewDefaultUserPreferences()
// Apply editor settings first (tabSize, indentSize, etc.) as raw-name defaults,
// then overlay language-specific settings with increasing precedence:
// editor < javascript < typescript < js/ts
if editorItem, ok := items["editor"]; ok && editorItem != nil {
if editorSettings, ok := editorItem.(map[string]any); ok {
prefs = prefs.withConfig(map[string]any{"unstable": editorSettings})
}
}
// Apply javascript, then typescript, then js/ts (highest precedence).
for _, section := range []string{"javascript", "typescript", "js/ts"} {
if item, ok := items[section]; ok && item != nil {
if settings, ok := item.(map[string]any); ok {
prefs = prefs.withConfig(settings)
}
}
}
return prefs
}

View File

@@ -0,0 +1,644 @@
package lsutil
import (
"reflect"
"testing"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/json"
"github.com/microsoft/typescript-go/internal/modulespecifiers"
"gotest.tools/v3/assert"
)
func fillNonZeroValues(v reflect.Value) {
t := v.Type()
for i := range t.NumField() {
field := v.Field(i)
if !field.CanSet() {
continue
}
switch field.Kind() {
case reflect.Bool:
field.SetBool(true)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
field.SetInt(1)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
field.SetUint(1)
case reflect.String:
val := getValidStringValue(field.Type())
field.SetString(val)
case reflect.Slice:
if field.Type().Elem().Kind() == reflect.String {
field.Set(reflect.ValueOf([]string{"test"}))
}
case reflect.Struct:
fillNonZeroValues(field)
}
}
}
func getValidStringValue(t reflect.Type) string {
typeName := t.String()
switch typeName {
case "lsutil.QuotePreference":
return string(QuotePreferenceSingle)
case "lsutil.JsxAttributeCompletionStyle":
return string(JsxAttributeCompletionStyleBraces)
case "lsutil.IncludeInlayParameterNameHints":
return string(IncludeInlayParameterNameHintsAll)
case "lsutil.SemicolonPreference":
return string(SemicolonPreferenceInsert)
case "modulespecifiers.ImportModuleSpecifierPreference":
return string(modulespecifiers.ImportModuleSpecifierPreferenceRelative)
case "modulespecifiers.ImportModuleSpecifierEndingPreference":
return string(modulespecifiers.ImportModuleSpecifierEndingPreferenceJs)
default:
return "test"
}
}
func TestUserPreferencesRoundtrip(t *testing.T) {
t.Parallel()
var original UserPreferences
fillNonZeroValues(reflect.ValueOf(&original).Elem())
jsonBytes, err := json.Marshal(&original)
assert.NilError(t, err)
t.Run("UnmarshalJSONFrom", func(t *testing.T) {
t.Parallel()
var parsed UserPreferences
err2 := json.Unmarshal(jsonBytes, &parsed)
assert.NilError(t, err2)
assert.DeepEqual(t, original, parsed)
})
t.Run("withConfig", func(t *testing.T) {
t.Parallel()
var config map[string]any
err2 := json.Unmarshal(jsonBytes, &config)
assert.NilError(t, err2)
parsed := UserPreferences{}.withConfig(config)
assert.DeepEqual(t, original, parsed)
})
}
func TestUserPreferencesSerialize(t *testing.T) {
t.Parallel()
t.Run("config path field serializes to nested path", func(t *testing.T) {
t.Parallel()
prefs := &UserPreferences{
QuotePreference: QuotePreferenceSingle,
}
jsonBytes, err := json.Marshal(prefs)
assert.NilError(t, err)
var actual map[string]any
err = json.Unmarshal(jsonBytes, &actual)
assert.NilError(t, err)
preferences := actual["preferences"].(map[string]any)
assert.Equal(t, "single", preferences["quoteStyle"])
})
t.Run("raw-only field serializes to unstable section", func(t *testing.T) {
t.Parallel()
prefs := &UserPreferences{
DisableSuggestions: core.TSTrue,
}
jsonBytes, err := json.Marshal(prefs)
assert.NilError(t, err)
var actual map[string]any
err = json.Unmarshal(jsonBytes, &actual)
assert.NilError(t, err)
unstable := actual["unstable"].(map[string]any)
assert.Equal(t, true, unstable["disableSuggestions"])
})
t.Run("inlay hint inversion on serialize", func(t *testing.T) {
t.Parallel()
prefs := &UserPreferences{
InlayHints: InlayHintsPreferences{
IncludeInlayParameterNameHints: IncludeInlayParameterNameHintsAll,
IncludeInlayParameterNameHintsWhenArgumentMatchesName: core.TSTrue,
},
}
jsonBytes, err := json.Marshal(prefs)
assert.NilError(t, err)
var actual map[string]any
err = json.Unmarshal(jsonBytes, &actual)
assert.NilError(t, err)
inlayHints := actual["inlayHints"].(map[string]any)
parameterNames := inlayHints["parameterNames"].(map[string]any)
assert.Equal(t, "all", parameterNames["enabled"])
assert.Equal(t, false, parameterNames["suppressWhenArgumentMatchesName"]) // inverted
})
t.Run("mixed config and unstable fields", func(t *testing.T) {
t.Parallel()
prefs := &UserPreferences{
QuotePreference: QuotePreferenceSingle,
DisableSuggestions: core.TSTrue,
DisplayPartsForJSDoc: core.TSTrue,
}
jsonBytes, err := json.Marshal(prefs)
assert.NilError(t, err)
var actual map[string]any
err = json.Unmarshal(jsonBytes, &actual)
assert.NilError(t, err)
preferences := actual["preferences"].(map[string]any)
assert.Equal(t, "single", preferences["quoteStyle"])
unstable := actual["unstable"].(map[string]any)
assert.Equal(t, true, unstable["disableSuggestions"])
assert.Equal(t, true, unstable["displayPartsForJSDoc"])
})
}
func TestUserPreferencesParseUnstable(t *testing.T) {
t.Parallel()
tests := []struct {
name string
json string
expected UserPreferences
}{
{
name: "unstable fields with correct casing",
json: `{
"unstable": {
"disableSuggestions": true,
"maximumHoverLength": 100,
"allowRenameOfImportPath": true
}
}`,
expected: UserPreferences{
DisableSuggestions: core.TSTrue,
MaximumHoverLength: 100,
AllowRenameOfImportPath: core.TSTrue,
},
},
{
name: "nested preferences path",
json: `{
"preferences": {
"quoteStyle": "single",
"useAliasesForRenames": true
}
}`,
expected: UserPreferences{
QuotePreference: QuotePreferenceSingle,
UseAliasesForRename: core.TSTrue,
},
},
{
name: "suggest section",
json: `{
"suggest": {
"autoImports": false,
"includeCompletionsForImportStatements": true
}
}`,
expected: UserPreferences{
IncludeCompletionsForModuleExports: core.TSFalse,
IncludeCompletionsForImportStatements: core.TSTrue,
},
},
{
name: "inlayHints with invert",
json: `{
"inlayHints": {
"parameterNames": {
"enabled": "all",
"suppressWhenArgumentMatchesName": true
}
}
}`,
expected: UserPreferences{
InlayHints: InlayHintsPreferences{
IncludeInlayParameterNameHints: IncludeInlayParameterNameHintsAll,
IncludeInlayParameterNameHintsWhenArgumentMatchesName: core.TSFalse, // inverted
},
},
},
{
name: "mixed config",
json: `{
"unstable": {
"displayPartsForJSDoc": true
},
"preferences": {
"importModuleSpecifier": "relative"
},
"workspaceSymbols": {
"excludeLibrarySymbols": true
}
}`,
expected: UserPreferences{
DisplayPartsForJSDoc: core.TSTrue,
ImportModuleSpecifierPreference: modulespecifiers.ImportModuleSpecifierPreferenceRelative,
ExcludeLibrarySymbolsInNavTo: core.TSTrue,
},
},
{
name: "stable config overrides unstable",
json: `{
"unstable": {
"quotePreference": "double"
},
"preferences": {
"quoteStyle": "single"
}
}`,
expected: UserPreferences{
QuotePreference: QuotePreferenceSingle, // stable wins
},
},
{
name: "unstable sets value when no stable config",
json: `{
"unstable": {
"includeAutomaticOptionalChainCompletions": false
}
}`,
expected: UserPreferences{
IncludeAutomaticOptionalChainCompletions: core.TSFalse,
},
},
{
name: "any field can be passed via unstable by its raw name",
json: `{
"unstable": {
"quotePreference": "double",
"includeCompletionsForModuleExports": true,
"excludeLibrarySymbolsInNavTo": true
}
}`,
expected: UserPreferences{
QuotePreference: QuotePreferenceDouble,
IncludeCompletionsForModuleExports: core.TSTrue,
ExcludeLibrarySymbolsInNavTo: core.TSTrue,
},
},
{
name: "TypeScript raw names work in unstable section",
json: `{
"unstable": {
"includeCompletionsForModuleExports": true,
"quotePreference": "single",
"providePrefixAndSuffixTextForRename": true,
"includeInlayParameterNameHints": "all",
"organizeImportsLocale": "en"
}
}`,
expected: UserPreferences{
IncludeCompletionsForModuleExports: core.TSTrue,
QuotePreference: QuotePreferenceSingle,
UseAliasesForRename: core.TSTrue,
OrganizeImportsLocale: "en",
InlayHints: InlayHintsPreferences{
IncludeInlayParameterNameHints: IncludeInlayParameterNameHintsAll,
},
},
},
{
name: "old raw organize imports unicode preferences load as raw state",
json: `{
"unstable": {
"organizeImportsCollation": "unicode",
"organizeImportsCaseFirst": "upper",
"organizeImportsIgnoreCase": false,
"organizeImportsNumericCollation": true
}
}`,
expected: UserPreferences{
OrganizeImportsCollation: OrganizeImportsCollationUnicode,
OrganizeImportsCaseFirst: OrganizeImportsCaseFirstUpper,
OrganizeImportsIgnoreCase: core.TSFalse,
OrganizeImportsNumericCollation: core.TSTrue,
},
},
{
name: "old top-level raw organize imports unicode preferences load as raw state",
json: `{
"organizeImportsCollation": "unicode",
"organizeImportsIgnoreCase": true
}`,
expected: UserPreferences{
OrganizeImportsCollation: OrganizeImportsCollationUnicode,
OrganizeImportsIgnoreCase: core.TSTrue,
},
},
{
name: "new top-level raw organize imports sort is accepted",
json: `{
"organizeImportsSort": "natural"
}`,
expected: UserPreferences{
OrganizeImportsSort: OrganizeImportsSortNatural,
},
},
{
name: "old raw organize imports ignore case loads as raw state",
json: `{
"unstable": {
"organizeImportsIgnoreCase": true
}
}`,
expected: UserPreferences{
OrganizeImportsIgnoreCase: core.TSTrue,
},
},
{
name: "new raw organize imports sort loads alongside old raw preferences",
json: `{
"unstable": {
"organizeImportsSort": "ordinal",
"organizeImportsCollation": "unicode",
"organizeImportsIgnoreCase": true
}
}`,
expected: UserPreferences{
OrganizeImportsSort: OrganizeImportsSortOrdinal,
OrganizeImportsCollation: OrganizeImportsCollationUnicode,
OrganizeImportsIgnoreCase: core.TSTrue,
},
},
{
name: "old nested organize imports unicode preferences load as raw state",
json: `{
"preferences": {
"organizeImports": {
"unicodeCollation": "unicode",
"caseSensitivity": "caseSensitive",
"numericCollation": true,
"caseFirst": "upper"
}
}
}`,
expected: UserPreferences{
OrganizeImportsCollation: OrganizeImportsCollationUnicode,
OrganizeImportsIgnoreCase: core.TSFalse,
OrganizeImportsNumericCollation: core.TSTrue,
OrganizeImportsCaseFirst: OrganizeImportsCaseFirstUpper,
},
},
{
name: "new nested organize imports sort loads alongside old nested preferences",
json: `{
"preferences": {
"organizeImports": {
"sort": "ordinalIgnoreCase",
"unicodeCollation": "unicode",
"caseSensitivity": "caseSensitive"
}
}
}`,
expected: UserPreferences{
OrganizeImportsSort: OrganizeImportsSortOrdinalIgnoreCase,
OrganizeImportsCollation: OrganizeImportsCollationUnicode,
OrganizeImportsIgnoreCase: core.TSFalse,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var config map[string]any
err := json.Unmarshal([]byte(tt.json), &config)
assert.NilError(t, err)
parsed := UserPreferences{}.withConfig(config)
assert.DeepEqual(t, tt.expected, parsed)
})
}
}
func TestUserPreferencesReportStyleChecksAsWarnings(t *testing.T) {
t.Parallel()
t.Run("reportStyleChecksAsWarnings via config path", func(t *testing.T) {
t.Parallel()
prefs := ParseUserPreferences(map[string]any{
"js/ts": map[string]any{
"reportStyleChecksAsWarnings": false,
},
})
assert.Equal(t, prefs.ReportStyleChecksAsWarnings, core.TSFalse)
})
t.Run("reportStyleChecksAsWarnings defaults to true", func(t *testing.T) {
t.Parallel()
prefs := NewDefaultUserPreferences()
assert.Equal(t, prefs.ReportStyleChecksAsWarnings, core.TSTrue)
})
t.Run("reportStyleChecksAsWarnings via unstable section", func(t *testing.T) {
t.Parallel()
prefs := ParseUserPreferences(map[string]any{
"js/ts": map[string]any{
"unstable": map[string]any{
"reportStyleChecksAsWarnings": false,
},
},
})
assert.Equal(t, prefs.ReportStyleChecksAsWarnings, core.TSFalse)
})
}
func TestUserPreferencesParseServerFeaturePreferences(t *testing.T) {
t.Parallel()
t.Run("preferred server feature settings", func(t *testing.T) {
t.Parallel()
prefs := ParseUserPreferences(map[string]any{
"js/ts": map[string]any{
"validate": map[string]any{"enabled": false},
"format": map[string]any{"enabled": false},
"autoClosingTags": map[string]any{
"enabled": false,
},
},
})
assert.Equal(t, prefs.EnableValidation, core.TSFalse)
assert.Equal(t, prefs.EnableFormatting, core.TSFalse)
assert.Equal(t, prefs.EnableAutoClosingTags, core.TSFalse)
})
t.Run("legacy server feature fallbacks", func(t *testing.T) {
t.Parallel()
prefs := ParseUserPreferences(map[string]any{
"typescript": map[string]any{
"validate": map[string]any{"enable": false},
"format": map[string]any{"enable": false},
"autoClosingTags": false,
},
})
assert.Equal(t, prefs.EnableValidation, core.TSFalse)
assert.Equal(t, prefs.EnableFormatting, core.TSFalse)
assert.Equal(t, prefs.EnableAutoClosingTags, core.TSFalse)
})
t.Run("preferred settings take precedence over fallbacks", func(t *testing.T) {
t.Parallel()
prefs := ParseUserPreferences(map[string]any{
"typescript": map[string]any{
"validate": map[string]any{"enable": false},
"format": map[string]any{"enable": false},
"autoClosingTags": false,
},
"js/ts": map[string]any{
"validate": map[string]any{"enabled": true},
"format": map[string]any{"enabled": true},
"autoClosingTags": map[string]any{
"enabled": true,
},
},
})
assert.Equal(t, prefs.EnableValidation, core.TSTrue)
assert.Equal(t, prefs.EnableFormatting, core.TSTrue)
assert.Equal(t, prefs.EnableAutoClosingTags, core.TSTrue)
})
}
func TestUserPreferencesParseJSDocCompletionPreferences(t *testing.T) {
t.Parallel()
t.Run("unified jsdoc enabled setting", func(t *testing.T) {
t.Parallel()
prefs := ParseUserPreferences(map[string]any{
"js/ts": map[string]any{
"suggest": map[string]any{
"jsdoc": map[string]any{
"enabled": false,
},
},
},
})
assert.Equal(t, prefs.EnableJSDocCompletions, core.TSFalse)
})
t.Run("language fallback completeJSDocs setting", func(t *testing.T) {
t.Parallel()
prefs := ParseUserPreferences(map[string]any{
"typescript": map[string]any{
"suggest": map[string]any{
"completeJSDocs": false,
},
},
})
assert.Equal(t, prefs.EnableJSDocCompletions, core.TSFalse)
})
t.Run("unified jsdoc enabled takes precedence over language fallback", func(t *testing.T) {
t.Parallel()
prefs := ParseUserPreferences(map[string]any{
"typescript": map[string]any{
"suggest": map[string]any{
"completeJSDocs": false,
},
},
"js/ts": map[string]any{
"suggest": map[string]any{
"jsdoc": map[string]any{
"enabled": true,
},
},
},
})
assert.Equal(t, prefs.EnableJSDocCompletions, core.TSTrue)
})
t.Run("unified jsdoc generateReturns setting", func(t *testing.T) {
t.Parallel()
prefs := ParseUserPreferences(map[string]any{
"js/ts": map[string]any{
"suggest": map[string]any{
"jsdoc": map[string]any{
"generateReturns": false,
},
},
},
})
assert.Equal(t, prefs.GenerateReturnInDocTemplate, core.TSFalse)
})
t.Run("language jsdoc generateReturns setting", func(t *testing.T) {
t.Parallel()
prefs := ParseUserPreferences(map[string]any{
"typescript": map[string]any{
"suggest": map[string]any{
"jsdoc": map[string]any{
"generateReturns": false,
},
},
},
})
assert.Equal(t, prefs.GenerateReturnInDocTemplate, core.TSFalse)
})
}
func TestUserPreferencesParseATA(t *testing.T) {
t.Parallel()
t.Run("ParseUserPreferences with unified ATA setting in js/ts section", func(t *testing.T) {
t.Parallel()
prefs := ParseUserPreferences(map[string]any{
"js/ts": map[string]any{
"tsserver": map[string]any{
"automaticTypeAcquisition": map[string]any{
"enabled": false,
},
},
},
})
assert.Assert(t, prefs.IsATADisabled())
assert.Equal(t, prefs.AutomaticTypeAcquisitionEnabled, core.TSFalse)
})
t.Run("ParseUserPreferences with deprecated disableAutomaticTypeAcquisition in typescript section", func(t *testing.T) {
t.Parallel()
prefs := ParseUserPreferences(map[string]any{
"typescript": map[string]any{
"disableAutomaticTypeAcquisition": true,
},
})
assert.Assert(t, prefs.IsATADisabled())
assert.Equal(t, prefs.DisableAutomaticTypeAcquisition, core.TSTrue)
})
t.Run("unified setting takes precedence over deprecated setting", func(t *testing.T) {
t.Parallel()
// Both settings set: unified (js/ts) should take precedence
prefs := ParseUserPreferences(map[string]any{
"typescript": map[string]any{
"disableAutomaticTypeAcquisition": true,
},
"js/ts": map[string]any{
"tsserver": map[string]any{
"automaticTypeAcquisition": map[string]any{
"enabled": true,
},
},
},
})
assert.Assert(t, !prefs.IsATADisabled())
assert.Equal(t, prefs.AutomaticTypeAcquisitionEnabled, core.TSTrue)
})
t.Run("IsATADisabled returns false when neither setting is configured", func(t *testing.T) {
t.Parallel()
prefs := NewDefaultUserPreferences()
assert.Assert(t, !prefs.IsATADisabled())
})
}

View File

@@ -0,0 +1,157 @@
package lsutil
import (
"strings"
"unicode"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/astnav"
"github.com/microsoft/typescript-go/internal/compiler"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/scanner"
"github.com/microsoft/typescript-go/internal/stringutil"
"github.com/microsoft/typescript-go/internal/tspath"
)
func ProbablyUsesSemicolons(file *ast.SourceFile) bool {
withSemicolon := 0
withoutSemicolon := 0
nStatementsToObserve := 5
var visit func(node *ast.Node) bool
visit = func(node *ast.Node) bool {
if node.Flags&ast.NodeFlagsReparsed != 0 {
return false
}
if SyntaxRequiresTrailingSemicolonOrASI(node.Kind) {
lastToken := GetLastToken(node, file)
if lastToken != nil && lastToken.Kind == ast.KindSemicolonToken {
withSemicolon++
} else {
withoutSemicolon++
}
} else if SyntaxRequiresTrailingCommaOrSemicolonOrASI(node.Kind) {
lastToken := GetLastToken(node, file)
if lastToken != nil && lastToken.Kind == ast.KindSemicolonToken {
withSemicolon++
} else if lastToken != nil && lastToken.Kind != ast.KindCommaToken {
lastTokenLine := scanner.GetECMALineOfPosition(
file,
astnav.GetStartOfNode(lastToken, file, false /*includeJSDoc*/),
)
nextTokenLine := scanner.GetECMALineOfPosition(
file,
scanner.SkipTrivia(file.Text(), lastToken.End()),
)
// Avoid counting missing semicolon in single-line objects:
// `function f(p: { x: string /*no semicolon here is insignificant*/ }) {`
if lastTokenLine != nextTokenLine {
withoutSemicolon++
}
}
}
if withSemicolon+withoutSemicolon >= nStatementsToObserve {
return true
}
return node.ForEachChild(visit)
}
file.ForEachChild(visit)
// One statement missing a semicolon isn't sufficient evidence to say the user
// doesn't want semicolons, because they may not even be done writing that statement.
if withSemicolon == 0 && withoutSemicolon <= 1 {
return true
}
// When both kinds of observation exist, treat the file as using semicolons when the
// ratio withSemicolon/withoutSemicolon exceeds 1/nStatementsToObserve (real arithmetic),
// implemented as an integer inequality to avoid truncation.
if withoutSemicolon == 0 {
return true
}
return withSemicolon*nStatementsToObserve > withoutSemicolon
}
func ShouldUseUriStyleNodeCoreModules(file *ast.SourceFile, program *compiler.Program) core.Tristate {
for _, node := range file.Imports() {
if core.NodeCoreModules()[node.Text()] && !core.ExclusivelyPrefixedNodeCoreModules[node.Text()] {
if strings.HasPrefix(node.Text(), "node:") {
return core.TSTrue
} else {
return core.TSFalse
}
}
}
return program.UsesUriStyleNodeCoreModules()
}
func QuotePreferenceFromString(str *ast.StringLiteral) QuotePreference {
if str.TokenFlags&ast.TokenFlagsSingleQuote != 0 {
return QuotePreferenceSingle
}
return QuotePreferenceDouble
}
func GetQuotePreference(sourceFile *ast.SourceFile, preferences UserPreferences) QuotePreference {
if preferences.QuotePreference != "" && preferences.QuotePreference != "auto" {
if preferences.QuotePreference == "single" {
return QuotePreferenceSingle
}
return QuotePreferenceDouble
}
// ignore synthetic import added when importHelpers: true
firstModuleSpecifier := core.Find(sourceFile.Imports(), func(n *ast.Node) bool {
return ast.IsStringLiteral(n) && !ast.NodeIsSynthesized(n.Parent)
})
if firstModuleSpecifier != nil {
return QuotePreferenceFromString(firstModuleSpecifier.AsStringLiteral())
}
return QuotePreferenceDouble
}
func ModuleSymbolToValidIdentifier(moduleSymbol *ast.Symbol, forceCapitalize bool) string {
return ModuleSpecifierToValidIdentifier(stringutil.StripQuotes(moduleSymbol.Name), forceCapitalize)
}
func ModuleSpecifierToValidIdentifier(moduleSpecifier string, forceCapitalize bool) string {
baseName := tspath.GetBaseFileName(strings.TrimSuffix(tspath.RemoveFileExtension(moduleSpecifier), "/index"))
res := []rune{}
lastCharWasValid := true
baseNameRunes := []rune(baseName)
if len(baseNameRunes) > 0 && scanner.IsIdentifierStart(baseNameRunes[0]) {
if forceCapitalize {
res = append(res, unicode.ToUpper(baseNameRunes[0]))
} else {
res = append(res, baseNameRunes[0])
}
} else {
lastCharWasValid = false
}
for i := 1; i < len(baseNameRunes); i++ {
isValid := scanner.IsIdentifierPart(baseNameRunes[i])
if isValid {
if !lastCharWasValid {
res = append(res, unicode.ToUpper(baseNameRunes[i]))
} else {
res = append(res, baseNameRunes[i])
}
}
lastCharWasValid = isValid
}
// Need `"_"` to ensure result isn't empty.
resString := string(res)
if resString != "" && !IsNonContextualKeyword(scanner.StringToToken(resString)) {
return resString
}
return "_" + resString
}
func IsNonContextualKeyword(token ast.Kind) bool {
return ast.IsKeywordKind(token) && !ast.IsContextualKeyword(token)
}

View File

@@ -0,0 +1,200 @@
package lsutil
import (
"testing"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/parser"
)
func parseTS(t *testing.T, text string) *ast.SourceFile {
t.Helper()
return parser.ParseSourceFile(ast.SourceFileParseOptions{
FileName: "/test.ts",
Path: "/test.ts",
}, text, core.ScriptKindTS)
}
func TestProbablyUsesSemicolons(t *testing.T) {
t.Parallel()
tests := []struct {
name string
src string
want bool
}{
{
name: "mixed semicolons and ASI favors semicolons when ratio exceeds one fifth",
// First five observations: 2 with semicolon, 3 without. Real ratio 2/3 > 1/5.
// Integer division bug compared against 1/5==0 and used with/without as ints,
// so the old check was effectively (with/without) > 0, which failed here.
src: `let a = 1;
let b = 2;
let c = 3
let d = 4
let e = 5
`,
want: true,
},
{
name: "consistent ASI with no semicolons",
src: `let a = 1
let b = 2
let c = 3
`,
want: false,
},
{
name: "consistent semicolons",
src: `let a = 1;
let b = 2;
let c = 3;
`,
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
file := parseTS(t, tt.src)
if got := ProbablyUsesSemicolons(file); got != tt.want {
t.Errorf("ProbablyUsesSemicolons() = %v, want %v", got, tt.want)
}
})
}
}
func TestResolveOrganizeImportsSort(t *testing.T) {
t.Parallel()
tests := []struct {
name string
preferences UserPreferences
want OrganizeImportsSort
}{
{
name: "explicit sort wins",
preferences: UserPreferences{
OrganizeImportsSort: OrganizeImportsSortOrdinal,
OrganizeImportsCollation: OrganizeImportsCollationUnicode,
OrganizeImportsIgnoreCase: core.TSTrue,
},
want: OrganizeImportsSortOrdinal,
},
{
name: "unicode case-sensitive maps to natural",
preferences: UserPreferences{
OrganizeImportsCollation: OrganizeImportsCollationUnicode,
OrganizeImportsIgnoreCase: core.TSFalse,
},
want: OrganizeImportsSortNatural,
},
{
name: "unicode ignore case maps to natural ignore case",
preferences: UserPreferences{
OrganizeImportsCollation: OrganizeImportsCollationUnicode,
OrganizeImportsIgnoreCase: core.TSTrue,
},
want: OrganizeImportsSortNaturalIgnoreCase,
},
{
name: "unicode unknown case sensitivity stays auto for detection",
preferences: UserPreferences{
OrganizeImportsCollation: OrganizeImportsCollationUnicode,
},
want: OrganizeImportsSortAuto,
},
{
name: "ordinal ignore case maps to ordinal ignore case",
preferences: UserPreferences{
OrganizeImportsIgnoreCase: core.TSTrue,
},
want: OrganizeImportsSortOrdinalIgnoreCase,
},
{
name: "ordinal case sensitive maps to ordinal",
preferences: UserPreferences{
OrganizeImportsIgnoreCase: core.TSFalse,
},
want: OrganizeImportsSortOrdinal,
},
{
name: "unknown ordinal stays auto",
want: OrganizeImportsSortAuto,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if got := ResolveOrganizeImportsSort(tt.preferences); got != tt.want {
t.Fatalf("ResolveOrganizeImportsSort() = %v, want %v", got, tt.want)
}
})
}
}
func TestCompareOrganizeImportsNaturalStrings(t *testing.T) {
t.Parallel()
comparer := getOrganizeImportsPresetStringComparer(OrganizeImportsSortNaturalIgnoreCase)
tests := []struct {
name string
a string
b string
want int
}{
{
name: "numeric runs sort by numeric value",
a: "a2",
b: "a100",
want: -1,
},
{
name: "numeric runs with equal value use raw tie break",
a: "a02",
b: "a2",
want: -1,
},
{
name: "accents are folded for primary comparison",
a: "À",
b: "B",
want: -1,
},
{
name: "raw comparison breaks accent ties",
a: "A",
b: "À",
want: -1,
},
{
name: "hyphen sorts before slash like Intl.Collator fallback",
a: "app-init",
b: "app/app",
want: -1,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if got := cmpSign(comparer(tt.a, tt.b)); got != tt.want {
t.Fatalf("comparer(%q, %q) = %v, want sign %v", tt.a, tt.b, got, tt.want)
}
})
}
}
func cmpSign(value int) int {
switch {
case value < 0:
return -1
case value > 0:
return 1
default:
return 0
}
}