diff --git a/docs/rules/no-unused-modules.md b/docs/rules/no-unused-modules.md new file mode 100644 index 0000000000..96d689123a --- /dev/null +++ b/docs/rules/no-unused-modules.md @@ -0,0 +1,101 @@ +# import/no-unused-modules + +Reports: + - modules without any exports + - individual exports not being statically `import`ed or `require`ed from other modules in the same project + +Note: dynamic imports are currently not supported. + +## Rule Details + + +### Options + +This rule takes the following option: + +- `src`: an array with files/paths to be analyzed. It only applies to unused exports. Defaults to `process.cwd()`, if not provided +- `ignoreExports`: an array with files/paths for which unused exports will not be reported (e.g module entry points in a published package) +- `missingExports`: if `true`, files without any exports are reported +- `unusedExports`: if `true`, exports without any static usage within other modules are reported. + + +### Example for missing exports +#### The following will be reported +```js +const class MyClass { /*...*/ } + +function makeClass() { return new MyClass(...arguments) } +``` + +#### The following will not be reported + +```js +export default function () { /*...*/ } +``` +```js +export const foo = function () { /*...*/ } +``` +```js +export { foo, bar } +``` +```js +export { foo as bar } +``` + +### Example for unused exports +given file-f: +```js +import { e } from 'file-a' +import { f } from 'file-b' +import * from 'file-c' +export * from 'file-d' +export { default, i0 } from 'file-e' // both will be reported + +export const j = 99 // will be reported +``` +and file-e: +```js +export const i0 = 9 // will not be reported +export const i1 = 9 // will be reported +export default () => {} // will not be reported +``` +and file-d: +```js +export const h = 8 // will not be reported +export default () => {} // will be reported, as export * only considers named exports and ignores default exports +``` +and file-c: +```js +export const g = 7 // will not be reported +export default () => {} // will not be reported +``` +and file-b: +```js +import two, { b, c, doAnything } from 'file-a' + +export const f = 6 // will not be reported +``` +and file-a: +```js +const b = 2 +const c = 3 +const d = 4 + +export const a = 1 // will be reported + +export { b, c } // will not be reported + +export { d as e } // will not be reported + +export function doAnything() { + // some code +} // will not be reported + +export default 5 // will not be reported +``` + + + +## When not to use + +If you don't mind having unused files or dead code within your codebase, you can disable this rule diff --git a/src/ExportMap.js b/src/ExportMap.js index 1cb5dc3e9c..5e12351957 100644 --- a/src/ExportMap.js +++ b/src/ExportMap.js @@ -379,6 +379,18 @@ ExportMap.parse = function (path, content, context) { function captureDependency(declaration) { if (declaration.source == null) return null + const importedSpecifiers = new Set() + const supportedTypes = new Set(['ImportDefaultSpecifier', 'ImportNamespaceSpecifier']) + if (declaration.specifiers) { + declaration.specifiers.forEach(specifier => { + if (supportedTypes.has(specifier.type)) { + importedSpecifiers.add(specifier.type) + } + if (specifier.type === 'ImportSpecifier') { + importedSpecifiers.add(specifier.local.name) + } + }) + } const p = remotePath(declaration.source.value) if (p == null) return null @@ -392,6 +404,7 @@ ExportMap.parse = function (path, content, context) { value: declaration.source.value, loc: declaration.source.loc, }, + importedSpecifiers, }) return getter } diff --git a/src/index.js b/src/index.js index 7df67867f5..7b5ef667a2 100644 --- a/src/index.js +++ b/src/index.js @@ -18,6 +18,7 @@ export const rules = { 'no-named-as-default': require('./rules/no-named-as-default'), 'no-named-as-default-member': require('./rules/no-named-as-default-member'), 'no-anonymous-default-export': require('./rules/no-anonymous-default-export'), + 'no-unused-modules': require('./rules/no-unused-modules'), 'no-commonjs': require('./rules/no-commonjs'), 'no-amd': require('./rules/no-amd'), diff --git a/src/rules/no-unused-modules.js b/src/rules/no-unused-modules.js new file mode 100644 index 0000000000..9f4203dc18 --- /dev/null +++ b/src/rules/no-unused-modules.js @@ -0,0 +1,706 @@ +/** + * @fileOverview Ensures that modules contain exports and/or all + * modules are consumed within other modules. + * @author René Fermann + */ + +import Exports from '../ExportMap' +import resolve from 'eslint-module-utils/resolve' +import docsUrl from '../docsUrl' + +// eslint/lib/util/glob-util has been moved to eslint/lib/util/glob-utils with version 5.3 +let listFilesToProcess +try { + listFilesToProcess = require('eslint/lib/util/glob-utils').listFilesToProcess +} catch (err) { + listFilesToProcess = require('eslint/lib/util/glob-util').listFilesToProcess +} + +const EXPORT_DEFAULT_DECLARATION = 'ExportDefaultDeclaration' +const EXPORT_NAMED_DECLARATION = 'ExportNamedDeclaration' +const EXPORT_ALL_DECLARATION = 'ExportAllDeclaration' +const IMPORT_DECLARATION = 'ImportDeclaration' +const IMPORT_NAMESPACE_SPECIFIER = 'ImportNamespaceSpecifier' +const IMPORT_DEFAULT_SPECIFIER = 'ImportDefaultSpecifier' +const VARIABLE_DECLARATION = 'VariableDeclaration' +const FUNCTION_DECLARATION = 'FunctionDeclaration' +const DEFAULT = 'default' + +let preparationDone = false +const importList = new Map() +const exportList = new Map() +const ignoredFiles = new Set() + +const isNodeModule = path => { + return /\/(node_modules)\//.test(path) +} + +/** + * read all files matching the patterns in src and ignoreExports + * + * return all files matching src pattern, which are not matching the ignoreExports pattern + */ +const resolveFiles = (src, ignoreExports) => { + const srcFiles = new Set() + const srcFileList = listFilesToProcess(src) + + // prepare list of ignored files + const ignoredFilesList = listFilesToProcess(ignoreExports) + ignoredFilesList.forEach(({ filename }) => ignoredFiles.add(filename)) + + // prepare list of source files, don't consider files from node_modules + srcFileList.filter(({ filename }) => !isNodeModule(filename)).forEach(({ filename }) => { + srcFiles.add(filename) + }) + return srcFiles +} + +/** + * parse all source files and build up 2 maps containing the existing imports and exports + */ +const prepareImportsAndExports = (srcFiles, context) => { + const exportAll = new Map() + srcFiles.forEach(file => { + const exports = new Map() + const imports = new Map() + const currentExports = Exports.get(file, context) + if (currentExports) { + const { dependencies, reexports, imports: localImportList, namespace } = currentExports + + // dependencies === export * from + const currentExportAll = new Set() + dependencies.forEach(value => { + currentExportAll.add(value().path) + }) + exportAll.set(file, currentExportAll) + + reexports.forEach((value, key) => { + if (key === DEFAULT) { + exports.set(IMPORT_DEFAULT_SPECIFIER, { whereUsed: new Set() }) + } else { + exports.set(key, { whereUsed: new Set() }) + } + const reexport = value.getImport() + if (!reexport) { + return + } + let localImport = imports.get(reexport.path) + let currentValue + if (value.local === DEFAULT) { + currentValue = IMPORT_DEFAULT_SPECIFIER + } else { + currentValue = value.local + } + if (typeof localImport !== 'undefined') { + localImport = new Set([...localImport, currentValue]) + } else { + localImport = new Set([currentValue]) + } + imports.set(reexport.path, localImport) + }) + + localImportList.forEach((value, key) => { + if (isNodeModule(key)) { + return + } + imports.set(key, value.importedSpecifiers) + }) + importList.set(file, imports) + + // build up export list only, if file is not ignored + if (ignoredFiles.has(file)) { + return + } + namespace.forEach((value, key) => { + if (key === DEFAULT) { + exports.set(IMPORT_DEFAULT_SPECIFIER, { whereUsed: new Set() }) + } else { + exports.set(key, { whereUsed: new Set() }) + } + }) + } + exports.set(EXPORT_ALL_DECLARATION, { whereUsed: new Set() }) + exports.set(IMPORT_NAMESPACE_SPECIFIER, { whereUsed: new Set() }) + exportList.set(file, exports) + }) + exportAll.forEach((value, key) => { + value.forEach(val => { + const currentExports = exportList.get(val) + const currentExport = currentExports.get(EXPORT_ALL_DECLARATION) + currentExport.whereUsed.add(key) + }) + }) +} + +/** + * traverse through all imports and add the respective path to the whereUsed-list + * of the corresponding export + */ +const determineUsage = () => { + importList.forEach((listValue, listKey) => { + listValue.forEach((value, key) => { + const exports = exportList.get(key) + if (typeof exports !== 'undefined') { + value.forEach(currentImport => { + let specifier + if (currentImport === IMPORT_NAMESPACE_SPECIFIER) { + specifier = IMPORT_NAMESPACE_SPECIFIER + } else if (currentImport === IMPORT_DEFAULT_SPECIFIER) { + specifier = IMPORT_DEFAULT_SPECIFIER + } else { + specifier = currentImport + } + if (typeof specifier !== 'undefined') { + const exportStatement = exports.get(specifier) + if (typeof exportStatement !== 'undefined') { + const { whereUsed } = exportStatement + whereUsed.add(listKey) + exports.set(specifier, { whereUsed }) + } + } + }) + } + }) + }) +} + +const getSrc = src => { + if (src) { + return src + } + return [process.cwd()] +} + +/** + * prepare the lists of existing imports and exports - should only be executed once at + * the start of a new eslint run + */ +const doPreparation = (src, ignoreExports, context) => { + const srcFiles = resolveFiles(getSrc(src), ignoreExports) + prepareImportsAndExports(srcFiles, context) + determineUsage() + preparationDone = true +} + +const newNamespaceImportExists = specifiers => + specifiers.some(({ type }) => type === IMPORT_NAMESPACE_SPECIFIER) + +const newDefaultImportExists = specifiers => + specifiers.some(({ type }) => type === IMPORT_DEFAULT_SPECIFIER) + +module.exports = { + meta: { + docs: { url: docsUrl('no-unused-modules') }, + schema: [{ + properties: { + src: { + description: 'files/paths to be analyzed (only for unused exports)', + type: 'array', + minItems: 1, + items: { + type: 'string', + minLength: 1, + }, + }, + ignoreExports: { + description: + 'files/paths for which unused exports will not be reported (e.g module entry points)', + type: 'array', + minItems: 1, + items: { + type: 'string', + minLength: 1, + }, + }, + missingExports: { + description: 'report modules without any exports', + type: 'boolean', + }, + unusedExports: { + description: 'report exports without any usage', + type: 'boolean', + }, + }, + not: { + properties: { + unusedExports: { enum: [false] }, + missingExports: { enum: [false] }, + }, + }, + anyOf:[{ + not: { + properties: { + unusedExports: { enum: [true] }, + }, + }, + required: ['missingExports'], + }, { + not: { + properties: { + missingExports: { enum: [true] }, + }, + }, + required: ['unusedExports'], + }, { + properties: { + unusedExports: { enum: [true] }, + }, + required: ['unusedExports'], + }, { + properties: { + missingExports: { enum: [true] }, + }, + required: ['missingExports'], + }], + }], + }, + + create: context => { + const { + src, + ignoreExports = [], + missingExports, + unusedExports, + } = context.options[0] + + if (unusedExports && !preparationDone) { + doPreparation(src, ignoreExports, context) + } + + const file = context.getFilename() + + const checkExportPresence = node => { + if (!missingExports) { + return + } + + const exportCount = exportList.get(file) + const exportAll = exportCount.get(EXPORT_ALL_DECLARATION) + const namespaceImports = exportCount.get(IMPORT_NAMESPACE_SPECIFIER) + + exportCount.delete(EXPORT_ALL_DECLARATION) + exportCount.delete(IMPORT_NAMESPACE_SPECIFIER) + if (missingExports && exportCount.size < 1) { + // node.body[0] === 'undefined' only happens, if everything is commented out in the file + // being linted + context.report(node.body[0] ? node.body[0] : node, 'No exports found') + } + exportCount.set(EXPORT_ALL_DECLARATION, exportAll) + exportCount.set(IMPORT_NAMESPACE_SPECIFIER, namespaceImports) + } + + const checkUsage = (node, exportedValue) => { + if (!unusedExports) { + return + } + + if (ignoredFiles.has(file)) { + return + } + + exports = exportList.get(file) + + // special case: export * from + const exportAll = exports.get(EXPORT_ALL_DECLARATION) + if (typeof exportAll !== 'undefined' && exportedValue !== IMPORT_DEFAULT_SPECIFIER) { + if (exportAll.whereUsed.size > 0) { + return + } + } + + // special case: namespace import + const namespaceImports = exports.get(IMPORT_NAMESPACE_SPECIFIER) + if (typeof namespaceImports !== 'undefined') { + if (namespaceImports.whereUsed.size > 0) { + return + } + } + + const exportStatement = exports.get(exportedValue) + + const value = exportedValue === IMPORT_DEFAULT_SPECIFIER ? DEFAULT : exportedValue + + if (typeof exportStatement !== 'undefined'){ + if (exportStatement.whereUsed.size < 1) { + context.report( + node, + `exported declaration '${value}' not used within other modules` + ) + } + } else { + context.report( + node, + `exported declaration '${value}' not used within other modules` + ) + } + } + + /** + * only useful for tools like vscode-eslint + * + * update lists of existing exports during runtime + */ + const updateExportUsage = node => { + if (ignoredFiles.has(file)) { + return + } + + let exports = exportList.get(file) + + // new module has been created during runtime + // include it in further processing + if (typeof exports === 'undefined') { + exports = new Map() + } + + const newExports = new Map() + const newExportIdentifiers = new Set() + + node.body.forEach(({ type, declaration, specifiers }) => { + if (type === EXPORT_DEFAULT_DECLARATION) { + newExportIdentifiers.add(IMPORT_DEFAULT_SPECIFIER) + } + if (type === EXPORT_NAMED_DECLARATION) { + if (specifiers.length > 0) { + specifiers.forEach(specifier => { + if (specifier.exported) { + newExportIdentifiers.add(specifier.exported.name) + } + }) + } + if (declaration) { + if (declaration.type === FUNCTION_DECLARATION) { + newExportIdentifiers.add(declaration.id.name) + } + if (declaration.type === VARIABLE_DECLARATION) { + declaration.declarations.forEach(({ id }) => { + newExportIdentifiers.add(id.name) + }) + } + } + } + }) + + // old exports exist within list of new exports identifiers: add to map of new exports + exports.forEach((value, key) => { + if (newExportIdentifiers.has(key)) { + newExports.set(key, value) + } + }) + + // new export identifiers added: add to map of new exports + newExportIdentifiers.forEach(key => { + if (!exports.has(key)) { + newExports.set(key, { whereUsed: new Set() }) + } + }) + + // preserve information about namespace imports + let exportAll = exports.get(EXPORT_ALL_DECLARATION) + let namespaceImports = exports.get(IMPORT_NAMESPACE_SPECIFIER) + + if (typeof namespaceImports === 'undefined') { + namespaceImports = { whereUsed: new Set() } + } + + newExports.set(EXPORT_ALL_DECLARATION, exportAll) + newExports.set(IMPORT_NAMESPACE_SPECIFIER, namespaceImports) + exportList.set(file, newExports) + } + + /** + * only useful for tools like vscode-eslint + * + * update lists of existing imports during runtime + */ + const updateImportUsage = node => { + if (!unusedExports) { + return + } + + let oldImportPaths = importList.get(file) + if (typeof oldImportPaths === 'undefined') { + oldImportPaths = new Map() + } + + const oldNamespaceImports = new Set() + const newNamespaceImports = new Set() + + const oldExportAll = new Set() + const newExportAll = new Set() + + const oldDefaultImports = new Set() + const newDefaultImports = new Set() + + const oldImports = new Map() + const newImports = new Map() + oldImportPaths.forEach((value, key) => { + if (value.has(EXPORT_ALL_DECLARATION)) { + oldExportAll.add(key) + } + if (value.has(IMPORT_NAMESPACE_SPECIFIER)) { + oldNamespaceImports.add(key) + } + if (value.has(IMPORT_DEFAULT_SPECIFIER)) { + oldDefaultImports.add(key) + } + value.forEach(val => { + if (val !== IMPORT_NAMESPACE_SPECIFIER && + val !== IMPORT_DEFAULT_SPECIFIER) { + oldImports.set(val, key) + } + }) + }) + + node.body.forEach(astNode => { + let resolvedPath + + // support for export { value } from 'module' + if (astNode.type === EXPORT_NAMED_DECLARATION) { + if (astNode.source) { + resolvedPath = resolve(astNode.source.value, context) + astNode.specifiers.forEach(specifier => { + let name + if (specifier.exported.name === DEFAULT) { + name = IMPORT_DEFAULT_SPECIFIER + } else { + name = specifier.local.name + } + newImports.set(name, resolvedPath) + }) + } + } + + if (astNode.type === EXPORT_ALL_DECLARATION) { + resolvedPath = resolve(astNode.source.value, context) + newExportAll.add(resolvedPath) + } + + if (astNode.type === IMPORT_DECLARATION) { + resolvedPath = resolve(astNode.source.value, context) + if (!resolvedPath) { + return + } + + if (isNodeModule(resolvedPath)) { + return + } + + if (newNamespaceImportExists(astNode.specifiers)) { + newNamespaceImports.add(resolvedPath) + } + + if (newDefaultImportExists(astNode.specifiers)) { + newDefaultImports.add(resolvedPath) + } + + astNode.specifiers.forEach(specifier => { + if (specifier.type === IMPORT_DEFAULT_SPECIFIER || + specifier.type === IMPORT_NAMESPACE_SPECIFIER) { + return + } + newImports.set(specifier.local.name, resolvedPath) + }) + } + }) + + newExportAll.forEach(value => { + if (!oldExportAll.has(value)) { + let imports = oldImportPaths.get(value) + if (typeof imports === 'undefined') { + imports = new Set() + } + imports.add(EXPORT_ALL_DECLARATION) + oldImportPaths.set(value, imports) + + let exports = exportList.get(value) + let currentExport + if (typeof exports !== 'undefined') { + currentExport = exports.get(EXPORT_ALL_DECLARATION) + } else { + exports = new Map() + exportList.set(value, exports) + } + + if (typeof currentExport !== 'undefined') { + currentExport.whereUsed.add(file) + } else { + const whereUsed = new Set() + whereUsed.add(file) + exports.set(EXPORT_ALL_DECLARATION, { whereUsed }) + } + } + }) + + oldExportAll.forEach(value => { + if (!newExportAll.has(value)) { + const imports = oldImportPaths.get(value) + imports.delete(EXPORT_ALL_DECLARATION) + + const exports = exportList.get(value) + if (typeof exports !== 'undefined') { + const currentExport = exports.get(EXPORT_ALL_DECLARATION) + if (typeof currentExport !== 'undefined') { + currentExport.whereUsed.delete(file) + } + } + } + }) + + newDefaultImports.forEach(value => { + if (!oldDefaultImports.has(value)) { + let imports = oldImportPaths.get(value) + if (typeof imports === 'undefined') { + imports = new Set() + } + imports.add(IMPORT_DEFAULT_SPECIFIER) + oldImportPaths.set(value, imports) + + let exports = exportList.get(value) + let currentExport + if (typeof exports !== 'undefined') { + currentExport = exports.get(IMPORT_DEFAULT_SPECIFIER) + } else { + exports = new Map() + exportList.set(value, exports) + } + + if (typeof currentExport !== 'undefined') { + currentExport.whereUsed.add(file) + } else { + const whereUsed = new Set() + whereUsed.add(file) + exports.set(IMPORT_DEFAULT_SPECIFIER, { whereUsed }) + } + } + }) + + oldDefaultImports.forEach(value => { + if (!newDefaultImports.has(value)) { + const imports = oldImportPaths.get(value) + imports.delete(IMPORT_DEFAULT_SPECIFIER) + + const exports = exportList.get(value) + if (typeof exports !== 'undefined') { + const currentExport = exports.get(IMPORT_DEFAULT_SPECIFIER) + if (typeof currentExport !== 'undefined') { + currentExport.whereUsed.delete(file) + } + } + } + }) + + newNamespaceImports.forEach(value => { + if (!oldNamespaceImports.has(value)) { + let imports = oldImportPaths.get(value) + if (typeof imports === 'undefined') { + imports = new Set() + } + imports.add(IMPORT_NAMESPACE_SPECIFIER) + oldImportPaths.set(value, imports) + + let exports = exportList.get(value) + let currentExport + if (typeof exports !== 'undefined') { + currentExport = exports.get(IMPORT_NAMESPACE_SPECIFIER) + } else { + exports = new Map() + exportList.set(value, exports) + } + + if (typeof currentExport !== 'undefined') { + currentExport.whereUsed.add(file) + } else { + const whereUsed = new Set() + whereUsed.add(file) + exports.set(IMPORT_NAMESPACE_SPECIFIER, { whereUsed }) + } + } + }) + + oldNamespaceImports.forEach(value => { + if (!newNamespaceImports.has(value)) { + const imports = oldImportPaths.get(value) + imports.delete(IMPORT_NAMESPACE_SPECIFIER) + + const exports = exportList.get(value) + if (typeof exports !== 'undefined') { + const currentExport = exports.get(IMPORT_NAMESPACE_SPECIFIER) + if (typeof currentExport !== 'undefined') { + currentExport.whereUsed.delete(file) + } + } + } + }) + + newImports.forEach((value, key) => { + if (!oldImports.has(key)) { + let imports = oldImportPaths.get(value) + if (typeof imports === 'undefined') { + imports = new Set() + } + imports.add(key) + oldImportPaths.set(value, imports) + + let exports = exportList.get(value) + let currentExport + if (typeof exports !== 'undefined') { + currentExport = exports.get(key) + } else { + exports = new Map() + exportList.set(value, exports) + } + + if (typeof currentExport !== 'undefined') { + currentExport.whereUsed.add(file) + } else { + const whereUsed = new Set() + whereUsed.add(file) + exports.set(key, { whereUsed }) + } + } + }) + + oldImports.forEach((value, key) => { + if (!newImports.has(key)) { + const imports = oldImportPaths.get(value) + imports.delete(key) + + const exports = exportList.get(value) + if (typeof exports !== 'undefined') { + const currentExport = exports.get(key) + if (typeof currentExport !== 'undefined') { + currentExport.whereUsed.delete(file) + } + } + } + }) + } + + return { + 'Program:exit': node => { + updateExportUsage(node) + updateImportUsage(node) + checkExportPresence(node) + }, + 'ExportDefaultDeclaration': node => { + checkUsage(node, IMPORT_DEFAULT_SPECIFIER) + }, + 'ExportNamedDeclaration': node => { + node.specifiers.forEach(specifier => { + checkUsage(node, specifier.exported.name) + }) + if (node.declaration) { + if (node.declaration.type === FUNCTION_DECLARATION) { + checkUsage(node, node.declaration.id.name) + } + if (node.declaration.type === VARIABLE_DECLARATION) { + node.declaration.declarations.forEach(declaration => { + checkUsage(node, declaration.id.name) + }) + } + } + }, + } + }, +} diff --git a/tests/files/no-unused-modules/empty_file.js b/tests/files/no-unused-modules/empty_file.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/files/no-unused-modules/file-0.js b/tests/files/no-unused-modules/file-0.js new file mode 100644 index 0000000000..e7615ad4f6 --- /dev/null +++ b/tests/files/no-unused-modules/file-0.js @@ -0,0 +1,12 @@ +import eslint from 'eslint' +import fileA from './file-a' +import { b } from './file-b' +import { c1, c2 } from './file-c' +import { d } from './file-d' +import { e } from './file-e' +import { e2 } from './file-e' +import { h2 } from './file-h' +import * as l from './file-l' +export * from './file-n' +export { default, o0, o3 } from './file-o' +export { p } from './file-p' diff --git a/tests/files/no-unused-modules/file-a.js b/tests/files/no-unused-modules/file-a.js new file mode 100644 index 0000000000..7c16dd4dd0 --- /dev/null +++ b/tests/files/no-unused-modules/file-a.js @@ -0,0 +1,2 @@ +import { o2 } from './file-o' +export default () => 1 diff --git a/tests/files/no-unused-modules/file-b.js b/tests/files/no-unused-modules/file-b.js new file mode 100644 index 0000000000..2e9a7c1c29 --- /dev/null +++ b/tests/files/no-unused-modules/file-b.js @@ -0,0 +1 @@ +export const b = 2 diff --git a/tests/files/no-unused-modules/file-c.js b/tests/files/no-unused-modules/file-c.js new file mode 100644 index 0000000000..44d2b2cc38 --- /dev/null +++ b/tests/files/no-unused-modules/file-c.js @@ -0,0 +1,7 @@ +const c1 = 3 + +function c2() { + return 3 +} + +export { c1, c2 } diff --git a/tests/files/no-unused-modules/file-d.js b/tests/files/no-unused-modules/file-d.js new file mode 100644 index 0000000000..c8b6f3fa67 --- /dev/null +++ b/tests/files/no-unused-modules/file-d.js @@ -0,0 +1,3 @@ +export function d() { + return 4 +} diff --git a/tests/files/no-unused-modules/file-e.js b/tests/files/no-unused-modules/file-e.js new file mode 100644 index 0000000000..a2b05f6041 --- /dev/null +++ b/tests/files/no-unused-modules/file-e.js @@ -0,0 +1,3 @@ +const e0 = 5 + +export { e0 as e } diff --git a/tests/files/no-unused-modules/file-f.js b/tests/files/no-unused-modules/file-f.js new file mode 100644 index 0000000000..b2a29e5597 --- /dev/null +++ b/tests/files/no-unused-modules/file-f.js @@ -0,0 +1 @@ +export default () => 1 diff --git a/tests/files/no-unused-modules/file-g.js b/tests/files/no-unused-modules/file-g.js new file mode 100644 index 0000000000..4a6bb623d7 --- /dev/null +++ b/tests/files/no-unused-modules/file-g.js @@ -0,0 +1 @@ +export const g = 2 diff --git a/tests/files/no-unused-modules/file-h.js b/tests/files/no-unused-modules/file-h.js new file mode 100644 index 0000000000..b38d70e548 --- /dev/null +++ b/tests/files/no-unused-modules/file-h.js @@ -0,0 +1,7 @@ +const h1 = 3 + +function h2() { + return 3 +} + +export { h1, h2 } diff --git a/tests/files/no-unused-modules/file-i.js b/tests/files/no-unused-modules/file-i.js new file mode 100644 index 0000000000..6c1fee78bc --- /dev/null +++ b/tests/files/no-unused-modules/file-i.js @@ -0,0 +1,7 @@ +const i1 = 3 + +function i2() { + return 3 +} + +export { i1, i2 } diff --git a/tests/files/no-unused-modules/file-ignored-a.js b/tests/files/no-unused-modules/file-ignored-a.js new file mode 100644 index 0000000000..b2a29e5597 --- /dev/null +++ b/tests/files/no-unused-modules/file-ignored-a.js @@ -0,0 +1 @@ +export default () => 1 diff --git a/tests/files/no-unused-modules/file-ignored-b.js b/tests/files/no-unused-modules/file-ignored-b.js new file mode 100644 index 0000000000..2e9a7c1c29 --- /dev/null +++ b/tests/files/no-unused-modules/file-ignored-b.js @@ -0,0 +1 @@ +export const b = 2 diff --git a/tests/files/no-unused-modules/file-ignored-c.js b/tests/files/no-unused-modules/file-ignored-c.js new file mode 100644 index 0000000000..44d2b2cc38 --- /dev/null +++ b/tests/files/no-unused-modules/file-ignored-c.js @@ -0,0 +1,7 @@ +const c1 = 3 + +function c2() { + return 3 +} + +export { c1, c2 } diff --git a/tests/files/no-unused-modules/file-ignored-d.js b/tests/files/no-unused-modules/file-ignored-d.js new file mode 100644 index 0000000000..c8b6f3fa67 --- /dev/null +++ b/tests/files/no-unused-modules/file-ignored-d.js @@ -0,0 +1,3 @@ +export function d() { + return 4 +} diff --git a/tests/files/no-unused-modules/file-ignored-e.js b/tests/files/no-unused-modules/file-ignored-e.js new file mode 100644 index 0000000000..a2b05f6041 --- /dev/null +++ b/tests/files/no-unused-modules/file-ignored-e.js @@ -0,0 +1,3 @@ +const e0 = 5 + +export { e0 as e } diff --git a/tests/files/no-unused-modules/file-ignored-l.js b/tests/files/no-unused-modules/file-ignored-l.js new file mode 100644 index 0000000000..48b2e14ad0 --- /dev/null +++ b/tests/files/no-unused-modules/file-ignored-l.js @@ -0,0 +1,6 @@ +const l0 = 5 +const l = 10 + +export { l0 as l1, l } + +export default () => {} diff --git a/tests/files/no-unused-modules/file-j.js b/tests/files/no-unused-modules/file-j.js new file mode 100644 index 0000000000..c59fb69273 --- /dev/null +++ b/tests/files/no-unused-modules/file-j.js @@ -0,0 +1,3 @@ +export function j() { + return 4 +} diff --git a/tests/files/no-unused-modules/file-k.js b/tests/files/no-unused-modules/file-k.js new file mode 100644 index 0000000000..62edf882d7 --- /dev/null +++ b/tests/files/no-unused-modules/file-k.js @@ -0,0 +1,3 @@ +const k0 = 5 + +export { k0 as k } diff --git a/tests/files/no-unused-modules/file-l.js b/tests/files/no-unused-modules/file-l.js new file mode 100644 index 0000000000..48b2e14ad0 --- /dev/null +++ b/tests/files/no-unused-modules/file-l.js @@ -0,0 +1,6 @@ +const l0 = 5 +const l = 10 + +export { l0 as l1, l } + +export default () => {} diff --git a/tests/files/no-unused-modules/file-m.js b/tests/files/no-unused-modules/file-m.js new file mode 100644 index 0000000000..f25fb35f47 --- /dev/null +++ b/tests/files/no-unused-modules/file-m.js @@ -0,0 +1,6 @@ +const m0 = 5 +const m = 10 + +export { m0 as m1, m } + +export default () => {} diff --git a/tests/files/no-unused-modules/file-n.js b/tests/files/no-unused-modules/file-n.js new file mode 100644 index 0000000000..7ac2e63744 --- /dev/null +++ b/tests/files/no-unused-modules/file-n.js @@ -0,0 +1,6 @@ +const n0 = 'n0' +const n1 = 42 + +export { n0, n1 } + +export default () => {} diff --git a/tests/files/no-unused-modules/file-o.js b/tests/files/no-unused-modules/file-o.js new file mode 100644 index 0000000000..002bd8cb66 --- /dev/null +++ b/tests/files/no-unused-modules/file-o.js @@ -0,0 +1,6 @@ +const o0 = 0 +const o1 = 1 + +export { o0, o1 as o2 } + +export default () => {} diff --git a/tests/files/no-unused-modules/node_modules.js b/tests/files/no-unused-modules/node_modules.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/src/rules/no-unused-modules.js b/tests/src/rules/no-unused-modules.js new file mode 100644 index 0000000000..214384130f --- /dev/null +++ b/tests/src/rules/no-unused-modules.js @@ -0,0 +1,550 @@ +import { test, testFilePath } from '../utils' + +import { RuleTester } from 'eslint' +import { expect } from 'chai' +import fs from 'fs' + +const ruleTester = new RuleTester() + , rule = require('rules/no-unused-modules') + +const error = message => ({ ruleId: 'no-unused-modules', message }) + +const missingExportsOptions = [{ + missingExports: true, +}] + +const unusedExportsOptions = [{ + unusedExports: true, + src: [testFilePath('./no-unused-modules/**/*.js')], + ignoreExports: [testFilePath('./no-unused-modules/*ignored*.js')], +}] + +// tests for missing exports +ruleTester.run('no-unused-modules', rule, { + valid: [ + test({ options: missingExportsOptions, + code: 'export default () => 1'}), + test({ options: missingExportsOptions, + code: 'export const a = 1'}), + test({ options: missingExportsOptions, + code: 'const a = 1; export { a }'}), + test({ options: missingExportsOptions, + code: 'function a() { return true }; export { a }'}), + test({ options: missingExportsOptions, + code: 'const a = 1; const b = 2; export { a, b }'}), + test({ options: missingExportsOptions, + code: 'const a = 1; export default a'}), + ], + invalid: [ + test({ + options: missingExportsOptions, + code: 'const a = 1', + errors: [error(`No exports found`)], + }), + test({ + options: missingExportsOptions, + code: '/* const a = 1 */', + errors: [error(`No exports found`)], + }), + ], +}) + + +// tests for exports +ruleTester.run('no-unused-modules', rule, { + valid: [ + + test({ options: unusedExportsOptions, + code: 'import { o2 } from "./file-o";export default () => 12', + filename: testFilePath('./no-unused-modules/file-a.js')}), + test({ options: unusedExportsOptions, + code: 'export const b = 2', + filename: testFilePath('./no-unused-modules/file-b.js')}), + test({ options: unusedExportsOptions, + code: 'const c1 = 3; function c2() { return 3 }; export { c1, c2 }', + filename: testFilePath('./no-unused-modules/file-c.js')}), + test({ options: unusedExportsOptions, + code: 'export function d() { return 4 }', + filename: testFilePath('./no-unused-modules/file-d.js')}), + test({ options: unusedExportsOptions, + code: 'const e0 = 5; export { e0 as e }', + filename: testFilePath('./no-unused-modules/file-e.js')}), + test({ options: unusedExportsOptions, + code: 'const l0 = 5; const l = 10; export { l0 as l1, l }; export default () => {}', + filename: testFilePath('./no-unused-modules/file-l.js')}), + test({ options: unusedExportsOptions, + code: 'const o0 = 0; const o1 = 1; export { o0, o1 as o2 }; export default () => {}', + filename: testFilePath('./no-unused-modules/file-o.js')}), + ], + invalid: [ + test({ options: unusedExportsOptions, + code: `import eslint from 'eslint' + import fileA from './file-a' + import { b } from './file-b' + import { c1, c2 } from './file-c' + import { d } from './file-d' + import { e } from './file-e' + import { e2 } from './file-e' + import { h2 } from './file-h' + import * as l from './file-l' + export * from './file-n' + export { default, o0, o3 } from './file-o' + export { p } from './file-p'`, + filename: testFilePath('./no-unused-modules/file-0.js'), + errors: [ + error(`exported declaration 'default' not used within other modules`), + error(`exported declaration 'o0' not used within other modules`), + error(`exported declaration 'o3' not used within other modules`), + error(`exported declaration 'p' not used within other modules`), + ]}), + test({ options: unusedExportsOptions, + code: `const n0 = 'n0'; const n1 = 42; export { n0, n1 }; export default () => {}`, + filename: testFilePath('./no-unused-modules/file-n.js'), + errors: [error(`exported declaration 'default' not used within other modules`)]}), + ], +}) + +// test for unused exports +ruleTester.run('no-unused-modules', rule, { + valid: [], + invalid: [ + test({ options: unusedExportsOptions, + code: 'export default () => 13', + filename: testFilePath('./no-unused-modules/file-f.js'), + errors: [error(`exported declaration 'default' not used within other modules`)]}), + test({ options: unusedExportsOptions, + code: 'export const g = 2', + filename: testFilePath('./no-unused-modules/file-g.js'), + errors: [error(`exported declaration 'g' not used within other modules`)]}), + test({ options: unusedExportsOptions, + code: 'const h1 = 3; function h2() { return 3 }; export { h1, h2 }', + filename: testFilePath('./no-unused-modules/file-h.js'), + errors: [error(`exported declaration 'h1' not used within other modules`)]}), + test({ options: unusedExportsOptions, + code: 'const i1 = 3; function i2() { return 3 }; export { i1, i2 }', + filename: testFilePath('./no-unused-modules/file-i.js'), + errors: [ + error(`exported declaration 'i1' not used within other modules`), + error(`exported declaration 'i2' not used within other modules`), + ]}), + test({ options: unusedExportsOptions, + code: 'export function j() { return 4 }', + filename: testFilePath('./no-unused-modules/file-j.js'), + errors: [error(`exported declaration 'j' not used within other modules`)]}), + test({ options: unusedExportsOptions, + code: 'const k0 = 5; export { k0 as k }', + filename: testFilePath('./no-unused-modules/file-k.js'), + errors: [error(`exported declaration 'k' not used within other modules`)]}), + ], +}) + +// // test for export from +ruleTester.run('no-unused-modules', rule, { + valid: [], + invalid: [ + test({ options: unusedExportsOptions, + code: `export { k } from '${testFilePath('./no-unused-modules/file-k.js')}'`, + filename: testFilePath('./no-unused-modules/file-j.js'), + errors: [error(`exported declaration 'k' not used within other modules`)]}), + ], +}) + +ruleTester.run('no-unused-modules', rule, { + valid: [ + test({ options: unusedExportsOptions, + code: 'const k0 = 5; export { k0 as k }', + filename: testFilePath('./no-unused-modules/file-k.js')}), + ], + invalid: [], +}) + +// test for ignored files +ruleTester.run('no-unused-modules', rule, { + valid: [ + test({ options: unusedExportsOptions, + code: 'export default () => 14', + filename: testFilePath('./no-unused-modules/file-ignored-a.js')}), + test({ options: unusedExportsOptions, + code: 'export const b = 2', + filename: testFilePath('./no-unused-modules/file-ignored-b.js')}), + test({ options: unusedExportsOptions, + code: 'const c1 = 3; function c2() { return 3 }; export { c1, c2 }', + filename: testFilePath('./no-unused-modules/file-ignored-c.js')}), + test({ options: unusedExportsOptions, + code: 'export function d() { return 4 }', + filename: testFilePath('./no-unused-modules/file-ignored-d.js')}), + test({ options: unusedExportsOptions, + code: 'const f = 5; export { f as e }', + filename: testFilePath('./no-unused-modules/file-ignored-e.js')}), + test({ options: unusedExportsOptions, + code: 'const l0 = 5; const l = 10; export { l0 as l1, l }; export default () => {}', + filename: testFilePath('./no-unused-modules/file-ignored-l.js')}), + ], + invalid: [], +}) + +// add named import for file with default export +ruleTester.run('no-unused-modules', rule, { + valid: [ + test({ options: unusedExportsOptions, + code: `import { f } from '${testFilePath('./no-unused-modules/file-f.js')}'`, + filename: testFilePath('./no-unused-modules/file-0.js')}), + ], + invalid: [ + test({ options: unusedExportsOptions, + code: 'export default () => 15', + filename: testFilePath('./no-unused-modules/file-f.js'), + errors: [error(`exported declaration 'default' not used within other modules`)]}), + ], +}) + +// add default import for file with default export +ruleTester.run('no-unused-modules', rule, { + valid: [ + test({ options: unusedExportsOptions, + code: `import f from '${testFilePath('./no-unused-modules/file-f.js')}'`, + filename: testFilePath('./no-unused-modules/file-0.js')}), + test({ options: unusedExportsOptions, + code: 'export default () => 16', + filename: testFilePath('./no-unused-modules/file-f.js')}), + ], + invalid: [], +}) + +// add default import for file with named export +ruleTester.run('no-unused-modules', rule, { + valid: [ + test({ options: unusedExportsOptions, + code: `import g from '${testFilePath('./no-unused-modules/file-g.js')}';import {h} from '${testFilePath('./no-unused-modules/file-gg.js')}'`, + filename: testFilePath('./no-unused-modules/file-0.js')}), + ], + invalid: [ + test({ options: unusedExportsOptions, + code: 'export const g = 2', + filename: testFilePath('./no-unused-modules/file-g.js'), + errors: [error(`exported declaration 'g' not used within other modules`)]})], +}) + +// add named import for file with named export +ruleTester.run('no-unused-modules', rule, { + valid: [ + test({ options: unusedExportsOptions, + code: `import { g } from '${testFilePath('./no-unused-modules/file-g.js')}'; import eslint from 'eslint'`, + filename: testFilePath('./no-unused-modules/file-0.js')}), + test({ options: unusedExportsOptions, + code: 'export const g = 2', + filename: testFilePath('./no-unused-modules/file-g.js')}), + ], + invalid: [], +}) + +// add different named import for file with named export +ruleTester.run('no-unused-modules', rule, { + valid: [ + test({ options: unusedExportsOptions, + code: `import { c } from '${testFilePath('./no-unused-modules/file-b.js')}'`, + filename: testFilePath('./no-unused-modules/file-0.js')}), + ], + invalid: [ + test({ options: unusedExportsOptions, + code: 'export const b = 2', + filename: testFilePath('./no-unused-modules/file-b.js'), + errors: [error(`exported declaration 'b' not used within other modules`)]}), + ], +}) + +// remove default import for file with default export +ruleTester.run('no-unused-modules', rule, { + valid: [ + test({ options: unusedExportsOptions, + code: `import { a1, a2 } from '${testFilePath('./no-unused-modules/file-a.js')}'`, + filename: testFilePath('./no-unused-modules/file-0.js')}), + ], + invalid: [ + test({ options: unusedExportsOptions, + code: 'export default () => 17', + filename: testFilePath('./no-unused-modules/file-a.js'), + errors: [error(`exported declaration 'default' not used within other modules`)]}), + ], +}) + +// add namespace import for file with unused exports +ruleTester.run('no-unused-modules', rule, { + valid: [], + invalid: [ + test({ options: unusedExportsOptions, + code: 'const m0 = 5; const m = 10; export { m0 as m1, m }; export default () => {}', + filename: testFilePath('./no-unused-modules/file-m.js'), + errors: [ + error(`exported declaration 'm1' not used within other modules`), + error(`exported declaration 'm' not used within other modules`), + error(`exported declaration 'default' not used within other modules`), + ]}), + ], +}) +ruleTester.run('no-unused-modules', rule, { + valid: [ + test({ options: unusedExportsOptions, + code: `import * as m from '${testFilePath('./no-unused-modules/file-m.js')}'; import unknown from 'unknown-module'`, + filename: testFilePath('./no-unused-modules/file-0.js')}), + test({ options: unusedExportsOptions, + code: 'const m0 = 5; const m = 10; export { m0 as m1, m }; export default () => {}', + filename: testFilePath('./no-unused-modules/file-m.js')}), + ], + invalid: [], +}) + +// remove all exports +ruleTester.run('no-unused-modules', rule, { + valid: [ + test({ options: unusedExportsOptions, + code: `/* import * as m from '${testFilePath('./no-unused-modules/file-m.js')}' */`, + filename: testFilePath('./no-unused-modules/file-0.js')}), + ], + invalid: [ + test({ options: unusedExportsOptions, + code: 'const m0 = 5; const m = 10; export { m0 as m1, m }; export default () => {}', + filename: testFilePath('./no-unused-modules/file-m.js'), + errors: [ + error(`exported declaration 'm1' not used within other modules`), + error(`exported declaration 'm' not used within other modules`), + error(`exported declaration 'default' not used within other modules`), + ]}), + ], +}) + +ruleTester.run('no-unused-modules', rule, { + valid: [ + test({ options: unusedExportsOptions, + code: `export * from '${testFilePath('./no-unused-modules/file-m.js')}';`, + filename: testFilePath('./no-unused-modules/file-0.js')}), + ], + invalid: [], +}) +ruleTester.run('no-unused-modules', rule, { + valid: [], + invalid: [ + test({ options: unusedExportsOptions, + code: 'const m0 = 5; const m = 10; export { m0 as m1, m }; export default () => {}', + filename: testFilePath('./no-unused-modules/file-m.js'), + errors: [error(`exported declaration 'default' not used within other modules`)]}), + ], +}) + +ruleTester.run('no-unused-modules', rule, { + valid: [], + invalid: [ + test({ options: unusedExportsOptions, + code: `export { m1, m} from '${testFilePath('./no-unused-modules/file-m.js')}';`, + filename: testFilePath('./no-unused-modules/file-0.js'), + errors: [ + error(`exported declaration 'm1' not used within other modules`), + error(`exported declaration 'm' not used within other modules`), + ]}), + test({ options: unusedExportsOptions, + code: 'const m0 = 5; const m = 10; export { m0 as m1, m }; export default () => {}', + filename: testFilePath('./no-unused-modules/file-m.js'), + errors: [error(`exported declaration 'default' not used within other modules`)]}), + ], +}) + +ruleTester.run('no-unused-modules', rule, { + valid: [ + // test({ options: unusedExportsOptions, + // code: `export { default, m1 } from '${testFilePath('./no-unused-modules/file-m.js')}';`, + // filename: testFilePath('./no-unused-modules/file-0.js')}), + ], + invalid: [ + test({ options: unusedExportsOptions, + code: `export { default, m1 } from '${testFilePath('./no-unused-modules/file-m.js')}';`, + filename: testFilePath('./no-unused-modules/file-0.js'), + errors: [ + error(`exported declaration 'default' not used within other modules`), + error(`exported declaration 'm1' not used within other modules`), + ]}), + test({ options: unusedExportsOptions, + code: 'const m0 = 5; const m = 10; export { m0 as m1, m }; export default () => {}', + filename: testFilePath('./no-unused-modules/file-m.js'), + errors: [error(`exported declaration 'm' not used within other modules`)]}), + ], +}) + +describe('test behaviour for new file', () => { + before(() => { + fs.writeFileSync(testFilePath('./no-unused-modules/file-added-0.js'), '', {encoding: 'utf8'}) + }) + + // add import in newly created file + ruleTester.run('no-unused-modules', rule, { + valid: [ + test({ options: unusedExportsOptions, + code: `import * as m from '${testFilePath('./no-unused-modules/file-m.js')}'`, + filename: testFilePath('./no-unused-modules/file-added-0.js')}), + test({ options: unusedExportsOptions, + code: 'const m0 = 5; const m = 10; export { m0 as m1, m }; export default () => {}', + filename: testFilePath('./no-unused-modules/file-m.js')}), + ], + invalid: [], + }) + + // add export for newly created file + ruleTester.run('no-unused-modules', rule, { + valid: [], + invalid: [ + test({ options: unusedExportsOptions, + code: `export default () => {2}`, + filename: testFilePath('./no-unused-modules/file-added-0.js'), + errors: [error(`exported declaration 'default' not used within other modules`)]}), + ], + }) + + ruleTester.run('no-unused-modules', rule, { + valid: [ + test({ options: unusedExportsOptions, + code: `import def from '${testFilePath('./no-unused-modules/file-added-0.js')}'`, + filename: testFilePath('./no-unused-modules/file-0.js')}), + test({ options: unusedExportsOptions, + code: `export default () => {}`, + filename: testFilePath('./no-unused-modules/file-added-0.js')}), + ], + invalid: [], + }) + + // export * only considers named imports. default imports still need to be reported + ruleTester.run('no-unused-modules', rule, { + valid: [ + test({ options: unusedExportsOptions, + code: `export * from '${testFilePath('./no-unused-modules/file-added-0.js')}'`, + filename: testFilePath('./no-unused-modules/file-0.js')}), + ], + invalid: [ + test({ options: unusedExportsOptions, + code: `export const z = 'z';export default () => {}`, + filename: testFilePath('./no-unused-modules/file-added-0.js'), + errors: [error(`exported declaration 'default' not used within other modules`)]}), + ], + }) + ruleTester.run('no-unused-modules', rule, { + valid: [ + test({ options: unusedExportsOptions, + code: `export const a = 2`, + filename: testFilePath('./no-unused-modules/file-added-0.js')}), + ], + invalid: [], + }) + + // remove export *. all exports need to be reported + ruleTester.run('no-unused-modules', rule, { + valid: [], + invalid: [ + test({ options: unusedExportsOptions, + code: `export { a } from '${testFilePath('./no-unused-modules/file-added-0.js')}'`, + filename: testFilePath('./no-unused-modules/file-0.js'), + errors: [error(`exported declaration 'a' not used within other modules`)]}), + test({ options: unusedExportsOptions, + code: `export const z = 'z';export default () => {}`, + filename: testFilePath('./no-unused-modules/file-added-0.js'), + errors: [ + error(`exported declaration 'z' not used within other modules`), + error(`exported declaration 'default' not used within other modules`), + ]}), + ], + }) + + + describe('test behaviour for new file', () => { + before(() => { + fs.writeFileSync(testFilePath('./no-unused-modules/file-added-1.js'), '', {encoding: 'utf8'}) + }) + ruleTester.run('no-unused-modules', rule, { + valid: [ + test({ options: unusedExportsOptions, + code: `export * from '${testFilePath('./no-unused-modules/file-added-1.js')}'`, + filename: testFilePath('./no-unused-modules/file-0.js')}), + ], + invalid: [ + test({ options: unusedExportsOptions, + code: `export const z = 'z';export default () => {}`, + filename: testFilePath('./no-unused-modules/file-added-1.js'), + errors: [error(`exported declaration 'default' not used within other modules`)]}), + ], + }) + after(() => { + if (fs.existsSync(testFilePath('./no-unused-modules/file-added-1.js'))) { + fs.unlinkSync(testFilePath('./no-unused-modules/file-added-1.js')) + } + }) + }) + + after(() => { + if (fs.existsSync(testFilePath('./no-unused-modules/file-added-0.js'))) { + fs.unlinkSync(testFilePath('./no-unused-modules/file-added-0.js')) + } + }) +}) + +describe('test behaviour for new file', () => { + before(() => { + fs.writeFileSync(testFilePath('./no-unused-modules/file-added-2.js'), '', {encoding: 'utf8'}) + }) + ruleTester.run('no-unused-modules', rule, { + valid: [ + test({ options: unusedExportsOptions, + code: `import added from '${testFilePath('./no-unused-modules/file-added-2.js')}'`, + filename: testFilePath('./no-unused-modules/file-added-1.js')}), + test({ options: unusedExportsOptions, + code: `export default () => {}`, + filename: testFilePath('./no-unused-modules/file-added-2.js')}), + ], + invalid: [], + }) + after(() => { + if (fs.existsSync(testFilePath('./no-unused-modules/file-added-2.js'))) { + fs.unlinkSync(testFilePath('./no-unused-modules/file-added-2.js')) + } + }) +}) + +describe('test behaviour for new file', () => { + before(() => { + fs.writeFileSync(testFilePath('./no-unused-modules/file-added-3.js'), '', {encoding: 'utf8'}) + }) + ruleTester.run('no-unused-modules', rule, { + valid: [ + test({ options: unusedExportsOptions, + code: `import { added } from '${testFilePath('./no-unused-modules/file-added-3.js')}'`, + filename: testFilePath('./no-unused-modules/file-added-1.js')}), + test({ options: unusedExportsOptions, + code: `export const added = () => {}`, + filename: testFilePath('./no-unused-modules/file-added-3.js')}), + ], + invalid: [], + }) + after(() => { + if (fs.existsSync(testFilePath('./no-unused-modules/file-added-3.js'))) { + fs.unlinkSync(testFilePath('./no-unused-modules/file-added-3.js')) + } + }) +}) + +describe('test behaviour for new file', () => { + before(() => { + fs.writeFileSync(testFilePath('./no-unused-modules/file-added-4.js.js'), '', {encoding: 'utf8'}) + }) + ruleTester.run('no-unused-modules', rule, { + valid: [ + test({ options: unusedExportsOptions, + code: `import * as added from '${testFilePath('./no-unused-modules/file-added-4.js.js')}'`, + filename: testFilePath('./no-unused-modules/file-added-1.js')}), + test({ options: unusedExportsOptions, + code: `export const added = () => {}; export default () => {}`, + filename: testFilePath('./no-unused-modules/file-added-4.js.js')}), + ], + invalid: [], + }) + after(() => { + if (fs.existsSync(testFilePath('./no-unused-modules/file-added-4.js.js'))) { + fs.unlinkSync(testFilePath('./no-unused-modules/file-added-4.js.js')) + } + }) +})