2025-10-15 10:12:44 +03:00

645 lines
22 KiB
Go

package format
import (
"strings"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/astnav"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/core"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/lsutil"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/scanner"
)
///
/// Contexts
///
type (
optionSelector = func(options *FormatCodeSettings) core.Tristate
anyOptionSelector[T comparable] = func(options *FormatCodeSettings) T
)
func semicolonOption(options *FormatCodeSettings) SemicolonPreference { return options.Semicolons }
func insertSpaceAfterCommaDelimiterOption(options *FormatCodeSettings) core.Tristate {
return options.InsertSpaceAfterCommaDelimiter
}
func insertSpaceAfterSemicolonInForStatementsOption(options *FormatCodeSettings) core.Tristate {
return options.InsertSpaceAfterSemicolonInForStatements
}
func insertSpaceBeforeAndAfterBinaryOperatorsOption(options *FormatCodeSettings) core.Tristate {
return options.InsertSpaceBeforeAndAfterBinaryOperators
}
func insertSpaceAfterConstructorOption(options *FormatCodeSettings) core.Tristate {
return options.InsertSpaceAfterConstructor
}
func insertSpaceAfterKeywordsInControlFlowStatementsOption(options *FormatCodeSettings) core.Tristate {
return options.InsertSpaceAfterKeywordsInControlFlowStatements
}
func insertSpaceAfterFunctionKeywordForAnonymousFunctionsOption(options *FormatCodeSettings) core.Tristate {
return options.InsertSpaceAfterFunctionKeywordForAnonymousFunctions
}
func insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesisOption(options *FormatCodeSettings) core.Tristate {
return options.InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis
}
func insertSpaceAfterOpeningAndBeforeClosingNonemptyBracketsOption(options *FormatCodeSettings) core.Tristate {
return options.InsertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets
}
func insertSpaceAfterOpeningAndBeforeClosingNonemptyBracesOption(options *FormatCodeSettings) core.Tristate {
return options.InsertSpaceAfterOpeningAndBeforeClosingNonemptyBraces
}
func insertSpaceAfterOpeningAndBeforeClosingEmptyBracesOption(options *FormatCodeSettings) core.Tristate {
return options.InsertSpaceAfterOpeningAndBeforeClosingEmptyBraces
}
func insertSpaceAfterOpeningAndBeforeClosingTemplateStringBracesOption(options *FormatCodeSettings) core.Tristate {
return options.InsertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces
}
func insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBracesOption(options *FormatCodeSettings) core.Tristate {
return options.InsertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces
}
func insertSpaceAfterTypeAssertionOption(options *FormatCodeSettings) core.Tristate {
return options.InsertSpaceAfterTypeAssertion
}
func insertSpaceBeforeFunctionParenthesisOption(options *FormatCodeSettings) core.Tristate {
return options.InsertSpaceBeforeFunctionParenthesis
}
func placeOpenBraceOnNewLineForFunctionsOption(options *FormatCodeSettings) core.Tristate {
return options.PlaceOpenBraceOnNewLineForFunctions
}
func placeOpenBraceOnNewLineForControlBlocksOption(options *FormatCodeSettings) core.Tristate {
return options.PlaceOpenBraceOnNewLineForControlBlocks
}
func insertSpaceBeforeTypeAnnotationOption(options *FormatCodeSettings) core.Tristate {
return options.InsertSpaceBeforeTypeAnnotation
}
func indentMultiLineObjectLiteralBeginningOnBlankLineOption(options *FormatCodeSettings) core.Tristate {
return options.IndentMultiLineObjectLiteralBeginningOnBlankLine
}
func indentSwitchCaseOption(options *FormatCodeSettings) core.Tristate {
return options.IndentSwitchCase
}
func optionEquals[T comparable](optionName anyOptionSelector[T], optionValue T) contextPredicate {
return func(context *formattingContext) bool {
if context.Options == nil {
return false
}
return optionName(context.Options) == optionValue
}
}
func isOptionEnabled(optionName optionSelector) contextPredicate {
return func(context *formattingContext) bool {
if context.Options == nil {
return false
}
return optionName(context.Options).IsTrue()
}
}
func isOptionDisabled(optionName optionSelector) contextPredicate {
return func(context *formattingContext) bool {
if context.Options == nil {
return true
}
return optionName(context.Options).IsFalse()
}
}
func isOptionDisabledOrUndefined(optionName optionSelector) contextPredicate {
return func(context *formattingContext) bool {
if context.Options == nil {
return true
}
return optionName(context.Options).IsFalseOrUnknown()
}
}
func isOptionDisabledOrUndefinedOrTokensOnSameLine(optionName optionSelector) contextPredicate {
return func(context *formattingContext) bool {
if context.Options == nil {
return true
}
return optionName(context.Options).IsFalseOrUnknown() || context.TokensAreOnSameLine()
}
}
func isOptionEnabledOrUndefined(optionName optionSelector) contextPredicate {
return func(context *formattingContext) bool {
if context.Options == nil {
return true
}
return optionName(context.Options).IsTrueOrUnknown()
}
}
func isForContext(context *formattingContext) bool {
return context.contextNode.Kind == ast.KindForStatement
}
func isNotForContext(context *formattingContext) bool {
return !isForContext(context)
}
func isBinaryOpContext(context *formattingContext) bool {
switch context.contextNode.Kind {
case ast.KindBinaryExpression:
return context.contextNode.AsBinaryExpression().OperatorToken.Kind != ast.KindCommaToken
case ast.KindConditionalExpression,
ast.KindConditionalType,
ast.KindAsExpression,
ast.KindExportSpecifier,
ast.KindImportSpecifier,
ast.KindTypePredicate,
ast.KindUnionType,
ast.KindIntersectionType,
ast.KindSatisfiesExpression:
return true
// equals in binding elements func foo([[x, y] = [1, 2]])
case ast.KindBindingElement:
// equals in type X = ...
fallthrough
case ast.KindTypeAliasDeclaration:
// equal in import a = module('a');
fallthrough
case ast.KindImportEqualsDeclaration:
// equal in export = 1
fallthrough
case ast.KindExportAssignment:
// equal in let a = 0
fallthrough
case ast.KindVariableDeclaration:
// equal in p = 0
fallthrough
case ast.KindParameter,
ast.KindEnumMember,
ast.KindPropertyDeclaration,
ast.KindPropertySignature:
return context.currentTokenSpan.Kind == ast.KindEqualsToken || context.nextTokenSpan.Kind == ast.KindEqualsToken
// "in" keyword in for (let x in []) { }
case ast.KindForInStatement:
// "in" keyword in [P in keyof T] T[P]
fallthrough
case ast.KindTypeParameter:
return context.currentTokenSpan.Kind == ast.KindInKeyword || context.nextTokenSpan.Kind == ast.KindInKeyword || context.currentTokenSpan.Kind == ast.KindEqualsToken || context.nextTokenSpan.Kind == ast.KindEqualsToken
// Technically, "of" is not a binary operator, but format it the same way as "in"
case ast.KindForOfStatement:
return context.currentTokenSpan.Kind == ast.KindOfKeyword || context.nextTokenSpan.Kind == ast.KindOfKeyword
}
return false
}
func isNotBinaryOpContext(context *formattingContext) bool {
return !isBinaryOpContext(context)
}
func isNotTypeAnnotationContext(context *formattingContext) bool {
return !isTypeAnnotationContext(context)
}
func isTypeAnnotationContext(context *formattingContext) bool {
contextKind := context.contextNode.Kind
return contextKind == ast.KindPropertyDeclaration ||
contextKind == ast.KindPropertySignature ||
contextKind == ast.KindParameter ||
contextKind == ast.KindVariableDeclaration ||
ast.IsFunctionLikeKind(contextKind)
}
func isOptionalPropertyContext(context *formattingContext) bool {
return ast.IsPropertyDeclaration(context.contextNode) && context.contextNode.AsPropertyDeclaration().PostfixToken != nil && context.contextNode.AsPropertyDeclaration().PostfixToken.Kind == ast.KindQuestionToken
}
func isNonOptionalPropertyContext(context *formattingContext) bool {
return !isOptionalPropertyContext(context)
}
func isConditionalOperatorContext(context *formattingContext) bool {
return context.contextNode.Kind == ast.KindConditionalExpression ||
context.contextNode.Kind == ast.KindConditionalType
}
func isSameLineTokenOrBeforeBlockContext(context *formattingContext) bool {
return context.TokensAreOnSameLine() || isBeforeBlockContext(context)
}
func isBraceWrappedContext(context *formattingContext) bool {
return context.contextNode.Kind == ast.KindObjectBindingPattern ||
context.contextNode.Kind == ast.KindMappedType ||
isSingleLineBlockContext(context)
}
// This check is done before an open brace in a control construct, a function, or a typescript block declaration
func isBeforeMultilineBlockContext(context *formattingContext) bool {
return isBeforeBlockContext(context) && !(context.NextNodeAllOnSameLine() || context.NextNodeBlockIsOnOneLine())
}
func isMultilineBlockContext(context *formattingContext) bool {
return isBlockContext(context) && !(context.ContextNodeAllOnSameLine() || context.ContextNodeBlockIsOnOneLine())
}
func isSingleLineBlockContext(context *formattingContext) bool {
return isBlockContext(context) && (context.ContextNodeAllOnSameLine() || context.ContextNodeBlockIsOnOneLine())
}
func isBlockContext(context *formattingContext) bool {
return nodeIsBlockContext(context.contextNode)
}
func isBeforeBlockContext(context *formattingContext) bool {
return nodeIsBlockContext(context.nextTokenParent)
}
// IMPORTANT!!! This method must return true ONLY for nodes with open and close braces as immediate children
func nodeIsBlockContext(node *ast.Node) bool {
if nodeIsTypeScriptDeclWithBlockContext(node) {
// This means we are in a context that looks like a block to the user, but in the grammar is actually not a node (it's a class, module, enum, object type literal, etc).
return true
}
switch node.Kind {
case ast.KindBlock,
ast.KindCaseBlock,
ast.KindObjectLiteralExpression,
ast.KindModuleBlock:
return true
}
return false
}
func isFunctionDeclContext(context *formattingContext) bool {
switch context.contextNode.Kind {
case ast.KindFunctionDeclaration,
ast.KindMethodDeclaration,
ast.KindMethodSignature:
// case ast.KindMemberFunctionDeclaration:
fallthrough
case ast.KindGetAccessor,
ast.KindSetAccessor:
// case ast.KindMethodSignature:
fallthrough
case ast.KindCallSignature,
ast.KindFunctionExpression,
ast.KindConstructor,
ast.KindArrowFunction:
// case ast.KindConstructorDeclaration:
// case ast.KindSimpleArrowFunctionExpression:
// case ast.KindParenthesizedArrowFunctionExpression:
fallthrough
case ast.KindInterfaceDeclaration: // This one is not truly a function, but for formatting purposes, it acts just like one
return true
}
return false
}
func isNotFunctionDeclContext(context *formattingContext) bool {
return !isFunctionDeclContext(context)
}
func isFunctionDeclarationOrFunctionExpressionContext(context *formattingContext) bool {
return context.contextNode.Kind == ast.KindFunctionDeclaration || context.contextNode.Kind == ast.KindFunctionExpression
}
func isTypeScriptDeclWithBlockContext(context *formattingContext) bool {
return nodeIsTypeScriptDeclWithBlockContext(context.contextNode)
}
func nodeIsTypeScriptDeclWithBlockContext(node *ast.Node) bool {
switch node.Kind {
case ast.KindClassDeclaration,
ast.KindClassExpression,
ast.KindInterfaceDeclaration,
ast.KindEnumDeclaration,
ast.KindTypeLiteral,
ast.KindModuleDeclaration,
ast.KindExportDeclaration,
ast.KindNamedExports,
ast.KindImportDeclaration,
ast.KindNamedImports:
return true
}
return false
}
func isAfterCodeBlockContext(context *formattingContext) bool {
switch context.currentTokenParent.Kind {
case ast.KindClassDeclaration,
ast.KindModuleDeclaration,
ast.KindEnumDeclaration,
ast.KindCatchClause,
ast.KindModuleBlock,
ast.KindSwitchStatement:
return true
case ast.KindBlock:
blockParent := context.currentTokenParent.Parent
// In a codefix scenario, we can't rely on parents being set. So just always return true.
if blockParent == nil || blockParent.Kind != ast.KindArrowFunction && blockParent.Kind != ast.KindFunctionExpression {
return true
}
}
return false
}
func isControlDeclContext(context *formattingContext) bool {
switch context.contextNode.Kind {
case ast.KindIfStatement,
ast.KindSwitchStatement,
ast.KindForStatement,
ast.KindForInStatement,
ast.KindForOfStatement,
ast.KindWhileStatement,
ast.KindTryStatement,
ast.KindDoStatement,
ast.KindWithStatement:
// TODO
// case ast.KindElseClause:
fallthrough
case ast.KindCatchClause:
return true
default:
return false
}
}
func isObjectContext(context *formattingContext) bool {
return context.contextNode.Kind == ast.KindObjectLiteralExpression
}
func isFunctionCallContext(context *formattingContext) bool {
return context.contextNode.Kind == ast.KindCallExpression
}
func isNewContext(context *formattingContext) bool {
return context.contextNode.Kind == ast.KindNewExpression
}
func isFunctionCallOrNewContext(context *formattingContext) bool {
return isFunctionCallContext(context) || isNewContext(context)
}
func isPreviousTokenNotComma(context *formattingContext) bool {
return context.currentTokenSpan.Kind != ast.KindCommaToken
}
func isNextTokenNotCloseBracket(context *formattingContext) bool {
return context.nextTokenSpan.Kind != ast.KindCloseBracketToken
}
func isNextTokenNotCloseParen(context *formattingContext) bool {
return context.nextTokenSpan.Kind != ast.KindCloseParenToken
}
func isArrowFunctionContext(context *formattingContext) bool {
return context.contextNode.Kind == ast.KindArrowFunction
}
func isImportTypeContext(context *formattingContext) bool {
return context.contextNode.Kind == ast.KindImportType
}
func isNonJsxSameLineTokenContext(context *formattingContext) bool {
return context.TokensAreOnSameLine() && context.contextNode.Kind != ast.KindJsxText
}
func isNonJsxTextContext(context *formattingContext) bool {
return context.contextNode.Kind != ast.KindJsxText
}
func isNonJsxElementOrFragmentContext(context *formattingContext) bool {
return context.contextNode.Kind != ast.KindJsxElement && context.contextNode.Kind != ast.KindJsxFragment
}
func isJsxExpressionContext(context *formattingContext) bool {
return context.contextNode.Kind == ast.KindJsxExpression || context.contextNode.Kind == ast.KindJsxSpreadAttribute
}
func isNextTokenParentJsxAttribute(context *formattingContext) bool {
return context.nextTokenParent.Kind == ast.KindJsxAttribute || (context.nextTokenParent.Kind == ast.KindJsxNamespacedName && context.nextTokenParent.Parent.Kind == ast.KindJsxAttribute)
}
func isJsxAttributeContext(context *formattingContext) bool {
return context.contextNode.Kind == ast.KindJsxAttribute
}
func isNextTokenParentNotJsxNamespacedName(context *formattingContext) bool {
return context.nextTokenParent.Kind != ast.KindJsxNamespacedName
}
func isNextTokenParentJsxNamespacedName(context *formattingContext) bool {
return context.nextTokenParent.Kind == ast.KindJsxNamespacedName
}
func isJsxSelfClosingElementContext(context *formattingContext) bool {
return context.contextNode.Kind == ast.KindJsxSelfClosingElement
}
func isNotBeforeBlockInFunctionDeclarationContext(context *formattingContext) bool {
return !isFunctionDeclContext(context) && !isBeforeBlockContext(context)
}
func isEndOfDecoratorContextOnSameLine(context *formattingContext) bool {
return context.TokensAreOnSameLine() &&
ast.HasDecorators(context.contextNode) &&
nodeIsInDecoratorContext(context.currentTokenParent) &&
!nodeIsInDecoratorContext(context.nextTokenParent)
}
func nodeIsInDecoratorContext(node *ast.Node) bool {
for node != nil && ast.IsExpression(node) {
node = node.Parent
}
return node != nil && node.Kind == ast.KindDecorator
}
func isStartOfVariableDeclarationList(context *formattingContext) bool {
return context.currentTokenParent.Kind == ast.KindVariableDeclarationList &&
scanner.GetTokenPosOfNode(context.currentTokenParent, context.SourceFile, false) == context.currentTokenSpan.Loc.Pos()
}
func isNotFormatOnEnter(context *formattingContext) bool {
return context.FormattingRequestKind != FormatRequestKindFormatOnEnter
}
func isModuleDeclContext(context *formattingContext) bool {
return context.contextNode.Kind == ast.KindModuleDeclaration
}
func isObjectTypeContext(context *formattingContext) bool {
return context.contextNode.Kind == ast.KindTypeLiteral // && context.contextNode.parent.Kind != ast.KindInterfaceDeclaration;
}
func isConstructorSignatureContext(context *formattingContext) bool {
return context.contextNode.Kind == ast.KindConstructSignature
}
func isTypeArgumentOrParameterOrAssertion(token TextRangeWithKind, parent *ast.Node) bool {
if token.Kind != ast.KindLessThanToken && token.Kind != ast.KindGreaterThanToken {
return false
}
switch parent.Kind {
case ast.KindTypeReference,
ast.KindTypeAssertionExpression,
ast.KindTypeAliasDeclaration,
ast.KindClassDeclaration,
ast.KindClassExpression,
ast.KindInterfaceDeclaration,
ast.KindFunctionDeclaration,
ast.KindFunctionExpression,
ast.KindArrowFunction,
ast.KindMethodDeclaration,
ast.KindMethodSignature,
ast.KindCallSignature,
ast.KindConstructSignature,
ast.KindCallExpression,
ast.KindNewExpression,
ast.KindExpressionWithTypeArguments:
return true
default:
return false
}
}
func isTypeArgumentOrParameterOrAssertionContext(context *formattingContext) bool {
return isTypeArgumentOrParameterOrAssertion(context.currentTokenSpan, context.currentTokenParent) ||
isTypeArgumentOrParameterOrAssertion(context.nextTokenSpan, context.nextTokenParent)
}
func isTypeAssertionContext(context *formattingContext) bool {
return context.contextNode.Kind == ast.KindTypeAssertionExpression
}
func isNonTypeAssertionContext(context *formattingContext) bool {
return !isTypeAssertionContext(context)
}
func isVoidOpContext(context *formattingContext) bool {
return context.currentTokenSpan.Kind == ast.KindVoidKeyword && context.currentTokenParent.Kind == ast.KindVoidExpression
}
func isYieldOrYieldStarWithOperand(context *formattingContext) bool {
return context.contextNode.Kind == ast.KindYieldExpression && context.contextNode.AsYieldExpression().Expression != nil
}
func isNonNullAssertionContext(context *formattingContext) bool {
return context.contextNode.Kind == ast.KindNonNullExpression
}
func isNotStatementConditionContext(context *formattingContext) bool {
return !isStatementConditionContext(context)
}
func isStatementConditionContext(context *formattingContext) bool {
switch context.contextNode.Kind {
case ast.KindIfStatement,
ast.KindForStatement,
ast.KindForInStatement,
ast.KindForOfStatement,
ast.KindDoStatement,
ast.KindWhileStatement:
return true
default:
return false
}
}
func isSemicolonDeletionContext(context *formattingContext) bool {
nextTokenKind := context.nextTokenSpan.Kind
nextTokenStart := context.nextTokenSpan.Loc.Pos()
if ast.IsTrivia(nextTokenKind) {
var nextRealToken *ast.Node
if context.nextTokenParent == context.currentTokenParent {
// !!! TODO: very different from strada, but strada's logic here is wonky - find the first ancestor without a parent? that's just the source file.
nextRealToken = astnav.FindNextToken(context.nextTokenParent, context.SourceFile.AsNode(), context.SourceFile)
} else {
nextRealToken = lsutil.GetFirstToken(context.nextTokenParent, context.SourceFile)
}
if nextRealToken == nil {
return true
}
nextTokenKind = nextRealToken.Kind
nextTokenStart = scanner.GetTokenPosOfNode(nextRealToken, context.SourceFile, false)
}
startLine, _ := scanner.GetECMALineAndCharacterOfPosition(context.SourceFile, context.currentTokenSpan.Loc.Pos())
endLine, _ := scanner.GetECMALineAndCharacterOfPosition(context.SourceFile, nextTokenStart)
if startLine == endLine {
return nextTokenKind == ast.KindCloseBraceToken || nextTokenKind == ast.KindEndOfFile
}
if nextTokenKind == ast.KindSemicolonToken &&
context.currentTokenSpan.Kind == ast.KindSemicolonToken {
return true
}
if nextTokenKind == ast.KindSemicolonClassElement ||
nextTokenKind == ast.KindSemicolonToken {
return false
}
if context.contextNode.Kind == ast.KindInterfaceDeclaration ||
context.contextNode.Kind == ast.KindTypeAliasDeclaration {
// Can't remove semicolon after `foo`; it would parse as a method declaration:
//
// interface I {
// foo;
// () void
// }
return context.currentTokenParent.Kind != ast.KindPropertySignature ||
context.currentTokenParent.Type() != nil ||
nextTokenKind != ast.KindOpenParenToken
}
if ast.IsPropertyDeclaration(context.currentTokenParent) {
return context.currentTokenParent.Initializer() == nil
}
return context.currentTokenParent.Kind != ast.KindForStatement &&
context.currentTokenParent.Kind != ast.KindEmptyStatement &&
context.currentTokenParent.Kind != ast.KindSemicolonClassElement &&
nextTokenKind != ast.KindOpenBracketToken &&
nextTokenKind != ast.KindOpenParenToken &&
nextTokenKind != ast.KindPlusToken &&
nextTokenKind != ast.KindMinusToken &&
nextTokenKind != ast.KindSlashToken &&
nextTokenKind != ast.KindRegularExpressionLiteral &&
nextTokenKind != ast.KindCommaToken &&
nextTokenKind != ast.KindTemplateExpression &&
nextTokenKind != ast.KindTemplateHead &&
nextTokenKind != ast.KindNoSubstitutionTemplateLiteral &&
nextTokenKind != ast.KindDotToken
}
func isSemicolonInsertionContext(context *formattingContext) bool {
return lsutil.PositionIsASICandidate(context.currentTokenSpan.Loc.End(), context.currentTokenParent, context.SourceFile)
}
func isNotPropertyAccessOnIntegerLiteral(context *formattingContext) bool {
return !ast.IsPropertyAccessExpression(context.contextNode) ||
!ast.IsNumericLiteral(context.contextNode.Expression()) ||
strings.Contains(context.contextNode.Expression().Text(), ".")
}