package ls import ( "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/debug" ) type Import struct { name string kind ImportKind // ImportKindCommonJS | ImportKindNamespace addAsTypeOnly AddAsTypeOnly propertyName string // Use when needing to generate an `ImportSpecifier with a `propertyName`; the name preceding "as" keyword (propertyName = "" when "as" is absent) } func (ct *changeTracker) addNamespaceQualifier(sourceFile *ast.SourceFile, qualification *Qualification) { ct.insertText(sourceFile, qualification.usagePosition, qualification.namespacePrefix+".") } func (ct *changeTracker) doAddExistingFix( sourceFile *ast.SourceFile, clause *ast.Node, // ImportClause | ObjectBindingPattern, defaultImport *Import, namedImports []*Import, // removeExistingImportSpecifiers *core.Set[ImportSpecifier | BindingElement] // !!! remove imports not implemented preferences *UserPreferences, ) { switch clause.Kind { case ast.KindObjectBindingPattern: if clause.Kind == ast.KindObjectBindingPattern { // bindingPattern := clause.AsBindingPattern() // !!! adding *and* removing imports not implemented // if (removeExistingImportSpecifiers && core.Some(bindingPattern.Elements, func(e *ast.Node) bool { // return removeExistingImportSpecifiers.Has(e) // })) { // If we're both adding and removing elements, just replace and reprint the whole // node. The change tracker doesn't understand all the operations and can insert or // leave behind stray commas. // ct.replaceNode( // sourceFile, // bindingPattern, // ct.NodeFactory.NewObjectBindingPattern([ // ...bindingPattern.Elements.Filter(func(e *ast.Node) bool { // return !removeExistingImportSpecifiers.Has(e) // }), // ...defaultImport ? [ct.NodeFactory.createBindingElement(/*dotDotDotToken*/ nil, /*propertyName*/ "default", defaultImport.name)] : emptyArray, // ...namedImports.map(i => ct.NodeFactory.createBindingElement(/*dotDotDotToken*/ nil, i.propertyName, i.name)), // ]), // ) // return // } if defaultImport != nil { ct.addElementToBindingPattern(sourceFile, clause, defaultImport.name, ptrTo("default")) } for _, specifier := range namedImports { ct.addElementToBindingPattern(sourceFile, clause, specifier.name, &specifier.propertyName) } return } case ast.KindImportClause: importClause := clause.AsImportClause() // promoteFromTypeOnly = true if we need to promote the entire original clause from type only promoteFromTypeOnly := importClause.IsTypeOnly() && core.Some(append(namedImports, defaultImport), func(i *Import) bool { if i == nil { return false } return i.addAsTypeOnly == AddAsTypeOnlyNotAllowed }) existingSpecifiers := []*ast.Node{} // []*ast.ImportSpecifier if importClause.NamedBindings != nil && importClause.NamedBindings.Kind == ast.KindNamedImports { existingSpecifiers = importClause.NamedBindings.Elements() } if defaultImport != nil { debug.Assert(clause.Name() == nil, "Cannot add a default import to an import clause that already has one") ct.insertNodeAt(sourceFile, core.TextPos(astnav.GetStartOfNode(clause, sourceFile, false)), ct.NodeFactory.NewIdentifier(defaultImport.name), changeNodeOptions{suffix: ", "}) } if len(namedImports) > 0 { // !!! OrganizeImports not yet implemented // specifierComparer, isSorted := OrganizeImports.getNamedImportSpecifierComparerWithDetection(importClause.Parent, preferences, sourceFile); newSpecifiers := core.Map(namedImports, func(namedImport *Import) *ast.Node { var identifier *ast.Node if namedImport.propertyName != "" { identifier = ct.NodeFactory.NewIdentifier(namedImport.propertyName).AsIdentifier().AsNode() } return ct.NodeFactory.NewImportSpecifier( (!importClause.IsTypeOnly() || promoteFromTypeOnly) && shouldUseTypeOnly(namedImport.addAsTypeOnly, preferences), identifier, ct.NodeFactory.NewIdentifier(namedImport.name), ) }) // !!! sort with specifierComparer // !!! remove imports not implemented // if (removeExistingImportSpecifiers) { // // If we're both adding and removing specifiers, just replace and reprint the whole // // node. The change tracker doesn't understand all the operations and can insert or // // leave behind stray commas. // ct.replaceNode( // sourceFile, // importClause.NamedBindings, // ct.NodeFactory.updateNamedImports( // importClause.NamedBindings.AsNamedImports(), // append(core.Filter(existingSpecifiers, func (s *ast.ImportSpecifier) bool {return !removeExistingImportSpecifiers.Has(s)}), newSpecifiers...), // !!! sort with specifierComparer // ), // ); // } else if (len(existingSpecifiers) > 0 && isSorted != false) { // !!! OrganizeImports not implemented // The sorting preference computed earlier may or may not have validated that these particular // import specifiers are sorted. If they aren't, `getImportSpecifierInsertionIndex` will return // nonsense. So if there are existing specifiers, even if we know the sorting preference, we // need to ensure that the existing specifiers are sorted according to the preference in order // to do a sorted insertion. // changed to check if existing specifiers are sorted // if we're promoting the clause from type-only, we need to transform the existing imports before attempting to insert the new named imports // transformedExistingSpecifiers := existingSpecifiers // if promoteFromTypeOnly && existingSpecifiers { // transformedExistingSpecifiers = ct.NodeFactory.updateNamedImports( // importClause.NamedBindings.AsNamedImports(), // core.SameMap(existingSpecifiers, func(e *ast.ImportSpecifier) *ast.ImportSpecifier { // return ct.NodeFactory.updateImportSpecifier(e, /*isTypeOnly*/ true, e.propertyName, e.name) // }), // ).elements // } // for _, spec := range newSpecifiers { // insertionIndex := OrganizeImports.getImportSpecifierInsertionIndex(transformedExistingSpecifiers, spec, specifierComparer); // ct.insertImportSpecifierAtIndex(sourceFile, spec, importClause.namedBindings as NamedImports, insertionIndex); // } // } else if len(existingSpecifiers) > 0 { for _, spec := range newSpecifiers { ct.insertNodeInListAfter(sourceFile, existingSpecifiers[len(existingSpecifiers)-1], spec.AsNode(), existingSpecifiers) } } else { if len(newSpecifiers) > 0 { namedImports := ct.NodeFactory.NewNamedImports(ct.NodeFactory.NewNodeList(newSpecifiers)) if importClause.NamedBindings != nil { ct.replaceNode(sourceFile, importClause.NamedBindings, namedImports, nil) } else { if clause.Name() == nil { panic("Import clause must have either named imports or a default import") } ct.insertNodeAfter(sourceFile, clause.Name(), namedImports) } } } } if promoteFromTypeOnly { // !!! promote type-only imports not implemented // ct.delete(sourceFile, getTypeKeywordOfTypeOnlyImport(clause, sourceFile)); // if (existingSpecifiers) { // // We used to convert existing specifiers to type-only only if compiler options indicated that // // would be meaningful (see the `importNameElisionDisabled` utility function), but user // // feedback indicated a preference for preserving the type-onlyness of existing specifiers // // regardless of whether it would make a difference in emit. // for _, specifier := range existingSpecifiers { // ct.insertModifierBefore(sourceFile, SyntaxKind.TypeKeyword, specifier); // } // } } default: panic("Unsupported clause kind: " + clause.Kind.String() + "for doAddExistingFix") } } func (ct *changeTracker) addElementToBindingPattern(sourceFile *ast.SourceFile, bindingPattern *ast.Node, name string, propertyName *string) { element := ct.newBindingElementFromNameAndPropertyName(name, propertyName) if len(bindingPattern.Elements()) > 0 { ct.insertNodeInListAfter(sourceFile, bindingPattern.Elements()[len(bindingPattern.Elements())-1], element, nil) } else { ct.replaceNode(sourceFile, bindingPattern, ct.NodeFactory.NewBindingPattern( ast.KindObjectBindingPattern, ct.NodeFactory.NewNodeList([]*ast.Node{element}), ), nil) } } func (ct *changeTracker) newBindingElementFromNameAndPropertyName(name string, propertyName *string) *ast.Node { var newPropertyNameIdentifier *ast.Node if propertyName != nil { newPropertyNameIdentifier = ct.NodeFactory.NewIdentifier(*propertyName) } return ct.NodeFactory.NewBindingElement( nil, /*dotDotDotToken*/ newPropertyNameIdentifier, ct.NodeFactory.NewIdentifier(name), nil, /* initializer */ ) } func (ct *changeTracker) insertImports(sourceFile *ast.SourceFile, imports []*ast.Statement, blankLineBetween bool, preferences *UserPreferences) { var existingImportStatements []*ast.Statement if imports[0].Kind == ast.KindVariableStatement { existingImportStatements = core.Filter(sourceFile.Statements.Nodes, ast.IsRequireVariableStatement) } else { existingImportStatements = core.Filter(sourceFile.Statements.Nodes, ast.IsAnyImportSyntax) } // !!! OrganizeImports // { comparer, isSorted } := OrganizeImports.getOrganizeImportsStringComparerWithDetection(existingImportStatements, preferences); // sortedNewImports := isArray(imports) ? toSorted(imports, (a, b) => OrganizeImports.compareImportsOrRequireStatements(a, b, comparer)) : [imports]; sortedNewImports := imports // !!! FutureSourceFile // if !isFullSourceFile(sourceFile) { // for _, newImport := range sortedNewImports { // // Insert one at a time to send correct original source file for accurate text reuse // // when some imports are cloned from existing ones in other files. // ct.insertStatementsInNewFile(sourceFile.fileName, []*ast.Node{newImport}, ast.GetSourceFileOfNode(getOriginalNode(newImport))) // } // return; // } // if len(existingImportStatements) > 0 && isSorted { // for _, newImport := range sortedNewImports { // insertionIndex := OrganizeImports.getImportDeclarationInsertionIndex(existingImportStatements, newImport, comparer) // if insertionIndex == 0 { // // If the first import is top-of-file, insert after the leading comment which is likely the header. // options := existingImportStatements[0] == sourceFile.statements[0] ? { leadingTriviaOption: textchanges.LeadingTriviaOption.Exclude } : {}; // ct.insertNodeBefore(sourceFile, existingImportStatements[0], newImport, /*blankLineBetween*/ false, options); // } else { // prevImport := existingImportStatements[insertionIndex - 1] // ct.insertNodeAfter(sourceFile, prevImport, newImport); // } // } // return // } if len(existingImportStatements) > 0 { ct.insertNodesAfter(sourceFile, existingImportStatements[len(existingImportStatements)-1], sortedNewImports) } else { ct.insertAtTopOfFile(sourceFile, sortedNewImports, blankLineBetween) } } func (ct *changeTracker) makeImport(defaultImport *ast.IdentifierNode, namedImports []*ast.Node, moduleSpecifier *ast.Expression, isTypeOnly bool) *ast.Statement { var newNamedImports *ast.Node if len(namedImports) > 0 { newNamedImports = ct.NodeFactory.NewNamedImports(ct.NodeFactory.NewNodeList(namedImports)) } var importClause *ast.Node if defaultImport != nil || newNamedImports != nil { importClause = ct.NodeFactory.NewImportClause(core.IfElse(isTypeOnly, ast.KindTypeKeyword, ast.KindUnknown), defaultImport, newNamedImports) } return ct.NodeFactory.NewImportDeclaration( /*modifiers*/ nil, importClause, moduleSpecifier, nil /*attributes*/) } func (ct *changeTracker) getNewImports( moduleSpecifier string, // quotePreference quotePreference, // !!! quotePreference defaultImport *Import, namedImports []*Import, namespaceLikeImport *Import, // { importKind: ImportKind.CommonJS | ImportKind.Namespace; } compilerOptions *core.CompilerOptions, preferences *UserPreferences, ) []*ast.Statement { moduleSpecifierStringLiteral := ct.NodeFactory.NewStringLiteral(moduleSpecifier) var statements []*ast.Statement // []AnyImportSyntax if defaultImport != nil || len(namedImports) > 0 { // `verbatimModuleSyntax` should prefer top-level `import type` - // even though it's not an error, it would add unnecessary runtime emit. topLevelTypeOnly := (defaultImport == nil || needsTypeOnly(defaultImport.addAsTypeOnly)) && core.Every(namedImports, func(i *Import) bool { return needsTypeOnly(i.addAsTypeOnly) }) || (compilerOptions.VerbatimModuleSyntax.IsTrue() || preferences.PreferTypeOnlyAutoImports) && defaultImport != nil && defaultImport.addAsTypeOnly != AddAsTypeOnlyNotAllowed && !core.Some(namedImports, func(i *Import) bool { return i.addAsTypeOnly == AddAsTypeOnlyNotAllowed }) var defaultImportNode *ast.Node if defaultImport != nil { defaultImportNode = ct.NodeFactory.NewIdentifier(defaultImport.name) } statements = append(statements, ct.makeImport(defaultImportNode, core.Map(namedImports, func(namedImport *Import) *ast.Node { var namedImportPropertyName *ast.Node if namedImport.propertyName != "" { namedImportPropertyName = ct.NodeFactory.NewIdentifier(namedImport.propertyName) } return ct.NodeFactory.NewImportSpecifier( !topLevelTypeOnly && shouldUseTypeOnly(namedImport.addAsTypeOnly, preferences), namedImportPropertyName, ct.NodeFactory.NewIdentifier(namedImport.name), ) }), moduleSpecifierStringLiteral, topLevelTypeOnly)) } if namespaceLikeImport != nil { var declaration *ast.Statement if namespaceLikeImport.kind == ImportKindCommonJS { declaration = ct.NodeFactory.NewImportEqualsDeclaration( /*modifiers*/ nil, shouldUseTypeOnly(namespaceLikeImport.addAsTypeOnly, preferences), ct.NodeFactory.NewIdentifier(namespaceLikeImport.name), ct.NodeFactory.NewExternalModuleReference(moduleSpecifierStringLiteral), ) } else { declaration = ct.NodeFactory.NewImportDeclaration( /*modifiers*/ nil, ct.NodeFactory.NewImportClause( /*phaseModifier*/ core.IfElse(shouldUseTypeOnly(namespaceLikeImport.addAsTypeOnly, preferences), ast.KindTypeKeyword, ast.KindUnknown), /*name*/ nil, ct.NodeFactory.NewNamespaceImport(ct.NodeFactory.NewIdentifier(namespaceLikeImport.name)), ), moduleSpecifierStringLiteral, /*attributes*/ nil, ) } statements = append(statements, declaration) } if len(statements) == 0 { panic("No statements to insert for new imports") } return statements } func needsTypeOnly(addAsTypeOnly AddAsTypeOnly) bool { return addAsTypeOnly == AddAsTypeOnlyRequired } func shouldUseTypeOnly(addAsTypeOnly AddAsTypeOnly, preferences *UserPreferences) bool { return needsTypeOnly(addAsTypeOnly) || addAsTypeOnly != AddAsTypeOnlyNotAllowed && preferences.PreferTypeOnlyAutoImports }