diff --git a/packages/knip/fixtures/fix/default-x.js b/packages/knip/fixtures/fix/default-x.js new file mode 100644 index 000000000..be6fb159d --- /dev/null +++ b/packages/knip/fixtures/fix/default-x.js @@ -0,0 +1,5 @@ +const x = 1; + +export default x; + +export const dx = 1; diff --git a/packages/knip/fixtures/fix/default.js b/packages/knip/fixtures/fix/default.js new file mode 100644 index 000000000..b7588bc32 --- /dev/null +++ b/packages/knip/fixtures/fix/default.js @@ -0,0 +1,2 @@ +export default 1; +export const d = 1; diff --git a/packages/knip/fixtures/fix/index.js b/packages/knip/fixtures/fix/index.js index 0c522001d..d2a9b614e 100644 --- a/packages/knip/fixtures/fix/index.js +++ b/packages/knip/fixtures/fix/index.js @@ -1,3 +1,5 @@ +import { d } from './default'; +import { dx } from './default-x'; import _ from 'lodash'; import { z, f, g, i } from './mod'; import { USED } from './access'; @@ -5,6 +7,8 @@ import { identifier } from './exports'; import { a } from './ignored'; import * as NS from './reexports'; +d; +dx; _; z; f; @@ -14,4 +18,6 @@ USED; identifier; a; NS.One; +NS.Rectangle; NS.Nine; +NS.setter; diff --git a/packages/knip/fixtures/fix/reexported.ts b/packages/knip/fixtures/fix/reexported.ts index 270827de7..95e6fff03 100644 --- a/packages/knip/fixtures/fix/reexported.ts +++ b/packages/knip/fixtures/fix/reexported.ts @@ -7,13 +7,20 @@ export { Two, Three }; export { Four as Fourth, Five as Fifth }; +export { Four as Rectangle, Five as Pentagon }; + type Six = any; type Seven = unknown; const Eight = 8; const Nine = 9; type Ten = unknown[]; -; + +export type { Six }; export { type Seven, Eight, Nine, type Ten }; export const One = 1; + +const fn = () => ({ get: () => 1, set: () => 1 }); + +export const { get: getter, set: setter } = fn(); diff --git a/packages/knip/fixtures/fix/reexports.js b/packages/knip/fixtures/fix/reexports.js index f974186b8..55f16cca0 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, Six, Seven, Eight, Nine, Ten } from './reexported'; +export { One, Rectangle, Six, Seven, Eight, Nine, Ten, getter, setter } 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 690f13a57..1638ab315 100644 --- a/packages/knip/src/IssueFixer.ts +++ b/packages/knip/src/IssueFixer.ts @@ -1,9 +1,9 @@ import { readFile, rm, writeFile } from 'node:fs/promises'; 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'; import { join, relative } from './util/path.js'; +import { removeExport } from './util/remove-export.js'; interface Fixer { isEnabled: boolean; @@ -46,7 +46,7 @@ export class IssueFixer { public async fixIssues(issues: Issues) { await this.removeUnusedFiles(issues); - await this.removeUnusedExportKeywords(issues); + await this.removeUnusedExports(issues); await this.removeUnusedDependencies(issues); } @@ -74,7 +74,7 @@ export class IssueFixer { } } - private async removeUnusedExportKeywords(issues: Issues) { + private async removeUnusedExports(issues: Issues) { const filePaths = new Set([...this.unusedTypeNodes.keys(), ...this.unusedExportNodes.keys()]); for (const filePath of filePaths) { const types = (this.isFixUnusedTypes && this.unusedTypeNodes.get(filePath)) || []; @@ -83,7 +83,7 @@ export class IssueFixer { if (exportPositions.length > 0) { const sourceFileText = exportPositions.reduce( - (text, [start, end, isCleanable]) => cleanExport({ text, start, end, isCleanable: Boolean(isCleanable) }), + (text, [start, end, flags]) => removeExport({ text, start, end, flags }), await readFile(filePath, 'utf-8') ); diff --git a/packages/knip/src/constants.ts b/packages/knip/src/constants.ts index fe2376d81..cc5d0228f 100644 --- a/packages/knip/src/constants.ts +++ b/packages/knip/src/constants.ts @@ -202,3 +202,9 @@ export const ISSUE_TYPE_TITLE: Record = { classMembers: 'Unused exported class members', duplicates: 'Duplicate exports', }; + +export const FIX_FLAGS = { + NONE: 0, + OBJECT_BINDING: 1 << 0, // remove next comma + EMPTY_DECLARATION: 1 << 1, // remove declaration if empty +} as const; diff --git a/packages/knip/src/types/exports.ts b/packages/knip/src/types/exports.ts index 53f1e5545..1f112f315 100644 --- a/packages/knip/src/types/exports.ts +++ b/packages/knip/src/types/exports.ts @@ -3,7 +3,7 @@ import type { SymbolType } from './issues.js'; type Identifier = string; -type ExportPosTuple = [number, number, boolean]; +type ExportPosTuple = [number, number, number]; export type Fix = ExportPosTuple | undefined; export type Fixes = Array; diff --git a/packages/knip/src/typescript/visitors/exports/exportAssignment.ts b/packages/knip/src/typescript/visitors/exports/exportAssignment.ts index d56b8e13a..183fb3678 100644 --- a/packages/knip/src/typescript/visitors/exports/exportAssignment.ts +++ b/packages/knip/src/typescript/visitors/exports/exportAssignment.ts @@ -1,4 +1,5 @@ import ts from 'typescript'; +import { FIX_FLAGS } from '../../../constants.js'; import type { Fix } from '../../../types/exports.js'; import { SymbolType } from '../../../types/issues.js'; import { exportVisitor as visit } from '../index.js'; @@ -11,7 +12,7 @@ export default visit( // export default 1; // export = identifier; const pos = node.getChildAt(1).getStart(); - const fix: Fix = isFixExports ? [node.getStart(), node.getEnd() + 1, false] : undefined; + const fix: Fix = isFixExports ? [node.getStart(), node.getEnd() + 1, FIX_FLAGS.NONE] : undefined; // @ts-expect-error We need the symbol in `addExport` const symbol = node.getSourceFile().locals?.get(node.expression.escapedText); return { node, symbol, identifier: 'default', type: SymbolType.UNKNOWN, pos, fix }; diff --git a/packages/knip/src/typescript/visitors/exports/exportDeclaration.ts b/packages/knip/src/typescript/visitors/exports/exportDeclaration.ts index f55f2d210..5fa9873d2 100644 --- a/packages/knip/src/typescript/visitors/exports/exportDeclaration.ts +++ b/packages/knip/src/typescript/visitors/exports/exportDeclaration.ts @@ -1,4 +1,5 @@ import ts from 'typescript'; +import { FIX_FLAGS } from '../../../constants.js'; import type { Fix } from '../../../types/exports.js'; import { SymbolType } from '../../../types/issues.js'; import type { BoundSourceFile } from '../../SourceFile.js'; @@ -25,7 +26,7 @@ export default visit( const type = element.isTypeOnly ? SymbolType.TYPE : nodeType; const fix: Fix = (isFixExports && type !== SymbolType.TYPE) || (isFixTypes && type === SymbolType.TYPE) - ? [element.getStart(), element.getEnd(), true] + ? [element.getStart(), element.getEnd(), FIX_FLAGS.OBJECT_BINDING | FIX_FLAGS.EMPTY_DECLARATION] : undefined; return { node: element, symbol, identifier, type, pos, fix }; }); diff --git a/packages/knip/src/typescript/visitors/exports/exportKeyword.ts b/packages/knip/src/typescript/visitors/exports/exportKeyword.ts index c298f831b..db8b1c62c 100644 --- a/packages/knip/src/typescript/visitors/exports/exportKeyword.ts +++ b/packages/knip/src/typescript/visitors/exports/exportKeyword.ts @@ -1,4 +1,5 @@ import ts from 'typescript'; +import { FIX_FLAGS } from '../../../constants.js'; import type { Fix } from '../../../types/exports.js'; import { SymbolType } from '../../../types/issues.js'; import { compact } from '../../../util/array.js'; @@ -18,10 +19,10 @@ export default visit( if (exportKeyword) { const getFix = (node: ts.Node, defaultKeyword?: ts.Node): Fix => - isFixExports ? [node.getStart(), (defaultKeyword ?? node).getEnd() + 1, false] : undefined; - const getElementFix = (node: ts.Node): Fix => - isFixExports ? [node.getStart(), node.getEnd(), false] : undefined; - const getTypeFix = (node: ts.Node): Fix => (isFixTypes ? [node.getStart(), node.getEnd() + 1, false] : undefined); + isFixExports ? [node.getStart(), (defaultKeyword ?? node).getEnd() + 1, FIX_FLAGS.NONE] : undefined; + + const getTypeFix = (node: ts.Node): Fix => + isFixTypes ? [node.getStart(), node.getEnd() + 1, FIX_FLAGS.NONE] : undefined; if (ts.isVariableStatement(node)) { // @ts-expect-error TODO Issue seems caused by mismatch between returned `node` types (but all ts.Node) @@ -31,7 +32,9 @@ export default visit( return compact( declaration.name.elements.map(element => { if (ts.isIdentifier(element.name)) { - const fix = getElementFix(element); + const fix = isFixExports + ? [element.getStart(), element.getEnd(), FIX_FLAGS.OBJECT_BINDING] + : undefined; return { node: element, identifier: element.name.escapedText.toString(), @@ -48,7 +51,7 @@ export default visit( return compact( declaration.name.elements.map(element => { if (ts.isBindingElement(element)) { - const fix = getElementFix(element); + const fix = isFixExports ? [element.getStart(), element.getEnd(), FIX_FLAGS.NONE] : undefined; return { node: element, identifier: element.getText(), diff --git a/packages/knip/src/typescript/visitors/exports/exportsAccessExpression.ts b/packages/knip/src/typescript/visitors/exports/exportsAccessExpression.ts index 8374c28a5..a18a75922 100644 --- a/packages/knip/src/typescript/visitors/exports/exportsAccessExpression.ts +++ b/packages/knip/src/typescript/visitors/exports/exportsAccessExpression.ts @@ -1,4 +1,5 @@ import ts from 'typescript'; +import { FIX_FLAGS } from '../../../constants.js'; import type { Fix } from '../../../types/exports.js'; import { SymbolType } from '../../../types/issues.js'; import { isJS } from '../helpers.js'; @@ -10,7 +11,7 @@ export default visit(isJS, (node, { isFixExports }) => { // Pattern: exports.NAME const identifier = node.left.name.getText(); const pos = node.left.name.pos; - const fix: Fix = isFixExports ? [node.getStart(), node.getEnd(), false] : undefined; + const fix: Fix = isFixExports ? [node.getStart(), node.getEnd(), FIX_FLAGS.NONE] : undefined; return { node: node.left.name, identifier, diff --git a/packages/knip/src/typescript/visitors/exports/moduleExportsAccessExpression.ts b/packages/knip/src/typescript/visitors/exports/moduleExportsAccessExpression.ts index 5f4486a5b..04bd43a14 100644 --- a/packages/knip/src/typescript/visitors/exports/moduleExportsAccessExpression.ts +++ b/packages/knip/src/typescript/visitors/exports/moduleExportsAccessExpression.ts @@ -1,4 +1,5 @@ import ts from 'typescript'; +import { FIX_FLAGS } from '../../../constants.js'; import type { Fix } from '../../../types/exports.js'; import { SymbolType } from '../../../types/issues.js'; import { hasRequireCall, isModuleExportsAccess, stripQuotes } from '../../ast-helpers.js'; @@ -16,7 +17,7 @@ export default visit(isJS, (node, { isFixExports }) => { // Pattern: module.exports.NAME const identifier = node.expression.left.name.getText(); const pos = node.expression.left.name.pos; - const fix: Fix = isFixExports ? [node.getStart(), node.getEnd(), false] : undefined; + const fix: Fix = isFixExports ? [node.getStart(), node.getEnd(), FIX_FLAGS.NONE] : undefined; return { node: node.expression.left.name, identifier, @@ -30,7 +31,7 @@ export default visit(isJS, (node, { isFixExports }) => { if (ts.isObjectLiteralExpression(expr) && expr.properties.every(ts.isShorthandPropertyAssignment)) { // Pattern: module.exports = { identifier, identifier2 } return expr.properties.map(node => { - const fix: Fix = isFixExports ? [node.getStart(), node.getEnd(), true] : undefined; + const fix: Fix = isFixExports ? [node.getStart(), node.getEnd(), FIX_FLAGS.NONE] : undefined; return { node, identifier: node.getText(), type: SymbolType.UNKNOWN, pos: node.getStart(), fix }; }); } @@ -52,7 +53,7 @@ export default visit(isJS, (node, { isFixExports }) => { // Pattern: module.exports['NAME'] const identifier = stripQuotes(node.expression.left.argumentExpression.getText()); const pos = node.expression.left.argumentExpression.pos; - const fix: Fix = isFixExports ? [node.getStart(), node.getEnd(), false] : undefined; + const fix: Fix = isFixExports ? [node.getStart(), node.getEnd(), FIX_FLAGS.NONE] : undefined; return { node: node.expression.left.argumentExpression, identifier, diff --git a/packages/knip/src/util/clean-export.ts b/packages/knip/src/util/clean-export.ts deleted file mode 100644 index 81a30eb12..000000000 --- a/packages/knip/src/util/clean-export.ts +++ /dev/null @@ -1,105 +0,0 @@ -interface FixerOptions { - text: string; - start: number; - end: number; - isCleanable: boolean; -} - -export const cleanExport = ({ text, start, end, isCleanable }: FixerOptions) => { - const beforeStart = text.substring(0, start); - const afterEnd = text.substring(end); - const exportKeyword = text.substring(start, end).trim(); - - if (exportKeyword === 'export' || exportKeyword === 'export default') return beforeStart + afterEnd; - if (!isCleanable) return beforeStart + afterEnd; - - let bracketOpenIndex = -1; - let bracketCloseIndex = -1; - let commaIndex = -1; - - let i = 0; - while (i <= afterEnd.length) { - const char = afterEnd[i]; - if (char === ',') { - commaIndex = i; - } else if (char === '}') { - bracketCloseIndex = i; - break; - } else if (!/\s/.test(char)) break; - i++; - } - - if (bracketCloseIndex === -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; - while (j >= 0) { - const char = beforeStart[j]; - if (char === '{') { - bracketOpenIndex = j; - 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(); - 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 - exportLength) + afterEnd.substring(fromSpecifierLength); - } - } - - return toBracket.substring(0, toBracket.length - exportLength) + afterEnd.substring(bracketCloseIndex + 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/src/util/remove-export.ts b/packages/knip/src/util/remove-export.ts new file mode 100644 index 000000000..d213c59ce --- /dev/null +++ b/packages/knip/src/util/remove-export.ts @@ -0,0 +1,78 @@ +import { FIX_FLAGS } from '../constants.js'; + +interface FixerOptions { + text: string; + start: number; + end: number; + flags: number; +} + +const getOpeningBracketIndex = (text: string) => { + let bracketOpenIndex = -1; + let j = text.length - 1; + while (j >= 0) { + const char = text[j]; + if (char === '{') { + bracketOpenIndex = j; + break; + } + if (!/\s/.test(char) && char !== ',') { + if (text.substring(j - 3, j + 1) === 'type') { + j = j - 4; + continue; + } + break; + } + j--; + } + return bracketOpenIndex; +}; + +export const removeExport = ({ text, start, end, flags }: FixerOptions) => { + const beforeStart = text.substring(0, start); + const afterEnd = text.substring(end); + + if (flags % FIX_FLAGS.NONE) return beforeStart + afterEnd; + + const exportKeyword = text.substring(start, end).trim(); + if (exportKeyword === 'export' || exportKeyword === 'export default') return beforeStart + afterEnd; + + let closingBracketOffset = -1; + let commaOffset = -1; + + if (flags & FIX_FLAGS.OBJECT_BINDING) { + let i = 0; + while (i <= afterEnd.length) { + const char = afterEnd[i]; + if (char === ',') { + commaOffset = i + 1; + } else if (char === '}') { + closingBracketOffset = i + 1; + break; + } else if (!/\s/.test(char)) break; + i++; + } + } + + if (flags & FIX_FLAGS.EMPTY_DECLARATION && closingBracketOffset !== -1) { + const openingBracketIndex = getOpeningBracketIndex(beforeStart); + if (closingBracketOffset !== -1 && openingBracketIndex !== -1) { + const beforeBracket = beforeStart.substring(0, openingBracketIndex).trim(); + const exportLength = beforeBracket.endsWith('export') ? 6 : beforeBracket.endsWith('export type') ? 12 : 0; + const exportKeywordOffset = beforeBracket.length - exportLength; + if (exportLength) { + const fromBracket = afterEnd.substring(closingBracketOffset).trim(); + if (fromBracket.startsWith('from')) { + const specifierQuoteMatch = afterEnd.match(/['"][^'"]+['"]/); + if (specifierQuoteMatch?.index) { + const fromSpecifierLength = specifierQuoteMatch.index + specifierQuoteMatch[0].length; + return beforeBracket.substring(0, exportKeywordOffset) + afterEnd.substring(fromSpecifierLength); + } + } + return beforeBracket.substring(0, exportKeywordOffset) + afterEnd.substring(closingBracketOffset); + } + } + } + + return beforeStart + (commaOffset === -1 ? afterEnd : afterEnd.substring(commaOffset)); +}; diff --git a/packages/knip/test/fix.test.ts b/packages/knip/test/fix.test.ts index 7a40bedc3..39d0cdb2f 100644 --- a/packages/knip/test/fix.test.ts +++ b/packages/knip/test/fix.test.ts @@ -23,7 +23,7 @@ enum McEnum {} export const z = x + y; -export const { , } = { a: 1, b: 1 }; +export const { } = { a: 1, b: 1 }; export const [, ] = [3, 4]; @@ -43,6 +43,21 @@ export type U = number; `module.exports.USED = 1; +`, + ], + [ + 'default-x.js', + await readContents('default-x.js'), + `const x = 1; + + +export const dx = 1; +`, + ], + [ + 'default.js', + await readContents('default.js'), + `export const d = 1; `, ], [ @@ -59,7 +74,7 @@ module.exports = { identifier, }; await readContents('reexports.js'), `; ; -export { One, Nine, } from './reexported'; +export { One, Rectangle, Nine, setter } from './reexported'; ; ; `, @@ -76,6 +91,8 @@ const Five = 5; ; +export { Four as Rectangle, }; + type Six = any; type Seven = unknown; const Eight = 8; @@ -86,6 +103,10 @@ type Ten = unknown[]; export { Nine, }; export const One = 1; + +const fn = () => ({ get: () => 1, set: () => 1 }); + +export const { set: setter } = fn(); `, ], [ @@ -166,6 +187,36 @@ export default class MyClass {} /** @knipignore */ export type U = number; +`, + ], + [ + 'reexported.ts', + await readContents('reexported.ts'), + `const Two = 2; +const Three = 3; +const Four = 4; +const Five = 5; + +export { Two, Three }; + +export { Four as Fourth, Five as Fifth }; + +export { Four as Rectangle, Five as Pentagon }; + +type Six = any; +type Seven = unknown; +const Eight = 8; +const Nine = 9; +type Ten = unknown[]; +; + +export { Eight, Nine, }; + +export const One = 1; + +const fn = () => ({ get: () => 1, set: () => 1 }); + +export const { get: getter, set: setter } = fn(); `, ], ]; diff --git a/packages/knip/test/util/clean-export.test.ts b/packages/knip/test/util/clean-export.test.ts deleted file mode 100644 index 58a73d0aa..000000000 --- a/packages/knip/test/util/clean-export.test.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { test } from 'bun:test'; -import assert from 'node:assert/strict'; -import { cleanExport } from '../../src/util/clean-export.js'; - -const getOpts = (text: string, value: string) => { - const start = text.indexOf(value); - return { text, start, end: start + 2, isCleanable: true }; -}; - -test('fixer', () => { - { - const text = 'export { AB }'; - assert.deepEqual(cleanExport(getOpts(text, 'AB')), ''); - } - - { - const text = 'export { AB, CD }'; - assert.deepEqual(cleanExport(getOpts(text, 'AB')), 'export { CD }'); - } - - { - const text = 'export { AB, CD, EF }'; - assert.deepEqual(cleanExport(getOpts(text, 'CD')), 'export { AB, EF }'); - } - - { - const text = 'export { AB, CD } from "specifier"'; - assert.deepEqual(cleanExport(getOpts(text, 'CD')), 'export { AB, } from "specifier"'); - } - - { - const text = 'export { AB, CD, EF } from "specifier"'; - assert.deepEqual(cleanExport(getOpts(text, 'CD')), 'export { AB, EF } from "specifier"'); - } - - { - const text = 'export { AB } from "specifier"'; - assert.deepEqual(cleanExport(getOpts(text, 'AB')), ''); - } - - { - const text = "export { AB } from './specifier'"; - assert.deepEqual(cleanExport(getOpts(text, 'AB')), ''); - } - - { - const text = 'export{AB}from"specifier"'; - assert.deepEqual(cleanExport(getOpts(text, 'AB')), ''); - } - - { - 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, }'); - } -}); diff --git a/packages/knip/test/util/remove-export.test.ts b/packages/knip/test/util/remove-export.test.ts new file mode 100644 index 000000000..bc8d823f7 --- /dev/null +++ b/packages/knip/test/util/remove-export.test.ts @@ -0,0 +1,150 @@ +import { test } from 'bun:test'; +import assert from 'node:assert/strict'; +import { FIX_FLAGS } from '../../src/constants.js'; +import { removeExport } from '../../src/util/remove-export.js'; + +const getOpts = (text: string, value: string, flags = FIX_FLAGS.OBJECT_BINDING | FIX_FLAGS.EMPTY_DECLARATION) => { + const start = text.indexOf(value); + return { text, start, end: start + value.length, flags }; +}; + +test('Clean export (FIX_FLAGS.NONE)', () => { + { + const text = 'export const x = 1;'; + assert.deepEqual(removeExport(getOpts(text, 'export ', 0)), 'const x = 1;'); + } + + { + const text = 'export default 1'; + assert.deepEqual(removeExport(getOpts(text, 'export default 1', 0)), ''); + } + + { + const text = 'export default class X {};'; + assert.deepEqual(removeExport(getOpts(text, 'export default ', 0)), 'class X {};'); + } + + { + const text = 'export = { x,\ny};'; + assert.deepEqual(removeExport(getOpts(text, 'export = { x,\ny}', 0)), ';'); + } + + { + const text = 'export const [ AB, CD ] = [1, 2];'; + assert.deepEqual(removeExport(getOpts(text, 'AB', 0)), 'export const [ , CD ] = [1, 2];'); + } +}); + +test('Clean export (FIX_FLAGS.OBJECT_BINDING)', () => { + { + const text = 'export const { AB, CD } = {};'; + assert.deepEqual(removeExport(getOpts(text, 'AB', 1)), 'export const { CD } = {};'); + } + + { + const text = 'export const { AB: A_B, CD: C_D } = fn();'; + assert.deepEqual(removeExport(getOpts(text, 'AB: A_B', 1)), 'export const { CD: C_D } = fn();'); + } +}); + +test('Clean export (FIX_FLAGS.EMPTY_DECLARATION)', () => { + { + const text = 'export { AB }'; + assert.deepEqual(removeExport(getOpts(text, 'AB')), ''); + } + + { + const text = 'export { AB, CD }'; + assert.deepEqual(removeExport(getOpts(text, 'AB')), 'export { CD }'); + } + + { + const text = 'export { AB, CD, EF }'; + assert.deepEqual(removeExport(getOpts(text, 'CD')), 'export { AB, EF }'); + } + + { + const text = 'export { AB, CD } from "specifier"'; + assert.deepEqual(removeExport(getOpts(text, 'CD')), 'export { AB, } from "specifier"'); + } + + { + const text = 'export { AB, CD, EF } from "specifier"'; + assert.deepEqual(removeExport(getOpts(text, 'CD')), 'export { AB, EF } from "specifier"'); + } + + { + const text = 'export { AB } from "specifier"'; + assert.deepEqual(removeExport(getOpts(text, 'AB')), ''); + } + + { + const text = "export { AB } from './specifier'"; + assert.deepEqual(removeExport(getOpts(text, 'AB')), ''); + } + + { + const text = "export { AB as A_B } from './specifier'"; + assert.deepEqual(removeExport(getOpts(text, 'AB as A_B')), ''); + } + + { + const text = 'export{AB}from"specifier"'; + assert.deepEqual(removeExport(getOpts(text, 'AB')), ''); + } + + { + const text = 'export{AB}\n\n\n from\n\n\n "specifier"'; + assert.deepEqual(removeExport(getOpts(text, 'AB')), ''); + } + + { + const text = 'export { AB } from "specifier"'; + assert.deepEqual(removeExport(getOpts(text, 'AB')), ''); + } + + { + const text = 'export type { AB }'; + assert.deepEqual(removeExport(getOpts(text, 'AB')), ''); + } + + { + const text = 'export { type AB }'; + assert.deepEqual(removeExport(getOpts(text, 'type AB')), ''); + } + + { + const text = 'export type { AB as A_B }'; + assert.deepEqual(removeExport(getOpts(text, 'AB as A_B')), ''); + } + + { + const text = 'export { type AB as A_B }'; + assert.deepEqual(removeExport(getOpts(text, 'type AB as A_B')), ''); + } + + { + const text = 'export { type AB, type CD, type EF }'; + assert.deepEqual(removeExport(getOpts(text, 'type CD')), 'export { type AB, type EF }'); + } + + { + const text = 'export { type AB, CD, type EF }'; + assert.deepEqual(removeExport(getOpts(text, 'type AB')), 'export { CD, type EF }'); + } + + { + const text = 'export { AB, CD, type EF }'; + assert.deepEqual(removeExport(getOpts(text, 'type EF')), 'export { AB, CD, }'); + } + + { + const text = 'export { AB, \nCD , \ntype EF, }'; + assert.deepEqual(removeExport(getOpts(text, 'type EF')), 'export { AB, \nCD , \n }'); + } + + { + const text = 'export { , \ntype EF, } from "specifier";'; + assert.deepEqual(removeExport(getOpts(text, 'type EF')), ';'); + } +});