diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 97be236ed0b9f..aeac7ceec6d74 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -848,38 +848,6 @@ export function arrayIsSorted(array: readonly T[], comparer: Comparer) { return true; } -/** @internal */ -export const enum SortKind { - None = 0, - CaseSensitive = 1 << 0, - CaseInsensitive = 1 << 1, - Both = CaseSensitive | CaseInsensitive, -} - -/** @internal */ -export function detectSortCaseSensitivity( - array: readonly T[], - getString: (element: T) => string, - compareStringsCaseSensitive: Comparer, - compareStringsCaseInsensitive: Comparer, -): SortKind { - let kind = SortKind.Both; - if (array.length < 2) return kind; - - let prevElement = getString(array[0]); - for (let i = 1, len = array.length; i < len && kind !== SortKind.None; i++) { - const element = getString(array[i]); - if (kind & SortKind.CaseSensitive && compareStringsCaseSensitive(prevElement, element) > 0) { - kind &= ~SortKind.CaseSensitive; - } - if (kind & SortKind.CaseInsensitive && compareStringsCaseInsensitive(prevElement, element) > 0) { - kind &= ~SortKind.CaseInsensitive; - } - prevElement = element; - } - return kind; -} - /** @internal */ export function arrayIsEqualTo(array1: readonly T[] | undefined, array2: readonly T[] | undefined, equalityComparer: (a: T, b: T, index: number) => boolean = equateValues): boolean { if (!array1 || !array2) { diff --git a/src/compiler/types.ts b/src/compiler/types.ts index bc6d608584359..30feee6d61e52 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -9969,7 +9969,7 @@ export interface UserPreferences { * * Default: `last` */ - readonly organizeImportsTypeOrder?: "first" | "last" | "inline"; + readonly organizeImportsTypeOrder?: OrganizeImportsTypeOrder; /** * Indicates whether to exclude standard library and node_modules file symbols from navTo results. */ @@ -9980,6 +9980,8 @@ export interface UserPreferences { readonly disableLineTextInReferences?: boolean; } +export type OrganizeImportsTypeOrder = "last" | "inline" | "first"; + /** Represents a bigint literal value without requiring bigint support */ export interface PseudoBigInt { negative: boolean; diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index 9d67eeece06e2..80400b140438e 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -114,6 +114,7 @@ import { removeFileExtension, removeSuffix, RequireVariableStatement, + sameMap, ScriptTarget, SemanticMeaning, shouldUseUriStyleNodeCoreModules, @@ -121,7 +122,6 @@ import { skipAlias, some, sort, - SortKind, SourceFile, stableSort, startsWith, @@ -1394,11 +1394,10 @@ function promoteFromTypeOnly( switch (aliasDeclaration.kind) { case SyntaxKind.ImportSpecifier: if (aliasDeclaration.isTypeOnly) { - const sortKind = OrganizeImports.detectImportSpecifierSorting(aliasDeclaration.parent.elements, preferences); - if (aliasDeclaration.parent.elements.length > 1 && sortKind) { + if (aliasDeclaration.parent.elements.length > 1) { const newSpecifier = factory.updateImportSpecifier(aliasDeclaration, /*isTypeOnly*/ false, aliasDeclaration.propertyName, aliasDeclaration.name); - const comparer = OrganizeImports.getOrganizeImportsComparer(preferences, sortKind === SortKind.CaseInsensitive); - const insertionIndex = OrganizeImports.getImportSpecifierInsertionIndex(aliasDeclaration.parent.elements, newSpecifier, comparer, preferences); + const { specifierComparer } = OrganizeImports.getNamedImportSpecifierComparerWithDetection(aliasDeclaration.parent.parent.parent, preferences, sourceFile); + const insertionIndex = OrganizeImports.getImportSpecifierInsertionIndex(aliasDeclaration.parent.elements, newSpecifier, specifierComparer); if (insertionIndex !== aliasDeclaration.parent.elements.indexOf(aliasDeclaration)) { changes.delete(sourceFile, aliasDeclaration); changes.insertImportSpecifierAtIndex(sourceFile, newSpecifier, aliasDeclaration.parent, insertionIndex); @@ -1440,8 +1439,9 @@ function promoteFromTypeOnly( if (convertExistingToTypeOnly) { const namedImports = tryCast(importClause.namedBindings, isNamedImports); if (namedImports && namedImports.elements.length > 1) { + const sortState = OrganizeImports.getNamedImportSpecifierComparerWithDetection(importClause.parent, preferences, sourceFile); if ( - OrganizeImports.detectImportSpecifierSorting(namedImports.elements, preferences) && + (sortState.isSorted !== false) && aliasDeclaration.kind === SyntaxKind.ImportSpecifier && namedImports.elements.indexOf(aliasDeclaration) !== 0 ) { @@ -1478,6 +1478,7 @@ function doAddExistingFix( return; } + // promoteFromTypeOnly = true if we need to promote the entire original clause from type only const promoteFromTypeOnly = clause.isTypeOnly && some([defaultImport, ...namedImports], i => i?.addAsTypeOnly === AddAsTypeOnly.NotAllowed); const existingSpecifiers = clause.namedBindings && tryCast(clause.namedBindings, isNamedImports)?.elements; @@ -1487,25 +1488,7 @@ function doAddExistingFix( } if (namedImports.length) { - // sort case sensitivity: - // - if the user preference is explicit, use that - // - otherwise, if there are enough existing import specifiers in this import to detect unambiguously, use that - // - otherwise, detect from other imports in the file - let ignoreCaseForSorting: boolean | undefined; - if (typeof preferences.organizeImportsIgnoreCase === "boolean") { - ignoreCaseForSorting = preferences.organizeImportsIgnoreCase; - } - else if (existingSpecifiers) { - const targetImportSorting = OrganizeImports.detectImportSpecifierSorting(existingSpecifiers, preferences); - if (targetImportSorting !== SortKind.Both) { - ignoreCaseForSorting = targetImportSorting === SortKind.CaseInsensitive; - } - } - if (ignoreCaseForSorting === undefined) { - ignoreCaseForSorting = OrganizeImports.detectSorting(sourceFile, preferences) === SortKind.CaseInsensitive; - } - - const comparer = OrganizeImports.getOrganizeImportsComparer(preferences, ignoreCaseForSorting); + const { specifierComparer, isSorted } = OrganizeImports.getNamedImportSpecifierComparerWithDetection(clause.parent, preferences, sourceFile); const newSpecifiers = stableSort( namedImports.map(namedImport => factory.createImportSpecifier( @@ -1514,7 +1497,7 @@ function doAddExistingFix( factory.createIdentifier(namedImport.name), ) ), - (s1, s2) => OrganizeImports.compareImportOrExportSpecifiers(s1, s2, comparer), + specifierComparer, ); // The sorting preference computed earlier may or may not have validated that these particular @@ -1522,15 +1505,16 @@ function doAddExistingFix( // 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. - const specifierSort = existingSpecifiers?.length && OrganizeImports.detectImportSpecifierSorting(existingSpecifiers, preferences); - if (specifierSort && !(ignoreCaseForSorting && specifierSort === SortKind.CaseSensitive)) { + + // changed to check if existing specifiers are sorted + if (existingSpecifiers?.length && isSorted !== false) { + // if we're promoting the clause from type-only, we need to transform the existing imports before attempting to insert the new named imports + const transformedExistingSpecifiers = (promoteFromTypeOnly && existingSpecifiers) ? factory.updateNamedImports( + clause.namedBindings as NamedImports, + sameMap(existingSpecifiers, e => factory.updateImportSpecifier(e, /*isTypeOnly*/ true, e.propertyName, e.name)), + ).elements : existingSpecifiers; for (const spec of newSpecifiers) { - // Organize imports puts type-only import specifiers last, so if we're - // adding a non-type-only specifier and converting all the other ones to - // type-only, there's no need to ask for the insertion index - it's 0. - const insertionIndex = promoteFromTypeOnly && !spec.isTypeOnly - ? 0 - : OrganizeImports.getImportSpecifierInsertionIndex(existingSpecifiers, spec, comparer, preferences); + const insertionIndex = OrganizeImports.getImportSpecifierInsertionIndex(transformedExistingSpecifiers, spec, specifierComparer); changes.insertImportSpecifierAtIndex(sourceFile, spec, clause.namedBindings as NamedImports, insertionIndex); } } diff --git a/src/services/organizeImports.ts b/src/services/organizeImports.ts index 20ddb07e00e1c..a6a300ed82df4 100644 --- a/src/services/organizeImports.ts +++ b/src/services/organizeImports.ts @@ -1,6 +1,5 @@ import { AnyImportOrRequireStatement, - arrayIsSorted, binarySearch, compareBooleans, Comparer, @@ -9,7 +8,6 @@ import { compareValues, Comparison, createScanner, - detectSortCaseSensitivity, EmitFlags, emptyArray, ExportDeclaration, @@ -17,13 +15,11 @@ import { Expression, factory, FileTextChanges, - find, FindAllReferences, firstOrUndefined, flatMap, formatting, getNewLineOrDefaultFromHost, - getStringComparer, getUILocale, group, groupBy, @@ -48,19 +44,17 @@ import { LanguageServiceHost, length, map, - MemoizeCache, - memoizeCached, NamedImportBindings, NamedImports, NamespaceImport, OrganizeImportsMode, + OrganizeImportsTypeOrder, Program, rangeIsOnSingleLine, Scanner, setEmitFlags, some, sort, - SortKind, SourceFile, stableSort, SyntaxKind, @@ -88,44 +82,63 @@ export function organizeImports( ): FileTextChanges[] { const changeTracker = textChanges.ChangeTracker.fromContext({ host, formatContext, preferences }); const shouldSort = mode === OrganizeImportsMode.SortAndCombine || mode === OrganizeImportsMode.All; - const shouldCombine = shouldSort; // These are currently inseparable, but I draw a distinction for clarity and in case we add modes in the future. + + // These are currently inseparable, but I draw a distinction for clarity and in case we add modes in the future. + const shouldCombine = shouldSort; const shouldRemove = mode === OrganizeImportsMode.RemoveUnused || mode === OrganizeImportsMode.All; + // All of the old ImportDeclarations in the file, in syntactic order. - const topLevelImportGroupDecls = groupByNewlineContiguous(sourceFile, sourceFile.statements.filter(isImportDeclaration)); + const topLevelImportDecls = sourceFile.statements.filter(isImportDeclaration); + const topLevelImportGroupDecls = groupByNewlineContiguous(sourceFile, topLevelImportDecls); - const comparer = getOrganizeImportsComparerWithDetection(preferences, shouldSort ? () => detectSortingWorker(topLevelImportGroupDecls, preferences) === SortKind.CaseInsensitive : undefined); + const { comparersToTest, typeOrdersToTest } = getDetectionLists(preferences); + const defaultComparer = comparersToTest[0]; - const processImportsOfSameModuleSpecifier = (importGroup: readonly ImportDeclaration[]) => { - if (shouldRemove) importGroup = removeUnusedImports(importGroup, sourceFile, program); - if (shouldCombine) importGroup = coalesceImportsWorker(importGroup, comparer, sourceFile, preferences); - if (shouldSort) importGroup = stableSort(importGroup, (s1, s2) => compareImportsOrRequireStatements(s1, s2, comparer)); - return importGroup; + // If case sensitivity is specified (true/false), then use the same setting for both. + const comparer = { + moduleSpecifierComparer: typeof preferences.organizeImportsIgnoreCase === "boolean" ? defaultComparer : undefined, + namedImportComparer: typeof preferences.organizeImportsIgnoreCase === "boolean" ? defaultComparer : undefined, + typeOrder: preferences.organizeImportsTypeOrder, }; + if (typeof preferences.organizeImportsIgnoreCase !== "boolean") { + // Otherwise, we must test for case-sensitivity. Named import case sensitivity will be tested with type order + ({ comparer: comparer.moduleSpecifierComparer } = detectModuleSpecifierCaseBySort(topLevelImportGroupDecls, comparersToTest)); + } - topLevelImportGroupDecls.forEach(importGroupDecl => organizeImportsWorker(importGroupDecl, processImportsOfSameModuleSpecifier)); + if (!comparer.typeOrder || typeof preferences.organizeImportsIgnoreCase !== "boolean") { + // Through getDetectionLists, the set of orders returned will be compatible + const namedImportSort = detectNamedImportOrganizationBySort(topLevelImportDecls, comparersToTest, typeOrdersToTest); + if (namedImportSort) { + const { namedImportComparer, typeOrder } = namedImportSort; + comparer.namedImportComparer = comparer.namedImportComparer ?? namedImportComparer; + comparer.typeOrder = comparer.typeOrder ?? typeOrder; + } + } + + topLevelImportGroupDecls.forEach(importGroupDecl => organizeImportsWorker(importGroupDecl, comparer)); // Exports are always used if (mode !== OrganizeImportsMode.RemoveUnused) { // All of the old ExportDeclarations in the file, in syntactic order. - getTopLevelExportGroups(sourceFile).forEach(exportGroupDecl => organizeImportsWorker(exportGroupDecl, group => coalesceExportsWorker(group, comparer, preferences))); + getTopLevelExportGroups(sourceFile).forEach(exportGroupDecl => organizeExportsWorker(exportGroupDecl, comparer.namedImportComparer)); } for (const ambientModule of sourceFile.statements.filter(isAmbientModule)) { if (!ambientModule.body) continue; const ambientModuleImportGroupDecls = groupByNewlineContiguous(sourceFile, ambientModule.body.statements.filter(isImportDeclaration)); - ambientModuleImportGroupDecls.forEach(importGroupDecl => organizeImportsWorker(importGroupDecl, processImportsOfSameModuleSpecifier)); + ambientModuleImportGroupDecls.forEach(importGroupDecl => organizeImportsWorker(importGroupDecl, comparer)); // Exports are always used if (mode !== OrganizeImportsMode.RemoveUnused) { const ambientModuleExportDecls = ambientModule.body.statements.filter(isExportDeclaration); - organizeImportsWorker(ambientModuleExportDecls, group => coalesceExportsWorker(group, comparer, preferences)); + organizeExportsWorker(ambientModuleExportDecls, comparer.namedImportComparer); } } return changeTracker.getChanges(); - function organizeImportsWorker( + function organizeDeclsWorker( oldImportDecls: readonly T[], coalesce: (group: readonly T[]) => readonly T[], ) { @@ -144,7 +157,7 @@ export function organizeImports( ? group(oldImportDecls, importDecl => getExternalModuleName(importDecl.moduleSpecifier)!) : [oldImportDecls]; const sortedImportGroups = shouldSort - ? stableSort(oldImportGroups, (group1, group2) => compareModuleSpecifiersWorker(group1[0].moduleSpecifier, group2[0].moduleSpecifier, comparer)) + ? stableSort(oldImportGroups, (group1, group2) => compareModuleSpecifiersWorker(group1[0].moduleSpecifier, group2[0].moduleSpecifier, comparer.moduleSpecifierComparer ?? defaultComparer)) : oldImportGroups; const newImportDecls = flatMap(sortedImportGroups, importGroup => getExternalModuleName(importGroup[0].moduleSpecifier) || importGroup[0].moduleSpecifier === undefined @@ -174,6 +187,38 @@ export function organizeImports( }, hasTrailingComment); } } + + function organizeImportsWorker(oldImportDecls: readonly ImportDeclaration[], comparer: { moduleSpecifierComparer?: Comparer; namedImportComparer?: Comparer; typeOrder?: OrganizeImportsTypeOrder; }) { + const detectedModuleCaseComparer = comparer.moduleSpecifierComparer ?? defaultComparer; + const detectedNamedImportCaseComparer = comparer.namedImportComparer ?? defaultComparer; + const detectedTypeOrder = comparer.typeOrder ?? "last"; + + const specifierComparer = getNamedImportSpecifierComparer({ organizeImportsTypeOrder: detectedTypeOrder }, detectedNamedImportCaseComparer); + const processImportsOfSameModuleSpecifier = (importGroup: readonly ImportDeclaration[]) => { + if (shouldRemove) importGroup = removeUnusedImports(importGroup, sourceFile, program); + if (shouldCombine) importGroup = coalesceImportsWorker(importGroup, detectedModuleCaseComparer, specifierComparer, sourceFile); + if (shouldSort) importGroup = stableSort(importGroup, (s1, s2) => compareImportsOrRequireStatements(s1, s2, detectedModuleCaseComparer)); + return importGroup; + }; + + organizeDeclsWorker(oldImportDecls, processImportsOfSameModuleSpecifier); + } + + function organizeExportsWorker(oldExportDecls: readonly ExportDeclaration[], specifierCaseComparer?: Comparer) { + const useComparer = getNamedImportSpecifierComparer(preferences, specifierCaseComparer); + organizeDeclsWorker(oldExportDecls, group => coalesceExportsWorker(group, useComparer)); + } +} + +/** @internal */ +export function getDetectionLists(preferences: UserPreferences): { comparersToTest: Comparer[]; typeOrdersToTest: OrganizeImportsTypeOrder[]; } { + // Returns the possible detection outcomes, given the user's preferences. The earlier in the list, the higher the priority. + return { + comparersToTest: typeof preferences.organizeImportsIgnoreCase === "boolean" + ? [getOrganizeImportsStringComparer(preferences, preferences.organizeImportsIgnoreCase)] + : [getOrganizeImportsStringComparer(preferences, /*ignoreCase*/ true), getOrganizeImportsStringComparer(preferences, /*ignoreCase*/ false)], + typeOrdersToTest: preferences.organizeImportsTypeOrder ? [preferences.organizeImportsTypeOrder] : ["last", "inline", "first"], + }; } function groupByNewlineContiguous(sourceFile: SourceFile, decls: T[]): T[][] { @@ -218,6 +263,37 @@ function isNewGroup(sourceFile: SourceFile, decl: ImportDeclaration | ExportDecl return false; } +function getTopLevelExportGroups(sourceFile: SourceFile) { + const topLevelExportGroups: ExportDeclaration[][] = []; + const statements = sourceFile.statements; + const len = length(statements); + + let i = 0; + let groupIndex = 0; + while (i < len) { + if (isExportDeclaration(statements[i])) { + if (topLevelExportGroups[groupIndex] === undefined) { + topLevelExportGroups[groupIndex] = []; + } + const exportDecl = statements[i] as ExportDeclaration; + if (exportDecl.moduleSpecifier) { + topLevelExportGroups[groupIndex].push(exportDecl); + i++; + } + else { + while (i < len && isExportDeclaration(statements[i])) { + topLevelExportGroups[groupIndex].push(statements[i++] as ExportDeclaration); + } + groupIndex++; + } + } + else { + i++; + } + } + return flatMap(topLevelExportGroups, exportGroupDecls => groupByNewlineContiguous(sourceFile, exportGroupDecls)); +} + function removeUnusedImports(oldImports: readonly ImportDeclaration[], sourceFile: SourceFile, program: Program) { const typeChecker = program.getTypeChecker(); const compilerOptions = program.getCompilerOptions(); @@ -293,32 +369,57 @@ function removeUnusedImports(oldImports: readonly ImportDeclaration[], sourceFil } } -function hasModuleDeclarationMatchingSpecifier(sourceFile: SourceFile, moduleSpecifier: Expression) { - const moduleSpecifierText = isStringLiteral(moduleSpecifier) && moduleSpecifier.text; - return isString(moduleSpecifierText) && some(sourceFile.moduleAugmentations, moduleName => - isStringLiteral(moduleName) - && moduleName.text === moduleSpecifierText); -} - function getExternalModuleName(specifier: Expression | undefined) { return specifier !== undefined && isStringLiteralLike(specifier) ? specifier.text : undefined; } -// Internal for testing -/** - * @param importGroup a list of ImportDeclarations, all with the same module name. +/* + * Returns entire import declarations because they may already have been rewritten and + * may lack parent pointers. The desired parts can easily be recovered based on the + * categorization. * - * @deprecated Only used for testing - * @internal + * NB: There may be overlap between `defaultImports` and `namespaceImports`/`namedImports`. */ -export function coalesceImports(importGroup: readonly ImportDeclaration[], ignoreCase: boolean, sourceFile?: SourceFile, preferences?: UserPreferences): readonly ImportDeclaration[] { - const comparer = getOrganizeImportsOrdinalStringComparer(ignoreCase); - return coalesceImportsWorker(importGroup, comparer, sourceFile, preferences); +function getCategorizedImports(importGroup: readonly ImportDeclaration[]) { + let importWithoutClause: ImportDeclaration | undefined; + const typeOnlyImports: ImportGroup = { defaultImports: [], namespaceImports: [], namedImports: [] }; + const regularImports: ImportGroup = { defaultImports: [], namespaceImports: [], namedImports: [] }; + + for (const importDeclaration of importGroup) { + if (importDeclaration.importClause === undefined) { + // Only the first such import is interesting - the others are redundant. + // Note: Unfortunately, we will lose trivia that was on this node. + importWithoutClause = importWithoutClause || importDeclaration; + continue; + } + + const group = importDeclaration.importClause.isTypeOnly ? typeOnlyImports : regularImports; + const { name, namedBindings } = importDeclaration.importClause; + + if (name) { + group.defaultImports.push(importDeclaration as ImportDeclarationWithDefaultImport); + } + + if (namedBindings) { + if (isNamespaceImport(namedBindings)) { + group.namespaceImports.push(importDeclaration as ImportDeclarationWithNamespaceImport); + } + else { + group.namedImports.push(importDeclaration as ImportDeclarationWithNamedImports); + } + } + } + + return { + importWithoutClause, + typeOnlyImports, + regularImports, + }; } -function coalesceImportsWorker(importGroup: readonly ImportDeclaration[], comparer: Comparer, sourceFile?: SourceFile, preferences?: UserPreferences): readonly ImportDeclaration[] { +function coalesceImportsWorker(importGroup: readonly ImportDeclaration[], comparer: Comparer, specifierComparer: Comparer, sourceFile?: SourceFile): readonly ImportDeclaration[] { if (importGroup.length === 0) { return importGroup; } @@ -391,7 +492,7 @@ function coalesceImportsWorker(importGroup: readonly ImportDeclaration[], compar newImportSpecifiers.push(...getNewImportSpecifiers(namedImports)); const sortedImportSpecifiers = factory.createNodeArray( - sortSpecifiers(newImportSpecifiers, comparer, preferences), + stableSort(newImportSpecifiers, specifierComparer), firstNamedImport?.importClause.namedBindings.elements.hasTrailingComma, ); @@ -458,63 +559,7 @@ interface ImportGroup { namedImports: ImportDeclarationWithNamedImports[]; } -/* - * Returns entire import declarations because they may already have been rewritten and - * may lack parent pointers. The desired parts can easily be recovered based on the - * categorization. - * - * NB: There may be overlap between `defaultImports` and `namespaceImports`/`namedImports`. - */ -function getCategorizedImports(importGroup: readonly ImportDeclaration[]) { - let importWithoutClause: ImportDeclaration | undefined; - const typeOnlyImports: ImportGroup = { defaultImports: [], namespaceImports: [], namedImports: [] }; - const regularImports: ImportGroup = { defaultImports: [], namespaceImports: [], namedImports: [] }; - - for (const importDeclaration of importGroup) { - if (importDeclaration.importClause === undefined) { - // Only the first such import is interesting - the others are redundant. - // Note: Unfortunately, we will lose trivia that was on this node. - importWithoutClause = importWithoutClause || importDeclaration; - continue; - } - - const group = importDeclaration.importClause.isTypeOnly ? typeOnlyImports : regularImports; - const { name, namedBindings } = importDeclaration.importClause; - - if (name) { - group.defaultImports.push(importDeclaration as ImportDeclarationWithDefaultImport); - } - - if (namedBindings) { - if (isNamespaceImport(namedBindings)) { - group.namespaceImports.push(importDeclaration as ImportDeclarationWithNamespaceImport); - } - else { - group.namedImports.push(importDeclaration as ImportDeclarationWithNamedImports); - } - } - } - - return { - importWithoutClause, - typeOnlyImports, - regularImports, - }; -} - -// Internal for testing -/** - * @param exportGroup a list of ExportDeclarations, all with the same module name. - * - * @deprecated Only used for testing - * @internal - */ -export function coalesceExports(exportGroup: readonly ExportDeclaration[], ignoreCase: boolean, preferences?: UserPreferences) { - const comparer = getOrganizeImportsOrdinalStringComparer(ignoreCase); - return coalesceExportsWorker(exportGroup, comparer, preferences); -} - -function coalesceExportsWorker(exportGroup: readonly ExportDeclaration[], comparer: Comparer, preferences?: UserPreferences) { +function coalesceExportsWorker(exportGroup: readonly ExportDeclaration[], specifierComparer: Comparer) { if (exportGroup.length === 0) { return exportGroup; } @@ -533,7 +578,7 @@ function coalesceExportsWorker(exportGroup: readonly ExportDeclaration[], compar const newExportSpecifiers: ExportSpecifier[] = []; newExportSpecifiers.push(...flatMap(exportGroup, i => i.exportClause && isNamedExports(i.exportClause) ? i.exportClause.elements : emptyArray)); - const sortedExportSpecifiers = sortSpecifiers(newExportSpecifiers, comparer, preferences); + const sortedExportSpecifiers = stableSort(newExportSpecifiers, specifierComparer); const exportDecl = exportGroup[0]; coalescedExports.push( @@ -600,12 +645,7 @@ function updateImportDeclarationAndClause( ); } -function sortSpecifiers(specifiers: readonly T[], comparer: Comparer, preferences?: UserPreferences): readonly T[] { - return stableSort(specifiers, (s1, s2) => compareImportOrExportSpecifiers(s1, s2, comparer, preferences)); -} - -/** @internal */ -export function compareImportOrExportSpecifiers(s1: T, s2: T, comparer: Comparer, preferences?: UserPreferences): Comparison { +function compareImportOrExportSpecifiers(s1: T, s2: T, comparer: Comparer, preferences?: UserPreferences): Comparison { switch (preferences?.organizeImportsTypeOrder) { case "first": return compareBooleans(s2.isTypeOnly, s1.isTypeOnly) || comparer(s1.name.text, s2.name.text); @@ -616,17 +656,6 @@ export function compareImportOrExportSpecifiers) { const name1 = m1 === undefined ? undefined : getExternalModuleName(m1); const name2 = m2 === undefined ? undefined : getExternalModuleName(m2); @@ -635,6 +664,10 @@ function compareModuleSpecifiersWorker(m1: Expression | undefined, m2: Expressio comparer(name1!, name2!); } +function getModuleNamesFromDecls(decls: readonly AnyImportOrRequireStatement[]): string[] { + return decls.map(s => getExternalModuleName(getModuleSpecifierExpression(s)) || ""); +} + function getModuleSpecifierExpression(declaration: AnyImportOrRequireStatement): Expression | undefined { switch (declaration.kind) { case SyntaxKind.ImportEqualsDeclaration: @@ -646,168 +679,132 @@ function getModuleSpecifierExpression(declaration: AnyImportOrRequireStatement): } } -/** @internal */ -export function detectSorting(sourceFile: SourceFile, preferences: UserPreferences): SortKind { - return detectSortingWorker( - groupByNewlineContiguous(sourceFile, sourceFile.statements.filter(isImportDeclaration)), - preferences, - ); +function hasModuleDeclarationMatchingSpecifier(sourceFile: SourceFile, moduleSpecifier: Expression) { + const moduleSpecifierText = isStringLiteral(moduleSpecifier) && moduleSpecifier.text; + return isString(moduleSpecifierText) && some(sourceFile.moduleAugmentations, moduleName => + isStringLiteral(moduleName) + && moduleName.text === moduleSpecifierText); } -function detectSortingWorker(importGroups: ImportDeclaration[][], preferences: UserPreferences): SortKind { - const collateCaseSensitive = getOrganizeImportsComparer(preferences, /*ignoreCase*/ false); - const collateCaseInsensitive = getOrganizeImportsComparer(preferences, /*ignoreCase*/ true); - let sortState = SortKind.Both; - let seenUnsortedGroup = false; - for (const importGroup of importGroups) { - // Check module specifiers - if (importGroup.length > 1) { - const moduleSpecifierSort = detectSortCaseSensitivity( - importGroup, - i => tryCast(i.moduleSpecifier, isStringLiteral)?.text ?? "", - collateCaseSensitive, - collateCaseInsensitive, - ); - if (moduleSpecifierSort) { - // Don't let a single unsorted group of module specifiers make the whole algorithm detect unsorted. - // If other things are sorted consistently, that's a stronger indicator than unsorted module specifiers. - sortState &= moduleSpecifierSort; - seenUnsortedGroup = true; - } - if (!sortState) { - return sortState; - } - } - - // Check import specifiers - const declarationWithNamedImports = find( - importGroup, - i => tryCast(i.importClause?.namedBindings, isNamedImports)?.elements.length! > 1, - ); - if (declarationWithNamedImports) { - const namedImportSort = detectImportSpecifierSorting((declarationWithNamedImports.importClause!.namedBindings as NamedImports).elements, preferences); - if (namedImportSort) { - // Don't let a single unsorted group of named imports make the whole algorithm detect unsorted. - // If other things are sorted consistently, that's a stronger indicator than unsorted named imports. - sortState &= namedImportSort; - seenUnsortedGroup = true; - } - if (!sortState) { - return sortState; - } - } +function getNewImportSpecifiers(namedImports: ImportDeclaration[]) { + return flatMap(namedImports, namedImport => + map(tryGetNamedBindingElements(namedImport), importSpecifier => + importSpecifier.name && importSpecifier.propertyName && importSpecifier.name.escapedText === importSpecifier.propertyName.escapedText + ? factory.updateImportSpecifier(importSpecifier, importSpecifier.isTypeOnly, /*propertyName*/ undefined, importSpecifier.name) + : importSpecifier)); +} - // Quit as soon as we've disambiguated. There's a chance that something later will disagree with what we've - // found so far, but this function is only intended to infer a preference, not validate the whole file for - // consistent and correct sorting. - if (sortState !== SortKind.Both) { - return sortState; - } - } - return seenUnsortedGroup ? SortKind.None : sortState; +function tryGetNamedBindingElements(namedImport: ImportDeclaration) { + return namedImport.importClause?.namedBindings && isNamedImports(namedImport.importClause.namedBindings) + ? namedImport.importClause.namedBindings.elements + : undefined; } -/** @internal */ -export function detectImportDeclarationSorting(imports: readonly AnyImportOrRequireStatement[], preferences: UserPreferences): SortKind { - const collateCaseSensitive = getOrganizeImportsComparer(preferences, /*ignoreCase*/ false); - const collateCaseInsensitive = getOrganizeImportsComparer(preferences, /*ignoreCase*/ true); - return detectSortCaseSensitivity( - imports, - s => getExternalModuleName(getModuleSpecifierExpression(s)) || "", - collateCaseSensitive, - collateCaseInsensitive, - ); +function detectModuleSpecifierCaseBySort(importDeclsByGroup: (readonly AnyImportOrRequireStatement[])[], comparersToTest: Comparer[]) { + const moduleSpecifiersByGroup: string[][] = []; + importDeclsByGroup.forEach(importGroup => { + // Turns importDeclsByGroup into string[][] of module specifiers grouped by declGroup + moduleSpecifiersByGroup.push(getModuleNamesFromDecls(importGroup)); + }); + return detectCaseSensitivityBySort(moduleSpecifiersByGroup, comparersToTest); } -class ImportSpecifierSortingCache implements MemoizeCache<[readonly ImportSpecifier[], UserPreferences], SortKind> { - private _lastPreferences: UserPreferences | undefined; - private _cache: WeakMap | undefined; +function detectNamedImportOrganizationBySort(originalGroups: readonly ImportDeclaration[], comparersToTest: Comparer[], typesToTest: OrganizeImportsTypeOrder[]): { namedImportComparer: Comparer; typeOrder: OrganizeImportsTypeOrder | undefined; isSorted: boolean; } | undefined { + // Filter for import declarations with named imports. Will be a flat array of import declarations without separations by group + let bothNamedImports = false; + const importDeclsWithNamed = originalGroups.filter(i => { + const namedImports = tryCast(i.importClause?.namedBindings, isNamedImports)?.elements; + if (!namedImports?.length) return false; + if (!bothNamedImports && namedImports.some(n => n.isTypeOnly) && namedImports.some(n => !n.isTypeOnly)) { + bothNamedImports = true; + } + return true; + }); + + // No need for more detection, if no named imports + if (importDeclsWithNamed.length === 0) return; - has([specifiers, preferences]: [readonly ImportSpecifier[], UserPreferences]) { - if (this._lastPreferences !== preferences || !this._cache) return false; - return this._cache.has(specifiers); - } + // Formats into lists of named imports, grouped by declaration + const namedImportsByDecl = importDeclsWithNamed.map(importDecl => { + return tryCast(importDecl.importClause?.namedBindings, isNamedImports)?.elements; + }).filter(elements => elements !== undefined) as any as ImportSpecifier[][]; - get([specifiers, preferences]: [readonly ImportSpecifier[], UserPreferences]) { - if (this._lastPreferences !== preferences || !this._cache) return undefined; - return this._cache.get(specifiers); + // If we don't have any import statements with both named regular and type imports, we do not need to detect a type ordering + if (!bothNamedImports || typesToTest.length === 0) { + const sortState = detectCaseSensitivityBySort(namedImportsByDecl.map(i => i.map(n => n.name.text)), comparersToTest); + return { + namedImportComparer: sortState.comparer, + typeOrder: typesToTest.length === 1 ? typesToTest[0] : undefined, + isSorted: sortState.isSorted, + }; } - set([specifiers, preferences]: [readonly ImportSpecifier[], UserPreferences], value: SortKind) { - if (this._lastPreferences !== preferences) { - this._lastPreferences = preferences; - this._cache = undefined; + const bestDiff = { first: Infinity, last: Infinity, inline: Infinity }; + const bestComparer = { first: comparersToTest[0], last: comparersToTest[0], inline: comparersToTest[0] }; + + for (const curComparer of comparersToTest) { + const currDiff = { first: 0, last: 0, inline: 0 }; + for (const importDecl of namedImportsByDecl) { + for (const typeOrder of typesToTest) { + currDiff[typeOrder] = (currDiff[typeOrder] ?? 0) + measureSortedness(importDecl, (n1, n2) => compareImportOrExportSpecifiers(n1, n2, curComparer, { organizeImportsTypeOrder: typeOrder })); + } + } + for (const key of typesToTest) { + const typeOrder = key as OrganizeImportsTypeOrder; + if (currDiff[typeOrder] < bestDiff[typeOrder]) { + bestDiff[typeOrder] = currDiff[typeOrder]; + bestComparer[typeOrder] = curComparer; + } } - this._cache ??= new WeakMap(); - this._cache.set(specifiers, value); } -} -/** @internal */ -export const detectImportSpecifierSorting = memoizeCached((specifiers: readonly ImportSpecifier[], preferences: UserPreferences): SortKind => { - // If types are not sorted as specified, then imports are assumed to be unsorted. - // If there is no type sorting specification, we default to "last" and move on to case sensitivity detection. - switch (preferences.organizeImportsTypeOrder) { - case "first": - if (!arrayIsSorted(specifiers, (s1, s2) => compareBooleans(s2.isTypeOnly, s1.isTypeOnly))) return SortKind.None; - break; - case "inline": - if ( - !arrayIsSorted(specifiers, (s1, s2) => { - const comparer = getStringComparer(/*ignoreCase*/ true); - return comparer(s1.name.text, s2.name.text); - }) - ) { - return SortKind.None; - } - break; - default: - if (!arrayIsSorted(specifiers, (s1, s2) => compareBooleans(s1.isTypeOnly, s2.isTypeOnly))) return SortKind.None; - break; + outer: for (const bestKey of typesToTest) { + const bestTypeOrder = bestKey as OrganizeImportsTypeOrder; + for (const testKey of typesToTest) { + const testTypeOrder = testKey as OrganizeImportsTypeOrder; + if (bestDiff[testTypeOrder] < bestDiff[bestTypeOrder]) continue outer; + } + return { namedImportComparer: bestComparer[bestTypeOrder], typeOrder: bestTypeOrder, isSorted: bestDiff[bestTypeOrder] === 0 }; } - const collateCaseSensitive = getOrganizeImportsComparer(preferences, /*ignoreCase*/ false); - const collateCaseInsensitive = getOrganizeImportsComparer(preferences, /*ignoreCase*/ true); + // Default behavior. It shouldn't be hit if typesToTest.length > 0 + return { namedImportComparer: bestComparer.last, typeOrder: "last", isSorted: bestDiff.last === 0 }; +} - if (preferences.organizeImportsTypeOrder !== "inline") { - const { type: regularImports, regular: typeImports } = groupBy(specifiers, s => s.isTypeOnly ? "type" : "regular"); - const regularCaseSensitivity = regularImports?.length - ? detectSortCaseSensitivity(regularImports, specifier => specifier.name.text, collateCaseSensitive, collateCaseInsensitive) - : undefined; - const typeCaseSensitivity = typeImports?.length - ? detectSortCaseSensitivity(typeImports, specifier => specifier.name.text ?? "", collateCaseSensitive, collateCaseInsensitive) - : undefined; - if (regularCaseSensitivity === undefined) { - return typeCaseSensitivity ?? SortKind.None; - } - if (typeCaseSensitivity === undefined) { - return regularCaseSensitivity; - } - if (regularCaseSensitivity === SortKind.None || typeCaseSensitivity === SortKind.None) { - return SortKind.None; +function measureSortedness(arr: readonly T[], comparer: Comparer) { + let i = 0; + for (let j = 0; j < arr.length - 1; j++) { + if (comparer(arr[j], arr[j + 1]) > 0) { + i++; } - return typeCaseSensitivity & regularCaseSensitivity; } + return i; +} - // else inline - return detectSortCaseSensitivity(specifiers, specifier => specifier.name.text, collateCaseSensitive, collateCaseInsensitive); -}, new ImportSpecifierSortingCache()); +function detectCaseSensitivityBySort(originalGroups: string[][], comparersToTest: Comparer[]): { comparer: Comparer; isSorted: boolean; } { + // Each entry in originalGroups will be sorted and compared against the original entry. + // The total diff of each comparison is the sum of the diffs over all groups + let bestComparer; + let bestDiff = Infinity; -/** @internal */ -export function getImportDeclarationInsertionIndex(sortedImports: readonly AnyImportOrRequireStatement[], newImport: AnyImportOrRequireStatement, comparer: Comparer) { - const index = binarySearch(sortedImports, newImport, identity, (a, b) => compareImportsOrRequireStatements(a, b, comparer)); - return index < 0 ? ~index : index; -} + for (const curComparer of comparersToTest) { + let diffOfCurrentComparer = 0; -/** @internal */ -export function getImportSpecifierInsertionIndex(sortedImports: readonly ImportSpecifier[], newImport: ImportSpecifier, comparer: Comparer, preferences: UserPreferences) { - const index = binarySearch(sortedImports, newImport, identity, (s1, s2) => compareImportOrExportSpecifiers(s1, s2, comparer, preferences)); - return index < 0 ? ~index : index; -} + for (const listToSort of originalGroups) { + if (listToSort.length <= 1) continue; + const diff = measureSortedness(listToSort, curComparer); + diffOfCurrentComparer += diff; + } -/** @internal */ -export function compareImportsOrRequireStatements(s1: AnyImportOrRequireStatement, s2: AnyImportOrRequireStatement, comparer: Comparer) { - return compareModuleSpecifiersWorker(getModuleSpecifierExpression(s1), getModuleSpecifierExpression(s2), comparer) || compareImportKind(s1, s2); + if (diffOfCurrentComparer < bestDiff) { + bestDiff = diffOfCurrentComparer; + bestComparer = curComparer; + } + } + return { + comparer: bestComparer ?? comparersToTest[0], + isSorted: bestDiff === 0, + }; } function compareImportKind(s1: AnyImportOrRequireStatement, s2: AnyImportOrRequireStatement) { @@ -836,20 +833,6 @@ function getImportKindOrder(s1: AnyImportOrRequireStatement) { } } -function getNewImportSpecifiers(namedImports: ImportDeclaration[]) { - return flatMap(namedImports, namedImport => - map(tryGetNamedBindingElements(namedImport), importSpecifier => - importSpecifier.name && importSpecifier.propertyName && importSpecifier.name.escapedText === importSpecifier.propertyName.escapedText - ? factory.updateImportSpecifier(importSpecifier, importSpecifier.isTypeOnly, /*propertyName*/ undefined, importSpecifier.name) - : importSpecifier)); -} - -function tryGetNamedBindingElements(namedImport: ImportDeclaration) { - return namedImport.importClause?.namedBindings && isNamedImports(namedImport.importClause.namedBindings) - ? namedImport.importClause.namedBindings.elements - : undefined; -} - function getOrganizeImportsOrdinalStringComparer(ignoreCase: boolean) { return ignoreCase ? compareStringsCaseInsensitiveEslintCompatible : compareStringsCaseSensitive; } @@ -884,46 +867,99 @@ function getOrganizeImportsLocale(preferences: UserPreferences): string { return resolvedLocale; } -/** @internal */ -export function getOrganizeImportsComparer(preferences: UserPreferences, ignoreCase: boolean): Comparer { +function getOrganizeImportsStringComparer(preferences: UserPreferences, ignoreCase: boolean): Comparer { const collation = preferences.organizeImportsCollation ?? "ordinal"; return collation === "unicode" ? getOrganizeImportsUnicodeStringComparer(ignoreCase, preferences) : getOrganizeImportsOrdinalStringComparer(ignoreCase); } -function getOrganizeImportsComparerWithDetection(preferences: UserPreferences, detectIgnoreCase?: () => boolean): Comparer { - const ignoreCase = typeof preferences.organizeImportsIgnoreCase === "boolean" ? preferences.organizeImportsIgnoreCase : detectIgnoreCase?.() ?? false; - return getOrganizeImportsComparer(preferences, ignoreCase); +/** @internal */ +export function getOrganizeImportsStringComparerWithDetection(originalImportDecls: readonly AnyImportOrRequireStatement[], preferences: UserPreferences): { comparer: Comparer; isSorted: boolean; } { + return detectModuleSpecifierCaseBySort([originalImportDecls], getDetectionLists(preferences).comparersToTest); +} +function getNamedImportSpecifierComparer(preferences: UserPreferences, comparer?: Comparer): Comparer { + const stringComparer = comparer ?? getOrganizeImportsOrdinalStringComparer(!!preferences.organizeImportsIgnoreCase); + return (s1, s2) => compareImportOrExportSpecifiers(s1, s2, stringComparer, preferences); } -function getTopLevelExportGroups(sourceFile: SourceFile) { - const topLevelExportGroups: ExportDeclaration[][] = []; - const statements = sourceFile.statements; - const len = length(statements); - - let i = 0; - let groupIndex = 0; - while (i < len) { - if (isExportDeclaration(statements[i])) { - if (topLevelExportGroups[groupIndex] === undefined) { - topLevelExportGroups[groupIndex] = []; - } - const exportDecl = statements[i] as ExportDeclaration; - if (exportDecl.moduleSpecifier) { - topLevelExportGroups[groupIndex].push(exportDecl); - i++; - } - else { - while (i < len && isExportDeclaration(statements[i])) { - topLevelExportGroups[groupIndex].push(statements[i++] as ExportDeclaration); - } - groupIndex++; - } +/** @internal */ +export function getNamedImportSpecifierComparerWithDetection(importDecl: ImportDeclaration, preferences: UserPreferences, sourceFile?: SourceFile): { specifierComparer: Comparer; isSorted: boolean | undefined; } { + // sort case sensitivity: + // - if the user preference is explicit, use that + // - otherwise, if there are enough existing import specifiers in this import to detect unambiguously, use that + // - otherwise, detect from other imports in the file + const { comparersToTest, typeOrdersToTest } = getDetectionLists(preferences); + const detectFromDecl = detectNamedImportOrganizationBySort([importDecl], comparersToTest, typeOrdersToTest); + let specifierComparer = getNamedImportSpecifierComparer(preferences, comparersToTest[0]); + let isSorted; + if (typeof preferences.organizeImportsIgnoreCase !== "boolean" || !preferences.organizeImportsTypeOrder) { + if (detectFromDecl) { + const { namedImportComparer, typeOrder, isSorted: isDetectedSorted } = detectFromDecl; + isSorted = isDetectedSorted; + specifierComparer = getNamedImportSpecifierComparer({ organizeImportsTypeOrder: typeOrder }, namedImportComparer); } - else { - i++; + else if (sourceFile) { + // If a sourceFile is specified, we can also try detecting using the other import statements + const detectFromFile = detectNamedImportOrganizationBySort(sourceFile.statements.filter(isImportDeclaration), comparersToTest, typeOrdersToTest); + if (detectFromFile) { + const { namedImportComparer, typeOrder, isSorted: isDetectedSorted } = detectFromFile; + isSorted = isDetectedSorted; + specifierComparer = getNamedImportSpecifierComparer({ organizeImportsTypeOrder: typeOrder }, namedImportComparer); + } } } - return flatMap(topLevelExportGroups, exportGroupDecls => groupByNewlineContiguous(sourceFile, exportGroupDecls)); + + return { specifierComparer, isSorted }; +} + +/** @internal */ +export function getImportDeclarationInsertionIndex(sortedImports: readonly AnyImportOrRequireStatement[], newImport: AnyImportOrRequireStatement, comparer: Comparer) { + const index = binarySearch(sortedImports, newImport, identity, (a, b) => compareImportsOrRequireStatements(a, b, comparer)); + return index < 0 ? ~index : index; +} + +/** @internal */ +export function getImportSpecifierInsertionIndex(sortedImports: readonly ImportSpecifier[], newImport: ImportSpecifier, comparer: Comparer) { + const index = binarySearch(sortedImports, newImport, identity, comparer); + return index < 0 ? ~index : index; +} + +/** @internal */ +export function compareImportsOrRequireStatements(s1: AnyImportOrRequireStatement, s2: AnyImportOrRequireStatement, comparer: Comparer) { + return compareModuleSpecifiersWorker(getModuleSpecifierExpression(s1), getModuleSpecifierExpression(s2), comparer) || compareImportKind(s1, s2); +} + +/* ======== Functions that are internal for testing ======== */ + +/** + * @param importGroup a list of ImportDeclarations, all with the same module name. + * + * @deprecated Only used for testing + * @internal + */ +export function testCoalesceImports(importGroup: readonly ImportDeclaration[], ignoreCase: boolean, sourceFile?: SourceFile, preferences?: UserPreferences): readonly ImportDeclaration[] { + const comparer = getOrganizeImportsOrdinalStringComparer(ignoreCase); + const specifierComparer = getNamedImportSpecifierComparer({ organizeImportsTypeOrder: preferences?.organizeImportsTypeOrder }, comparer); + return coalesceImportsWorker(importGroup, comparer, specifierComparer, sourceFile); +} + +/** + * @param exportGroup a list of ExportDeclarations, all with the same module name. + * + * @deprecated Only used for testing + * @internal + */ +export function testCoalesceExports(exportGroup: readonly ExportDeclaration[], ignoreCase: boolean, preferences?: UserPreferences) { + const comparer = (s1: ExportSpecifier, s2: ExportSpecifier) => compareImportOrExportSpecifiers(s1, s2, getOrganizeImportsOrdinalStringComparer(ignoreCase), { organizeImportsTypeOrder: preferences?.organizeImportsTypeOrder ?? "last" }); + return coalesceExportsWorker(exportGroup, comparer); +} + +/** + * @deprecated Only used for testing + * @internal + */ +export function compareModuleSpecifiers(m1: Expression | undefined, m2: Expression | undefined, ignoreCase?: boolean) { + const comparer = getOrganizeImportsOrdinalStringComparer(!!ignoreCase); + return compareModuleSpecifiersWorker(m1, m2, comparer); } diff --git a/src/services/utilities.ts b/src/services/utilities.ts index dc5c6c777528e..62443ac2049b6 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -327,7 +327,6 @@ import { skipOuterExpressions, skipParentheses, some, - SortKind, SourceFile, SourceFileLike, SourceMapper, @@ -2624,14 +2623,12 @@ export function insertImports(changes: textChanges.ChangeTracker, sourceFile: So const decl = isArray(imports) ? imports[0] : imports; const importKindPredicate: (node: Node) => node is AnyImportOrRequireStatement = decl.kind === SyntaxKind.VariableStatement ? isRequireVariableStatement : isAnyImportSyntax; const existingImportStatements = filter(sourceFile.statements, importKindPredicate); - let sortKind = isArray(imports) ? OrganizeImports.detectImportDeclarationSorting(imports, preferences) : SortKind.Both; - const comparer = OrganizeImports.getOrganizeImportsComparer(preferences, sortKind === SortKind.CaseInsensitive); + const { comparer, isSorted } = OrganizeImports.getOrganizeImportsStringComparerWithDetection(existingImportStatements, preferences); const sortedNewImports = isArray(imports) ? stableSort(imports, (a, b) => OrganizeImports.compareImportsOrRequireStatements(a, b, comparer)) : [imports]; if (!existingImportStatements.length) { changes.insertNodesAtTopOfFile(sourceFile, sortedNewImports, blankLineBetween); } - else if (existingImportStatements && (sortKind = OrganizeImports.detectImportDeclarationSorting(existingImportStatements, preferences))) { - const comparer = OrganizeImports.getOrganizeImportsComparer(preferences, sortKind === SortKind.CaseInsensitive); + else if (existingImportStatements && isSorted) { for (const newImport of sortedNewImports) { const insertionIndex = OrganizeImports.getImportDeclarationInsertionIndex(existingImportStatements, newImport, comparer); if (insertionIndex === 0) { diff --git a/src/testRunner/unittests/services/organizeImports.ts b/src/testRunner/unittests/services/organizeImports.ts index e2970ef0d84a1..30c5abcfc0f58 100644 --- a/src/testRunner/unittests/services/organizeImports.ts +++ b/src/testRunner/unittests/services/organizeImports.ts @@ -52,12 +52,12 @@ describe("unittests:: services:: organizeImports", () => { describe("Coalesce imports", () => { it("No imports", () => { - assert.isEmpty(ts.OrganizeImports.coalesceImports([], /*ignoreCase*/ true)); + assert.isEmpty(ts.OrganizeImports.testCoalesceImports([], /*ignoreCase*/ true)); }); it("Sort specifiers - case-insensitive", () => { const sortedImports = parseImports(`import { default as M, a as n, B, y, Z as O } from "lib";`); - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true); + const actualCoalescedImports = ts.OrganizeImports.testCoalesceImports(sortedImports, /*ignoreCase*/ true); const expectedCoalescedImports = parseImports(`import { B, default as M, a as n, Z as O, y } from "lib";`); assertListEqual(actualCoalescedImports, expectedCoalescedImports); }); @@ -67,7 +67,7 @@ describe("unittests:: services:: organizeImports", () => { `import "lib";`, `import "lib";`, ); - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true); + const actualCoalescedImports = ts.OrganizeImports.testCoalesceImports(sortedImports, /*ignoreCase*/ true); const expectedCoalescedImports = parseImports(`import "lib";`); assertListEqual(actualCoalescedImports, expectedCoalescedImports); }); @@ -77,7 +77,7 @@ describe("unittests:: services:: organizeImports", () => { `import * as x from "lib";`, `import * as y from "lib";`, ); - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true); + const actualCoalescedImports = ts.OrganizeImports.testCoalesceImports(sortedImports, /*ignoreCase*/ true); const expectedCoalescedImports = sortedImports; assertListEqual(actualCoalescedImports, expectedCoalescedImports); }); @@ -87,7 +87,7 @@ describe("unittests:: services:: organizeImports", () => { `import x from "lib";`, `import y from "lib";`, ); - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true); + const actualCoalescedImports = ts.OrganizeImports.testCoalesceImports(sortedImports, /*ignoreCase*/ true); const expectedCoalescedImports = parseImports(`import { default as x, default as y } from "lib";`); assertListEqual(actualCoalescedImports, expectedCoalescedImports); }); @@ -97,7 +97,7 @@ describe("unittests:: services:: organizeImports", () => { `import { x } from "lib";`, `import { y as z } from "lib";`, ); - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true); + const actualCoalescedImports = ts.OrganizeImports.testCoalesceImports(sortedImports, /*ignoreCase*/ true); const expectedCoalescedImports = parseImports(`import { x, y as z } from "lib";`); assertListEqual(actualCoalescedImports, expectedCoalescedImports); }); @@ -107,7 +107,7 @@ describe("unittests:: services:: organizeImports", () => { `import "lib";`, `import * as x from "lib";`, ); - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true); + const actualCoalescedImports = ts.OrganizeImports.testCoalesceImports(sortedImports, /*ignoreCase*/ true); const expectedCoalescedImports = sortedImports; assertListEqual(actualCoalescedImports, expectedCoalescedImports); }); @@ -117,7 +117,7 @@ describe("unittests:: services:: organizeImports", () => { `import "lib";`, `import x from "lib";`, ); - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true); + const actualCoalescedImports = ts.OrganizeImports.testCoalesceImports(sortedImports, /*ignoreCase*/ true); const expectedCoalescedImports = sortedImports; assertListEqual(actualCoalescedImports, expectedCoalescedImports); }); @@ -127,7 +127,7 @@ describe("unittests:: services:: organizeImports", () => { `import "lib";`, `import { x } from "lib";`, ); - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true); + const actualCoalescedImports = ts.OrganizeImports.testCoalesceImports(sortedImports, /*ignoreCase*/ true); const expectedCoalescedImports = sortedImports; assertListEqual(actualCoalescedImports, expectedCoalescedImports); }); @@ -137,7 +137,7 @@ describe("unittests:: services:: organizeImports", () => { `import * as x from "lib";`, `import y from "lib";`, ); - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true); + const actualCoalescedImports = ts.OrganizeImports.testCoalesceImports(sortedImports, /*ignoreCase*/ true); const expectedCoalescedImports = parseImports( `import y, * as x from "lib";`, ); @@ -149,7 +149,7 @@ describe("unittests:: services:: organizeImports", () => { `import * as x from "lib";`, `import { y } from "lib";`, ); - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true); + const actualCoalescedImports = ts.OrganizeImports.testCoalesceImports(sortedImports, /*ignoreCase*/ true); const expectedCoalescedImports = sortedImports; assertListEqual(actualCoalescedImports, expectedCoalescedImports); }); @@ -159,7 +159,7 @@ describe("unittests:: services:: organizeImports", () => { `import x from "lib";`, `import { y } from "lib";`, ); - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true); + const actualCoalescedImports = ts.OrganizeImports.testCoalesceImports(sortedImports, /*ignoreCase*/ true); const expectedCoalescedImports = parseImports( `import x, { y } from "lib";`, ); @@ -177,7 +177,7 @@ describe("unittests:: services:: organizeImports", () => { `import z from "lib";`, `import { a } from "lib";`, ); - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true); + const actualCoalescedImports = ts.OrganizeImports.testCoalesceImports(sortedImports, /*ignoreCase*/ true); const expectedCoalescedImports = parseImports( `import "lib";`, `import * as x from "lib";`, @@ -194,7 +194,7 @@ describe("unittests:: services:: organizeImports", () => { `import * as y from "lib";`, `import z from "lib";`, ); - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true); + const actualCoalescedImports = ts.OrganizeImports.testCoalesceImports(sortedImports, /*ignoreCase*/ true); const expectedCoalescedImports = sortedImports; assertListEqual(actualCoalescedImports, expectedCoalescedImports); }); @@ -205,7 +205,7 @@ describe("unittests:: services:: organizeImports", () => { `import type { y } from "lib";`, `import { z } from "lib";`, ); - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true); + const actualCoalescedImports = ts.OrganizeImports.testCoalesceImports(sortedImports, /*ignoreCase*/ true); const expectedCoalescedImports = parseImports( `import { z } from "lib";`, `import type { x, y } from "lib";`, @@ -221,7 +221,7 @@ describe("unittests:: services:: organizeImports", () => { ); // Default import could be rewritten as a named import to combine with `x`, // but seems of debatable merit. - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true); + const actualCoalescedImports = ts.OrganizeImports.testCoalesceImports(sortedImports, /*ignoreCase*/ true); const expectedCoalescedImports = actualCoalescedImports; assertListEqual(actualCoalescedImports, expectedCoalescedImports); }); @@ -229,19 +229,19 @@ describe("unittests:: services:: organizeImports", () => { describe("Coalesce exports", () => { it("No exports", () => { - assert.isEmpty(ts.OrganizeImports.coalesceExports([], /*ignoreCase*/ true)); + assert.isEmpty(ts.OrganizeImports.testCoalesceExports([], /*ignoreCase*/ true)); }); it("Sort specifiers - case-insensitive", () => { const sortedExports = parseExports(`export { default as M, a as n, B, y, Z as O } from "lib";`); - const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports, /*ignoreCase*/ true); + const actualCoalescedExports = ts.OrganizeImports.testCoalesceExports(sortedExports, /*ignoreCase*/ true); const expectedCoalescedExports = parseExports(`export { B, default as M, a as n, Z as O, y } from "lib";`); assertListEqual(actualCoalescedExports, expectedCoalescedExports); }); it("Sort specifiers - type-only-inline", () => { const sortedImports = parseImports(`import { type z, y, type x, c, type b, a } from "lib";`); - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true, ts.getSourceFileOfNode(sortedImports[0]), { organizeImportsTypeOrder: "inline" }); + const actualCoalescedImports = ts.OrganizeImports.testCoalesceImports(sortedImports, /*ignoreCase*/ true, ts.getSourceFileOfNode(sortedImports[0]), { organizeImportsTypeOrder: "inline" }); const expectedCoalescedImports = parseImports(`import { a, type b, c, type x, y, type z } from "lib";`); assertListEqual(actualCoalescedImports, expectedCoalescedImports); }); @@ -251,7 +251,7 @@ describe("unittests:: services:: organizeImports", () => { `export * from "lib";`, `export * from "lib";`, ); - const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports, /*ignoreCase*/ true); + const actualCoalescedExports = ts.OrganizeImports.testCoalesceExports(sortedExports, /*ignoreCase*/ true); const expectedCoalescedExports = parseExports(`export * from "lib";`); assertListEqual(actualCoalescedExports, expectedCoalescedExports); }); @@ -261,7 +261,7 @@ describe("unittests:: services:: organizeImports", () => { `export { x };`, `export { y as z };`, ); - const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports, /*ignoreCase*/ true); + const actualCoalescedExports = ts.OrganizeImports.testCoalesceExports(sortedExports, /*ignoreCase*/ true); const expectedCoalescedExports = parseExports(`export { x, y as z };`); assertListEqual(actualCoalescedExports, expectedCoalescedExports); }); @@ -271,7 +271,7 @@ describe("unittests:: services:: organizeImports", () => { `export { x } from "lib";`, `export { y as z } from "lib";`, ); - const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports, /*ignoreCase*/ true); + const actualCoalescedExports = ts.OrganizeImports.testCoalesceExports(sortedExports, /*ignoreCase*/ true); const expectedCoalescedExports = parseExports(`export { x, y as z } from "lib";`); assertListEqual(actualCoalescedExports, expectedCoalescedExports); }); @@ -281,7 +281,7 @@ describe("unittests:: services:: organizeImports", () => { `export * from "lib";`, `export { y } from "lib";`, ); - const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports, /*ignoreCase*/ true); + const actualCoalescedExports = ts.OrganizeImports.testCoalesceExports(sortedExports, /*ignoreCase*/ true); const expectedCoalescedExports = sortedExports; assertListEqual(actualCoalescedExports, expectedCoalescedExports); }); @@ -292,7 +292,7 @@ describe("unittests:: services:: organizeImports", () => { `export { y as w, z as default };`, `export { w as q };`, ); - const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports, /*ignoreCase*/ true); + const actualCoalescedExports = ts.OrganizeImports.testCoalesceExports(sortedExports, /*ignoreCase*/ true); const expectedCoalescedExports = parseExports( `export { z as default, w as q, y as w, x };`, ); @@ -305,7 +305,7 @@ describe("unittests:: services:: organizeImports", () => { `export * from "lib";`, `export { z as b } from "lib";`, ); - const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports, /*ignoreCase*/ true); + const actualCoalescedExports = ts.OrganizeImports.testCoalesceExports(sortedExports, /*ignoreCase*/ true); const expectedCoalescedExports = parseExports( `export * from "lib";`, `export { x as a, z as b, y } from "lib";`, @@ -318,7 +318,7 @@ describe("unittests:: services:: organizeImports", () => { `export { x };`, `export type { y };`, ); - const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports, /*ignoreCase*/ true); + const actualCoalescedExports = ts.OrganizeImports.testCoalesceExports(sortedExports, /*ignoreCase*/ true); const expectedCoalescedExports = sortedExports; assertListEqual(actualCoalescedExports, expectedCoalescedExports); }); @@ -328,7 +328,7 @@ describe("unittests:: services:: organizeImports", () => { `export type { x };`, `export type { y };`, ); - const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports, /*ignoreCase*/ true); + const actualCoalescedExports = ts.OrganizeImports.testCoalesceExports(sortedExports, /*ignoreCase*/ true); const expectedCoalescedExports = parseExports( `export type { x, y };`, ); @@ -387,6 +387,89 @@ export const Other = 1; assert.isEmpty(changes); }); + testDetectionBaseline("detection1", /*skipDestructiveCodeActions*/ false, { + path: "/test.ts", + content: `import { abc, Abc } from 'b'; +import { I, M, R } from 'a'; +const x = abc + Abc + I + M + R;`, + }); + + testDetectionBaseline("detection2", /*skipDestructiveCodeActions*/ false, { + path: "/test.ts", + content: `import { abc, Abc } from 'a'; +import { I, M, R } from 'b'; +const x = abc + Abc + I + M + R;`, + }); + + testDetectionBaseline("detection3", /*skipDestructiveCodeActions*/ false, { + path: "/test.ts", + content: `import { I, M, R } from 'a'; +import { Abc, abc } from 'b'; +const x = abc + Abc + I + M + R;`, + }); + + testDetectionBaseline("detection4", /*skipDestructiveCodeActions*/ false, { + path: "/test.ts", + content: `import { I, M, R } from 'a'; +import { abc, Abc } from 'b'; +const x = abc + Abc + I + M + R;`, + }); + + testDetectionBaseline("detection5", /*skipDestructiveCodeActions*/ false, { + path: "/test.ts", + content: `import { + Type9, + Type2, + Type8, + Type7, + Type5, + Type4, + Type3, + Type1, + func9, + Type6, + func5, + func6, + func8, + func4, + func7, + func3, + func2, + func1, +} from "foo"; +console.log(Type1, Type2, Type3, Type4, Type5, Type6, Type7, Type8, Type9, func1, func2, func3, func4, func5, func6, func7, func8, func9);`, + }); + + testDetectionBaseline("detection6", /*skipDestructiveCodeActions*/ false, { + path: "/test.ts", + content: `import { A, B, a, b } from 'foo'; +console.log(A, B, a, b);`, + }); + + testDetectionBaseline("detection7", /*skipDestructiveCodeActions*/ false, { + path: "/test.ts", + content: `import { A, a, B, b } from 'foo'; +console.log(A, B, a, b);`, + }); + + testDetectionBaseline("detection8", /*skipDestructiveCodeActions*/ false, { + path: "/test.ts", + content: `import { A, a, b, B } from 'foo'; +console.log(A, B, a, b);`, + }); + + testDetectionBaseline("detection9", /*skipDestructiveCodeActions*/ false, { + path: "/test.ts", + content: `import { a, b, A, B } from 'foo'; +console.log(A, B, a, b);`, + }); + + testDetectionBaseline("detection10", /*skipDestructiveCodeActions*/ false, { + path: "/test.ts", + content: `import { a, A, b, B } from 'foo'; +console.log(A, B, a, b);`, + }); + testOrganizeImports("Renamed_used", /*skipDestructiveCodeActions*/ false, { path: "/test.ts", content: ` @@ -981,6 +1064,27 @@ export * from "lib"; ); } + function testDetectionBaseline(testName: string, skipDestructiveCodeActions: boolean, testFile: File, ...otherFiles: File[]) { + it(testName, () => { + // this differs from the test above, in that it doesn't assert that there are changes + const baselinePath = `organizeImports/${testName}.ts`; + const { path: testPath, content: testContent } = testFile; + const languageService = makeLanguageService(testFile, ...otherFiles); + const changes = languageService.organizeImports({ skipDestructiveCodeActions, type: "file", fileName: testPath }, ts.testFormatSettings, ts.emptyOptions); + + const newText = changes.length ? ts.textChanges.applyChanges(testContent, changes[0].textChanges) : testContent; + Harness.Baseline.runBaseline( + baselinePath, + [ + "// ==ORIGINAL==", + testContent, + "// ==ORGANIZED==", + newText, + ].join(newLineCharacter), + ); + }); + } + function makeLanguageService(...files: File[]) { const host = createServerHost(files); const projectService = new TestProjectService({ host, useSingleInferredProject: true }); diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 61c467dc32ec9..18507000a0df3 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -8253,7 +8253,7 @@ declare namespace ts { * * Default: `last` */ - readonly organizeImportsTypeOrder?: "first" | "last" | "inline"; + readonly organizeImportsTypeOrder?: OrganizeImportsTypeOrder; /** * Indicates whether to exclude standard library and node_modules file symbols from navTo results. */ @@ -8263,6 +8263,7 @@ declare namespace ts { readonly generateReturnInDocTemplate?: boolean; readonly disableLineTextInReferences?: boolean; } + type OrganizeImportsTypeOrder = "last" | "inline" | "first"; /** Represents a bigint literal value without requiring bigint support */ interface PseudoBigInt { negative: boolean; diff --git a/tests/baselines/reference/organizeImports/detection1.ts b/tests/baselines/reference/organizeImports/detection1.ts new file mode 100644 index 0000000000000..74455d34569fc --- /dev/null +++ b/tests/baselines/reference/organizeImports/detection1.ts @@ -0,0 +1,8 @@ +// ==ORIGINAL== +import { abc, Abc } from 'b'; +import { I, M, R } from 'a'; +const x = abc + Abc + I + M + R; +// ==ORGANIZED== +import { I, M, R } from 'a'; +import { abc, Abc } from 'b'; +const x = abc + Abc + I + M + R; \ No newline at end of file diff --git a/tests/baselines/reference/organizeImports/detection10.ts b/tests/baselines/reference/organizeImports/detection10.ts new file mode 100644 index 0000000000000..2ffeb72c48871 --- /dev/null +++ b/tests/baselines/reference/organizeImports/detection10.ts @@ -0,0 +1,6 @@ +// ==ORIGINAL== +import { a, A, b, B } from 'foo'; +console.log(A, B, a, b); +// ==ORGANIZED== +import { a, A, b, B } from 'foo'; +console.log(A, B, a, b); \ No newline at end of file diff --git a/tests/baselines/reference/organizeImports/detection2.ts b/tests/baselines/reference/organizeImports/detection2.ts new file mode 100644 index 0000000000000..6adec0e28fd7e --- /dev/null +++ b/tests/baselines/reference/organizeImports/detection2.ts @@ -0,0 +1,8 @@ +// ==ORIGINAL== +import { abc, Abc } from 'a'; +import { I, M, R } from 'b'; +const x = abc + Abc + I + M + R; +// ==ORGANIZED== +import { abc, Abc } from 'a'; +import { I, M, R } from 'b'; +const x = abc + Abc + I + M + R; \ No newline at end of file diff --git a/tests/baselines/reference/organizeImports/detection3.ts b/tests/baselines/reference/organizeImports/detection3.ts new file mode 100644 index 0000000000000..34d52aa97a4d6 --- /dev/null +++ b/tests/baselines/reference/organizeImports/detection3.ts @@ -0,0 +1,8 @@ +// ==ORIGINAL== +import { I, M, R } from 'a'; +import { Abc, abc } from 'b'; +const x = abc + Abc + I + M + R; +// ==ORGANIZED== +import { I, M, R } from 'a'; +import { Abc, abc } from 'b'; +const x = abc + Abc + I + M + R; \ No newline at end of file diff --git a/tests/baselines/reference/organizeImports/detection4.ts b/tests/baselines/reference/organizeImports/detection4.ts new file mode 100644 index 0000000000000..08eee79e68c97 --- /dev/null +++ b/tests/baselines/reference/organizeImports/detection4.ts @@ -0,0 +1,8 @@ +// ==ORIGINAL== +import { I, M, R } from 'a'; +import { abc, Abc } from 'b'; +const x = abc + Abc + I + M + R; +// ==ORGANIZED== +import { I, M, R } from 'a'; +import { abc, Abc } from 'b'; +const x = abc + Abc + I + M + R; \ No newline at end of file diff --git a/tests/baselines/reference/organizeImports/detection5.ts b/tests/baselines/reference/organizeImports/detection5.ts new file mode 100644 index 0000000000000..f4169e4f29522 --- /dev/null +++ b/tests/baselines/reference/organizeImports/detection5.ts @@ -0,0 +1,44 @@ +// ==ORIGINAL== +import { + Type9, + Type2, + Type8, + Type7, + Type5, + Type4, + Type3, + Type1, + func9, + Type6, + func5, + func6, + func8, + func4, + func7, + func3, + func2, + func1, +} from "foo"; +console.log(Type1, Type2, Type3, Type4, Type5, Type6, Type7, Type8, Type9, func1, func2, func3, func4, func5, func6, func7, func8, func9); +// ==ORGANIZED== +import { + Type1, + Type2, + Type3, + Type4, + Type5, + Type6, + Type7, + Type8, + Type9, + func1, + func2, + func3, + func4, + func5, + func6, + func7, + func8, + func9, +} from "foo"; +console.log(Type1, Type2, Type3, Type4, Type5, Type6, Type7, Type8, Type9, func1, func2, func3, func4, func5, func6, func7, func8, func9); \ No newline at end of file diff --git a/tests/baselines/reference/organizeImports/detection6.ts b/tests/baselines/reference/organizeImports/detection6.ts new file mode 100644 index 0000000000000..75cab8092d135 --- /dev/null +++ b/tests/baselines/reference/organizeImports/detection6.ts @@ -0,0 +1,6 @@ +// ==ORIGINAL== +import { A, B, a, b } from 'foo'; +console.log(A, B, a, b); +// ==ORGANIZED== +import { A, B, a, b } from 'foo'; +console.log(A, B, a, b); \ No newline at end of file diff --git a/tests/baselines/reference/organizeImports/detection7.ts b/tests/baselines/reference/organizeImports/detection7.ts new file mode 100644 index 0000000000000..183961ef15dd7 --- /dev/null +++ b/tests/baselines/reference/organizeImports/detection7.ts @@ -0,0 +1,6 @@ +// ==ORIGINAL== +import { A, a, B, b } from 'foo'; +console.log(A, B, a, b); +// ==ORGANIZED== +import { A, a, B, b } from 'foo'; +console.log(A, B, a, b); \ No newline at end of file diff --git a/tests/baselines/reference/organizeImports/detection8.ts b/tests/baselines/reference/organizeImports/detection8.ts new file mode 100644 index 0000000000000..791ff2994e2de --- /dev/null +++ b/tests/baselines/reference/organizeImports/detection8.ts @@ -0,0 +1,6 @@ +// ==ORIGINAL== +import { A, a, b, B } from 'foo'; +console.log(A, B, a, b); +// ==ORGANIZED== +import { A, a, b, B } from 'foo'; +console.log(A, B, a, b); \ No newline at end of file diff --git a/tests/baselines/reference/organizeImports/detection9.ts b/tests/baselines/reference/organizeImports/detection9.ts new file mode 100644 index 0000000000000..1079617d5220e --- /dev/null +++ b/tests/baselines/reference/organizeImports/detection9.ts @@ -0,0 +1,6 @@ +// ==ORIGINAL== +import { a, b, A, B } from 'foo'; +console.log(A, B, a, b); +// ==ORGANIZED== +import { a, A, b, B } from 'foo'; +console.log(A, B, a, b); \ No newline at end of file diff --git a/tests/cases/fourslash/autoImportSortCaseSensitivity1.ts b/tests/cases/fourslash/autoImportSortCaseSensitivity1.ts index 993b3ec855972..f4120d5beb02e 100644 --- a/tests/cases/fourslash/autoImportSortCaseSensitivity1.ts +++ b/tests/cases/fourslash/autoImportSortCaseSensitivity1.ts @@ -28,7 +28,7 @@ //// d/*1*/ goTo.marker("0"); -verify.importFixAtPosition([`import { A, B, C, a } from "./exports1";\na`]); +verify.importFixAtPosition([`import { a, A, B, C } from "./exports1";\na`]); verify.importFixAtPosition([`import { a, A, B, C } from "./exports1";\na`], /*errorCode*/ undefined, { organizeImportsIgnoreCase: true }); diff --git a/tests/cases/fourslash/autoImportTypeImport4.ts b/tests/cases/fourslash/autoImportTypeImport4.ts index 82dff821438f7..a8f987b659597 100644 --- a/tests/cases/fourslash/autoImportTypeImport4.ts +++ b/tests/cases/fourslash/autoImportTypeImport4.ts @@ -23,27 +23,28 @@ // @Filename: /index0.ts //// import { A, B, C } from "./exports1"; -//// a/*0*/; +//// a/*0*//*0a*/; //// b; // @Filename: /index1.ts //// import { A, B, C, type Y, type Z } from "./exports1"; -//// a/*1*/; +//// a/*1*//*1a*//*1b*//*1c*/; //// b; // @Filename: /index2.ts //// import { A, a, B, b, type Y, type Z } from "./exports1"; //// import { E } from "./exports2"; -//// d/*2*/ +//// d/*2*//*2a*//*2b*//*2c*/ // addition of correctly sorted type imports should not affect behavior as shown in autoImportSortCaseSensitivity1.ts goTo.marker("0"); verify.importFixAtPosition([ - `import { A, B, C, a } from "./exports1";\na;\nb;`, - `import { A, B, C, b } from "./exports1";\na;\nb;`, + `import { a, A, B, C } from "./exports1";\na;\nb;`, + `import { A, b, B, C } from "./exports1";\na;\nb;`, ], /*errorCode*/ undefined, { organizeImportsTypeOrder : "last" }); +goTo.marker("0a"); verify.importFixAtPosition([ `import { a, A, B, C } from "./exports1";\na;\nb;`, `import { A, b, B, C } from "./exports1";\na;\nb;` @@ -53,11 +54,12 @@ verify.importFixAtPosition([ goTo.marker("1"); verify.importFixAtPosition([ - `import { A, B, C, a, type Y, type Z } from "./exports1";\na;\nb;`, - `import { A, B, C, b, type Y, type Z } from "./exports1";\na;\nb;`, + `import { a, A, B, C, type Y, type Z } from "./exports1";\na;\nb;`, + `import { A, b, B, C, type Y, type Z } from "./exports1";\na;\nb;`, ], /*errorCode*/ undefined, { organizeImportsTypeOrder : "last" }); +goTo.marker("1a"); verify.importFixAtPosition([ `import { a, A, B, C, type Y, type Z } from "./exports1";\na;\nb;`, `import { A, b, B, C, type Y, type Z } from "./exports1";\na;\nb;` @@ -65,13 +67,15 @@ verify.importFixAtPosition([ /*errorCode*/ undefined, { organizeImportsIgnoreCase: true, organizeImportsTypeOrder : "last" }); +goTo.marker("1b"); // if we sort inline and sensitive, then all upper case imports should be sorted before any lower case imports verify.importFixAtPosition([ - `import { A, B, C, type Y, type Z, a } from "./exports1";\na;\nb;`, - `import { A, B, C, type Y, type Z, b } from "./exports1";\na;\nb;`, + `import { a, A, B, C, type Y, type Z } from "./exports1";\na;\nb;`, + `import { A, b, B, C, type Y, type Z } from "./exports1";\na;\nb;`, ], /*errorCode*/ undefined, { organizeImportsTypeOrder : "inline" }); +goTo.marker("1c"); verify.importFixAtPosition([ `import { a, A, B, C, type Y, type Z } from "./exports1";\na;\nb;`, `import { A, b, B, C, type Y, type Z } from "./exports1";\na;\nb;` @@ -87,6 +91,7 @@ d`, ], /*errorCode*/ undefined, { organizeImportsTypeOrder : "last" }); +goTo.marker("2a"); verify.importFixAtPosition([ `import { A, a, B, b, type Y, type Z } from "./exports1"; import { E, d } from "./exports2"; @@ -94,7 +99,7 @@ d` ], /*errorCode*/ undefined, { organizeImportsIgnoreCase: false, organizeImportsTypeOrder : "last" }); - +goTo.marker("2b"); verify.importFixAtPosition([ `import { A, a, B, b, type Y, type Z } from "./exports1"; import { d, E } from "./exports2"; @@ -102,6 +107,7 @@ d`, ], /*errorCode*/ undefined, { organizeImportsTypeOrder : "last" }); +goTo.marker("2c"); verify.importFixAtPosition([ `import { A, a, B, b, type Y, type Z } from "./exports1"; import { E, d } from "./exports2"; diff --git a/tests/cases/fourslash/autoImportTypeImport5.ts b/tests/cases/fourslash/autoImportTypeImport5.ts index a0800d3f236f0..753f2a3a7f8ec 100644 --- a/tests/cases/fourslash/autoImportTypeImport5.ts +++ b/tests/cases/fourslash/autoImportTypeImport5.ts @@ -35,8 +35,8 @@ // addition of correctly sorted regular imports should not affect correctly sorted type imports goTo.marker("0"); verify.importFixAtPosition([ - `import { type X, type Y, type Z, type x } from "./exports1";\nconst foo: x;\nconst bar: y;`, - `import { type X, type Y, type Z, type y } from "./exports1";\nconst foo: x;\nconst bar: y;`, + `import { type x, type X, type Y, type Z } from "./exports1";\nconst foo: x;\nconst bar: y;`, + `import { type X, type y, type Y, type Z } from "./exports1";\nconst foo: x;\nconst bar: y;`, ], /*errorCode*/ undefined, { organizeImportsTypeOrder : "last" }); @@ -47,8 +47,8 @@ verify.importFixAtPosition([ /*errorCode*/ undefined, { organizeImportsIgnoreCase: true, organizeImportsTypeOrder : "last" }); verify.importFixAtPosition([ - `import { type X, type Y, type Z, type x } from "./exports1";\nconst foo: x;\nconst bar: y;`, - `import { type X, type Y, type Z, type y } from "./exports1";\nconst foo: x;\nconst bar: y;`, + `import { type x, type X, type Y, type Z } from "./exports1";\nconst foo: x;\nconst bar: y;`, + `import { type X, type y, type Y, type Z } from "./exports1";\nconst foo: x;\nconst bar: y;`, ], /*errorCode*/ undefined, { organizeImportsTypeOrder : "inline" }); @@ -59,8 +59,8 @@ verify.importFixAtPosition([ /*errorCode*/ undefined, { organizeImportsIgnoreCase: true, organizeImportsTypeOrder : "inline" }); verify.importFixAtPosition([ - `import { type X, type Y, type Z, type x } from "./exports1";\nconst foo: x;\nconst bar: y;`, - `import { type X, type Y, type Z, type y } from "./exports1";\nconst foo: x;\nconst bar: y;`, + `import { type x, type X, type Y, type Z } from "./exports1";\nconst foo: x;\nconst bar: y;`, + `import { type X, type y, type Y, type Z } from "./exports1";\nconst foo: x;\nconst bar: y;`, ], /*errorCode*/ undefined, { organizeImportsTypeOrder : "first" }); @@ -73,8 +73,8 @@ verify.importFixAtPosition([ goTo.marker("1"); verify.importFixAtPosition([ - `import { A, B, type X, type Y, type Z, type x } from "./exports1";\nconst foo: x;\nconst bar: y;`, - `import { A, B, type X, type Y, type Z, type y } from "./exports1";\nconst foo: x;\nconst bar: y;`, + `import { A, B, type x, type X, type Y, type Z } from "./exports1";\nconst foo: x;\nconst bar: y;`, + `import { A, B, type X, type y, type Y, type Z } from "./exports1";\nconst foo: x;\nconst bar: y;`, ], /*errorCode*/ undefined, { organizeImportsTypeOrder : "last" }); diff --git a/tests/cases/fourslash/codeFixInferFromUsageContextualImport1.ts b/tests/cases/fourslash/codeFixInferFromUsageContextualImport1.ts index f41399a478ac2..5e4a4ca3542ff 100644 --- a/tests/cases/fourslash/codeFixInferFromUsageContextualImport1.ts +++ b/tests/cases/fourslash/codeFixInferFromUsageContextualImport1.ts @@ -20,7 +20,7 @@ goTo.file("/b.ts"); verify.codeFix({ description: "Infer parameter types from usage", newFileContent: -`import { User, getEmail } from "./a"; +`import { getEmail, User } from "./a"; export function f(user: User) { getEmail(user); diff --git a/tests/cases/fourslash/importNameCodeFix_all_promoteType.ts b/tests/cases/fourslash/importNameCodeFix_all_promoteType.ts new file mode 100644 index 0000000000000..4a86151ac5c55 --- /dev/null +++ b/tests/cases/fourslash/importNameCodeFix_all_promoteType.ts @@ -0,0 +1,42 @@ +/// + +// @Filename: /a.ts +//// export class A {} +//// export class B {} +//// export class C {} +//// export class D {} +//// export class E {} +//// export class F {} +//// export class G {} + +// @Filename: /b.ts +//// import type { A, C, D, E, G } from './a'; +//// type Z = B | A; +//// new F; + +// @Filename: /c.ts +//// import type { A, C, D, E, G } from './a'; +//// type Z = B | A; +//// type Y = F; + +goTo.file('/b.ts'); +verify.codeFixAll({ + fixId: "fixMissingImport", + fixAllDescription: "Add all missing imports", + newFileContent: +`import { B, F, type A, type C, type D, type E, type G } from './a'; +type Z = B | A; +new F;` +}); + +goTo.file('/c.ts'); +verify.codeFixAll({ + fixId: "fixMissingImport", + fixAllDescription: "Add all missing imports", + newFileContent: +`import type { A, B, C, D, E, F, G } from './a'; +type Z = B | A; +type Y = F;` +}); + + diff --git a/tests/cases/fourslash/importNameCodeFix_importType7.ts b/tests/cases/fourslash/importNameCodeFix_importType7.ts index c74a3c2c22837..b218c6623f5c8 100644 --- a/tests/cases/fourslash/importNameCodeFix_importType7.ts +++ b/tests/cases/fourslash/importNameCodeFix_importType7.ts @@ -16,6 +16,8 @@ //// new SomePig/**/ goTo.marker(""); + +// since we cannot detect a type order from the original file, type order defaults to last verify.importFixAtPosition([ `import { SomePig, diff --git a/tests/cases/fourslash/organizeImports22.ts b/tests/cases/fourslash/organizeImports22.ts new file mode 100644 index 0000000000000..0e3c97d912704 --- /dev/null +++ b/tests/cases/fourslash/organizeImports22.ts @@ -0,0 +1,28 @@ +/// + +//// import {abc, Abc, bc, Bc} from 'b'; +//// import { +//// I, +//// R, +//// M, +//// } from 'a'; +//// console.log(abc, Abc, bc, Bc, I, R, M); + +verify.organizeImports( +`import { + I, + M, + R, +} from 'a'; +import { abc, Abc, bc, Bc } from 'b'; +console.log(abc, Abc, bc, Bc, I, R, M);`); + +// organize already-organized imports to make sure output is stable +verify.organizeImports( +`import { + I, + M, + R, +} from 'a'; +import { abc, Abc, bc, Bc } from 'b'; +console.log(abc, Abc, bc, Bc, I, R, M);`); diff --git a/tests/cases/fourslash/organizeImports23.ts b/tests/cases/fourslash/organizeImports23.ts new file mode 100644 index 0000000000000..e13e332a63fea --- /dev/null +++ b/tests/cases/fourslash/organizeImports23.ts @@ -0,0 +1,31 @@ +/// + +//// import {abc, Abc, type bc, type Bc} from 'b'; +//// import { +//// I, +//// R, +//// M, +//// } from 'a'; +//// type x = bc | Bc; +//// console.log(abc, Abc, I, R, M); + +verify.organizeImports( +`import { + I, + M, + R, +} from 'a'; +import { abc, Abc, type bc, type Bc } from 'b'; +type x = bc | Bc; +console.log(abc, Abc, I, R, M);`); + +// organize already-organized imports to make sure output is stable +verify.organizeImports( +`import { + I, + M, + R, +} from 'a'; +import { abc, Abc, type bc, type Bc } from 'b'; +type x = bc | Bc; +console.log(abc, Abc, I, R, M);`); diff --git a/tests/cases/fourslash/organizeImportsType10.ts b/tests/cases/fourslash/organizeImportsType10.ts new file mode 100644 index 0000000000000..55247daf7ed0f --- /dev/null +++ b/tests/cases/fourslash/organizeImportsType10.ts @@ -0,0 +1,50 @@ +/// + +////import { +//// type Type1, +//// type Type2, +//// func4, +//// type Type3, +//// type Type4, +//// type Type5, +//// type Type7, +//// type Type8, +//// type Type9, +//// func1, +//// func2, +//// type Type6, +//// func3, +//// func5, +//// func6, +//// func7, +//// func8, +//// func9, +////} from "foo"; +////interface Use extends Type1, Type2, Type3, Type4, Type5, Type6, Type7, Type8, Type9 {} +////console.log(func1, func2, func3, func4, func5, func6, func7, func8, func9); + +verify.organizeImports( +`import { + type Type1, + type Type2, + type Type3, + type Type4, + type Type5, + type Type6, + type Type7, + type Type8, + type Type9, + func1, + func2, + func3, + func4, + func5, + func6, + func7, + func8, + func9, +} from "foo"; +interface Use extends Type1, Type2, Type3, Type4, Type5, Type6, Type7, Type8, Type9 {} +console.log(func1, func2, func3, func4, func5, func6, func7, func8, func9);`, +/*mode*/ undefined, +{ organizeImportsIgnoreCase: true }); diff --git a/tests/cases/fourslash/organizeImportsType11.ts b/tests/cases/fourslash/organizeImportsType11.ts new file mode 100644 index 0000000000000..89c07d83eaeea --- /dev/null +++ b/tests/cases/fourslash/organizeImportsType11.ts @@ -0,0 +1,50 @@ +/// + +////import { +//// type Type1, +//// type Type2, +//// func4, +//// type Type3, +//// type Type4, +//// type Type5, +//// type Type7, +//// type Type8, +//// type Type9, +//// func1, +//// func2, +//// type Type6, +//// func3, +//// func5, +//// func6, +//// func7, +//// func8, +//// func9, +////} from "foo"; +////interface Use extends Type1, Type2, Type3, Type4, Type5, Type6, Type7, Type8, Type9 {} +////console.log(func1, func2, func3, func4, func5, func6, func7, func8, func9); + +verify.organizeImports( +`import { + type Type1, + type Type2, + type Type3, + type Type4, + type Type5, + type Type6, + type Type7, + type Type8, + type Type9, + func1, + func2, + func3, + func4, + func5, + func6, + func7, + func8, + func9, +} from "foo"; +interface Use extends Type1, Type2, Type3, Type4, Type5, Type6, Type7, Type8, Type9 {} +console.log(func1, func2, func3, func4, func5, func6, func7, func8, func9);` +); + \ No newline at end of file diff --git a/tests/cases/fourslash/organizeImportsType5.ts b/tests/cases/fourslash/organizeImportsType5.ts index 1f8bb8dd602a7..698577ac2cd4a 100644 --- a/tests/cases/fourslash/organizeImportsType5.ts +++ b/tests/cases/fourslash/organizeImportsType5.ts @@ -12,18 +12,32 @@ //// } from './foo'; //// console.log(A, a, B, b, c, C, d, D); -verify.organizeImports( -`import { +verify.organizeImports(`import { type A, + a, + b, b as B, + type c, c as C, - type d as D, + d, + type d as D +} from './foo'; +console.log(A, a, B, b, c, C, d, D);`, + /*mode*/ undefined, + { organizeImportsIgnoreCase: "auto", organizeImportsTypeOrder: "inline" } +); + +verify.organizeImports(`import { + type A, a, b, + b as B, type c, - d + c as C, + d, + type d as D } from './foo'; console.log(A, a, B, b, c, C, d, D);`, /*mode*/ undefined, { organizeImportsIgnoreCase: "auto", organizeImportsTypeOrder: "inline" } -); +); \ No newline at end of file diff --git a/tests/cases/fourslash/organizeImportsType8.ts b/tests/cases/fourslash/organizeImportsType8.ts index da43f893431df..2c63d54cdb38e 100644 --- a/tests/cases/fourslash/organizeImportsType8.ts +++ b/tests/cases/fourslash/organizeImportsType8.ts @@ -20,17 +20,15 @@ console.log(a, b, A, B);`, edit.replaceLine(0, 'import { type A, type a, b, B } from "foo2";'); verify.organizeImports( -`import { B, b, type A, type a } from "foo2"; +`import { b, B, type A, type a } from "foo2"; console.log(a, b, A, B);`, /*mode*/ undefined, { organizeImportsIgnoreCase: "auto", organizeImportsTypeOrder: "last" } ); -// default behavior is { organizeImportsTypeOrder: "last" } - edit.replaceLine(0, 'import { type A, type a, b, B } from "foo3";'); verify.organizeImports( -`import { B, b, type A, type a } from "foo3"; +`import { type A, type a, b, B } from "foo3"; console.log(a, b, A, B);`, /*mode*/ undefined, { organizeImportsIgnoreCase: "auto" } @@ -38,7 +36,7 @@ console.log(a, b, A, B);`, edit.replaceLine(0, 'import { type A, type a, b, B } from "foo4";'); verify.organizeImports( -`import { b, B, type A, type a } from "foo4"; +`import { type A, type a, b, B } from "foo4"; console.log(a, b, A, B);`, /*mode*/ undefined, { organizeImportsIgnoreCase: true } @@ -46,7 +44,7 @@ console.log(a, b, A, B);`, edit.replaceLine(0, 'import { type A, type a, b, B } from "foo5";'); verify.organizeImports( -`import { B, b, type A, type a } from "foo5"; +`import { type A, B, type a, b } from "foo5"; console.log(a, b, A, B);`, /*mode*/ undefined, { organizeImportsIgnoreCase: false } diff --git a/tests/cases/fourslash/organizeImportsType9.ts b/tests/cases/fourslash/organizeImportsType9.ts index efe6107e020b6..1bbc26d8c31e3 100644 --- a/tests/cases/fourslash/organizeImportsType9.ts +++ b/tests/cases/fourslash/organizeImportsType9.ts @@ -19,7 +19,7 @@ console.log(a, b, A, B);`, edit.replaceLine(0, 'import { type a, type A, b, B } from "foo2";'); verify.organizeImports( -`import { B, b, type A, type a } from "foo2"; +`import { b, B, type a, type A } from "foo2"; console.log(a, b, A, B);`, /*mode*/ undefined, { organizeImportsIgnoreCase: "auto", organizeImportsTypeOrder: "last" } @@ -27,7 +27,7 @@ console.log(a, b, A, B);`, edit.replaceLine(0, 'import { type a, type A, b, B } from "foo3";'); verify.organizeImports( -`import { B, b, type A, type a } from "foo3"; +`import { type a, type A, b, B } from "foo3"; console.log(a, b, A, B);`, /*mode*/ undefined, { organizeImportsIgnoreCase: "auto" } @@ -35,14 +35,14 @@ console.log(a, b, A, B);`, edit.replaceLine(0, 'import { type a, type A, b, B } from "foo4";'); verify.organizeImports( -`import { b, B, type a, type A } from "foo4"; +`import { type a, type A, b, B } from "foo4"; console.log(a, b, A, B);`, /*mode*/ undefined, { organizeImportsIgnoreCase: true }); edit.replaceLine(0, 'import { type a, type A, b, B } from "foo5";'); verify.organizeImports( -`import { B, b, type A, type a } from "foo5"; +`import { type A, B, type a, b } from "foo5"; console.log(a, b, A, B);`, /*mode*/ undefined, { organizeImportsIgnoreCase: false }); \ No newline at end of file