diff --git a/packages/knip/fixtures/fix/index.js b/packages/knip/fixtures/fix/index.js index 60b2c9410..0c522001d 100644 --- a/packages/knip/fixtures/fix/index.js +++ b/packages/knip/fixtures/fix/index.js @@ -14,3 +14,4 @@ USED; identifier; a; NS.One; +NS.Nine; diff --git a/packages/knip/fixtures/fix/reexported.ts b/packages/knip/fixtures/fix/reexported.ts index e751b8b78..270827de7 100644 --- a/packages/knip/fixtures/fix/reexported.ts +++ b/packages/knip/fixtures/fix/reexported.ts @@ -7,4 +7,13 @@ export { Two, Three }; export { Four as Fourth, Five as Fifth }; +type Six = any; +type Seven = unknown; +const Eight = 8; +const Nine = 9; +type Ten = unknown[]; +; + +export { type Seven, Eight, Nine, type Ten }; + export const One = 1; diff --git a/packages/knip/fixtures/fix/reexports.js b/packages/knip/fixtures/fix/reexports.js index 5d55b3076..f974186b8 100644 --- a/packages/knip/fixtures/fix/reexports.js +++ b/packages/knip/fixtures/fix/reexports.js @@ -1,5 +1,5 @@ export { RangeSlider } from './reexported'; export { Rating } from './reexported'; -export { One } from './reexported'; +export { One, Six, Seven, Eight, Nine, Ten } from './reexported'; export { Col, Col as KCol } from './reexported'; export { Row as KRow, Row } from './reexported'; diff --git a/packages/knip/src/IssueFixer.ts b/packages/knip/src/IssueFixer.ts index f38bd8f10..690f13a57 100644 --- a/packages/knip/src/IssueFixer.ts +++ b/packages/knip/src/IssueFixer.ts @@ -1,5 +1,5 @@ import { readFile, rm, writeFile } from 'node:fs/promises'; -import type { Fixes } from './types/exports.js'; +import type { Fix, Fixes } from './types/exports.js'; import type { Issues } from './types/issues.js'; import { cleanExport } from './util/clean-export.js'; import { load, save } from './util/package-json.js'; @@ -20,8 +20,8 @@ export class IssueFixer { isFixUnusedTypes = true; isFixUnusedExports = true; - unusedTypeNodes: Map> = new Map(); - unusedExportNodes: Map> = new Map(); + unusedTypeNodes: Map> = new Map(); + unusedExportNodes: Map> = new Map(); constructor({ isEnabled, cwd, fixTypes = [], isRemoveFiles }: Fixer) { this.isEnabled = isEnabled; @@ -77,16 +77,13 @@ export class IssueFixer { private async removeUnusedExportKeywords(issues: Issues) { const filePaths = new Set([...this.unusedTypeNodes.keys(), ...this.unusedExportNodes.keys()]); for (const filePath of filePaths) { - const exportPositions: Fixes = [ - ...(this.isFixUnusedTypes ? (this.unusedTypeNodes.get(filePath) ?? []) : []), - ...(this.isFixUnusedExports ? (this.unusedExportNodes.get(filePath) ?? []) : []), - ].sort((a, b) => b[0] - a[0]); + const types = (this.isFixUnusedTypes && this.unusedTypeNodes.get(filePath)) || []; + const exports = (this.isFixUnusedExports && this.unusedExportNodes.get(filePath)) || []; + const exportPositions = [...types, ...exports].filter(fix => fix !== undefined).sort((a, b) => b[0] - a[0]); if (exportPositions.length > 0) { const sourceFileText = exportPositions.reduce( - (text, [start, end, isCleanable]) => { - return cleanExport({ text, start, end, isCleanable: Boolean(isCleanable) }); - }, + (text, [start, end, isCleanable]) => cleanExport({ text, start, end, isCleanable: Boolean(isCleanable) }), await readFile(filePath, 'utf-8') ); diff --git a/packages/knip/src/util/clean-export.ts b/packages/knip/src/util/clean-export.ts index 34ab2abf1..81a30eb12 100644 --- a/packages/knip/src/util/clean-export.ts +++ b/packages/knip/src/util/clean-export.ts @@ -30,7 +30,23 @@ export const cleanExport = ({ text, start, end, isCleanable }: FixerOptions) => } if (bracketCloseIndex === -1) { - return beforeStart + (commaIndex === -1 ? afterEnd : afterEnd.substring(commaIndex + 1)); + let x = 0; + let j = beforeStart.length - 1; + while (j >= 0) { + const char = beforeStart[j]; + if (!/\s/.test(char)) { + if (beforeStart.substring(j - 3, j + 1) === 'type') { + x = 5; + } + break; + } + j--; + } + + return ( + beforeStart.substring(0, beforeStart.length - x) + + (commaIndex === -1 ? afterEnd : afterEnd.substring(commaIndex + 1)) + ); } let j = beforeStart.length - 1; @@ -40,25 +56,50 @@ export const cleanExport = ({ text, start, end, isCleanable }: FixerOptions) => bracketOpenIndex = j; break; } - if (!/\s/.test(char)) break; + if (!/\s/.test(char)) { + if (beforeStart.substring(j - 3, j + 1) === 'type') { + j = j - 4; + continue; + } + break; + } j--; } if (bracketCloseIndex !== -1 && bracketOpenIndex !== -1) { const toBracket = beforeStart.substring(0, bracketOpenIndex).trim(); - if (toBracket.endsWith('export')) { + const exportLength = toBracket.endsWith('export') ? 6 : toBracket.endsWith('export type') ? 12 : 0; + if (exportLength) { const fromBracket = afterEnd.substring(bracketCloseIndex + 1).trim(); if (fromBracket.startsWith('from')) { const quoteMatch = afterEnd.match(/['"].*?['"]/); if (quoteMatch?.index) { const fromSpecifierLength = quoteMatch.index + quoteMatch[0].length; - return toBracket.substring(0, toBracket.length - 6) + afterEnd.substring(fromSpecifierLength); + return toBracket.substring(0, toBracket.length - exportLength) + afterEnd.substring(fromSpecifierLength); } } - return toBracket.substring(0, toBracket.length - 6) + afterEnd.substring(bracketCloseIndex + 1); + return toBracket.substring(0, toBracket.length - exportLength) + afterEnd.substring(bracketCloseIndex + 1); } } - return beforeStart + (commaIndex === -1 ? afterEnd : afterEnd.substring(commaIndex + 1)); + { + let x = 0; + let j = beforeStart.length - 1; + while (j >= 0) { + const char = beforeStart[j]; + if (!/\s/.test(char)) { + if (beforeStart.substring(j - 3, j + 1) === 'type') { + x = 5; + } + break; + } + j--; + } + + return ( + beforeStart.substring(0, beforeStart.length - x) + + (commaIndex === -1 ? afterEnd : afterEnd.substring(commaIndex + 1)) + ); + } }; diff --git a/packages/knip/test/fix.test.ts b/packages/knip/test/fix.test.ts index c246b316e..7a40bedc3 100644 --- a/packages/knip/test/fix.test.ts +++ b/packages/knip/test/fix.test.ts @@ -59,7 +59,7 @@ module.exports = { identifier, }; await readContents('reexports.js'), `; ; -export { One } from './reexported'; +export { One, Nine, } from './reexported'; ; ; `, @@ -76,6 +76,15 @@ const Five = 5; ; +type Six = any; +type Seven = unknown; +const Eight = 8; +const Nine = 9; +type Ten = unknown[]; +; + +export { Nine, }; + export const One = 1; `, ], diff --git a/packages/knip/test/util/clean-export.test.ts b/packages/knip/test/util/clean-export.test.ts index 8920f8e4a..58a73d0aa 100644 --- a/packages/knip/test/util/clean-export.test.ts +++ b/packages/knip/test/util/clean-export.test.ts @@ -52,4 +52,29 @@ test('fixer', () => { const text = 'export { AB } from "specifier"'; assert.deepEqual(cleanExport(getOpts(text, 'AB')), ''); } + + { + const text = 'export type { AB }'; + assert.deepEqual(cleanExport(getOpts(text, 'AB')), ''); + } + + { + const text = 'export { type AB }'; + assert.deepEqual(cleanExport(getOpts(text, 'AB')), ''); + } + + { + const text = 'export { type AB, type CD, type EF }'; + assert.deepEqual(cleanExport(getOpts(text, 'CD')), 'export { type AB, type EF }'); + } + + { + const text = 'export { type AB, CD, type EF }'; + assert.deepEqual(cleanExport(getOpts(text, 'AB')), 'export { CD, type EF }'); + } + + { + const text = 'export { AB, CD, type EF }'; + assert.deepEqual(cleanExport(getOpts(text, 'EF')), 'export { AB, CD, }'); + } });