eslint-plugin-import-x
Version:
Import with sanity.
731 lines • 30.8 kB
JavaScript
import path from 'node:path';
import { TSESTree } from '@typescript-eslint/utils';
import eslintUnsupportedApi from 'eslint/use-at-your-own-risk';
import { ExportMap, recursivePatternCapture, createRule, resolve, getFileExtensions, readPkgUp, visit, getValue, } from '../utils/index.js';
const { FileEnumerator } = eslintUnsupportedApi;
function listFilesToProcess(src, extensions) {
const enumerator = new FileEnumerator({
extensions,
});
return Array.from(enumerator.iterateFiles(src), ({ filePath, ignored }) => ({
ignored,
filename: filePath,
}));
}
const DEFAULT = 'default';
const { AST_NODE_TYPES } = TSESTree;
function forEachDeclarationIdentifier(declaration, cb) {
if (declaration) {
const isTypeDeclaration = declaration.type === AST_NODE_TYPES.TSInterfaceDeclaration ||
declaration.type === AST_NODE_TYPES.TSTypeAliasDeclaration ||
declaration.type === AST_NODE_TYPES.TSEnumDeclaration;
if (declaration.type === AST_NODE_TYPES.FunctionDeclaration ||
declaration.type === AST_NODE_TYPES.ClassDeclaration ||
isTypeDeclaration) {
cb(declaration.id.name, isTypeDeclaration);
}
else if (declaration.type === AST_NODE_TYPES.VariableDeclaration) {
for (const { id } of declaration.declarations) {
if (id.type === AST_NODE_TYPES.ObjectPattern) {
recursivePatternCapture(id, pattern => {
if (pattern.type === AST_NODE_TYPES.Identifier) {
cb(pattern.name, false);
}
});
}
else if (id.type === AST_NODE_TYPES.ArrayPattern) {
for (const el of id.elements) {
if (el?.type === AST_NODE_TYPES.Identifier) {
cb(el.name, false);
}
}
}
else {
cb(id.name, false);
}
}
}
}
}
const importList = new Map();
const exportList = new Map();
const visitorKeyMap = new Map();
const ignoredFiles = new Set();
const filesOutsideSrc = new Set();
const isNodeModule = (path) => /([/\\])(node_modules)\1/.test(path);
const resolveFiles = (src, ignoreExports, context) => {
const extensions = [...getFileExtensions(context.settings)];
const srcFileList = listFilesToProcess(src, extensions);
const ignoredFilesList = listFilesToProcess(ignoreExports, extensions);
for (const { filename } of ignoredFilesList)
ignoredFiles.add(filename);
return new Set(srcFileList.flatMap(({ filename }) => isNodeModule(filename) ? [] : filename));
};
const prepareImportsAndExports = (srcFiles, context) => {
const exportAll = new Map();
for (const file of srcFiles) {
const exports = new Map();
const imports = new Map();
const currentExports = ExportMap.get(file, context);
if (currentExports) {
const { dependencies, reexports, imports: localImportList, namespace, visitorKeys, } = currentExports;
visitorKeyMap.set(file, visitorKeys);
const currentExportAll = new Set();
for (const getDependency of dependencies) {
const dependency = getDependency();
if (dependency === null) {
continue;
}
currentExportAll.add(dependency.path);
}
exportAll.set(file, currentExportAll);
for (const [key, value] of reexports.entries()) {
if (key === DEFAULT) {
exports.set(AST_NODE_TYPES.ImportDefaultSpecifier, {
whereUsed: new Set(),
});
}
else {
exports.set(key, { whereUsed: new Set() });
}
const reexport = value.getImport();
if (!reexport) {
continue;
}
let localImport = imports.get(reexport.path);
const currentValue = value.local === DEFAULT
? AST_NODE_TYPES.ImportDefaultSpecifier
: value.local;
localImport =
localImport === undefined
? new Set([currentValue])
: new Set([...localImport, currentValue]);
imports.set(reexport.path, localImport);
}
for (const [key, value] of localImportList.entries()) {
if (isNodeModule(key)) {
continue;
}
const localImport = imports.get(key) || new Set();
for (const { importedSpecifiers } of value.declarations) {
for (const specifier of importedSpecifiers) {
localImport.add(specifier);
}
}
imports.set(key, localImport);
}
importList.set(file, imports);
if (ignoredFiles.has(file)) {
continue;
}
for (const [key, _value] of namespace.entries()) {
if (key === DEFAULT) {
exports.set(AST_NODE_TYPES.ImportDefaultSpecifier, {
whereUsed: new Set(),
});
}
else {
exports.set(key, { whereUsed: new Set() });
}
}
}
exports.set(AST_NODE_TYPES.ExportAllDeclaration, {
whereUsed: new Set(),
});
exports.set(AST_NODE_TYPES.ImportNamespaceSpecifier, {
whereUsed: new Set(),
});
exportList.set(file, exports);
}
for (const [key, value] of exportAll.entries()) {
for (const val of value) {
const currentExports = exportList.get(val);
if (currentExports) {
const currentExport = currentExports.get(AST_NODE_TYPES.ExportAllDeclaration);
currentExport.whereUsed.add(key);
}
}
}
};
const determineUsage = () => {
for (const [listKey, listValue] of importList.entries()) {
for (const [key, value] of listValue.entries()) {
const exports = exportList.get(key);
if (exports !== undefined) {
for (const currentImport of value) {
let specifier;
if (currentImport === AST_NODE_TYPES.ImportNamespaceSpecifier) {
specifier = AST_NODE_TYPES.ImportNamespaceSpecifier;
}
else if (currentImport === AST_NODE_TYPES.ImportDefaultSpecifier) {
specifier = AST_NODE_TYPES.ImportDefaultSpecifier;
}
else {
specifier = currentImport;
}
if (specifier !== undefined) {
const exportStatement = exports.get(specifier);
if (exportStatement !== undefined) {
const { whereUsed } = exportStatement;
whereUsed.add(listKey);
exports.set(specifier, { whereUsed });
}
}
}
}
}
}
};
let srcFiles;
let lastPrepareKey;
const doPreparation = (src, ignoreExports, context) => {
const prepareKey = JSON.stringify({
src: src.sort(),
ignoreExports: (ignoreExports || []).sort(),
extensions: [...getFileExtensions(context.settings)].sort(),
});
if (prepareKey === lastPrepareKey) {
return;
}
importList.clear();
exportList.clear();
ignoredFiles.clear();
filesOutsideSrc.clear();
srcFiles = resolveFiles(src, ignoreExports, context);
prepareImportsAndExports(srcFiles, context);
determineUsage();
lastPrepareKey = prepareKey;
};
const newNamespaceImportExists = (specifiers) => specifiers.some(({ type }) => type === AST_NODE_TYPES.ImportNamespaceSpecifier);
const newDefaultImportExists = (specifiers) => specifiers.some(({ type }) => type === AST_NODE_TYPES.ImportDefaultSpecifier);
const fileIsInPkg = (file) => {
const { pkg, path: pkgPath } = readPkgUp({ cwd: file });
const basePath = path.dirname(pkgPath);
const checkPkgFieldString = (pkgField) => {
if (path.join(basePath, pkgField) === file) {
return true;
}
};
const checkPkgFieldObject = (pkgField) => {
const pkgFieldFiles = Object.values(pkgField).flatMap(value => typeof value === 'boolean' ? [] : path.join(basePath, value));
if (pkgFieldFiles.includes(file)) {
return true;
}
};
const checkPkgField = (pkgField) => {
if (typeof pkgField === 'string') {
return checkPkgFieldString(pkgField);
}
if (typeof pkgField === 'object') {
return checkPkgFieldObject(pkgField);
}
};
if (!pkg) {
return false;
}
if (pkg.private === true) {
return false;
}
if (pkg.bin && checkPkgField(pkg.bin)) {
return true;
}
if (pkg.browser && checkPkgField(pkg.browser)) {
return true;
}
if (pkg.main && checkPkgFieldString(pkg.main)) {
return true;
}
return false;
};
export default createRule({
name: 'no-unused-modules',
meta: {
type: 'suggestion',
docs: {
category: 'Helpful warnings',
description: 'Forbid modules without exports, or exports without matching import in another module.',
},
schema: [
{
type: 'object',
properties: {
src: {
description: 'files/paths to be analyzed (only for unused exports)',
type: 'array',
uniqueItems: true,
items: {
type: 'string',
minLength: 1,
},
},
ignoreExports: {
description: 'files/paths for which unused exports will not be reported (e.g module entry points)',
type: 'array',
uniqueItems: true,
items: {
type: 'string',
minLength: 1,
},
},
missingExports: {
description: 'report modules without any exports',
type: 'boolean',
},
unusedExports: {
description: 'report exports without any usage',
type: 'boolean',
},
ignoreUnusedTypeExports: {
description: 'ignore type exports without any usage',
type: 'boolean',
},
},
anyOf: [
{
type: 'object',
properties: {
unusedExports: {
type: 'boolean',
enum: [true],
},
src: {
type: 'array',
minItems: 1,
},
},
required: ['unusedExports'],
},
{
type: 'object',
properties: {
missingExports: {
type: 'boolean',
enum: [true],
},
},
required: ['missingExports'],
},
],
},
],
messages: {
notFound: 'No exports found',
unused: "exported declaration '{{value}}' not used within other modules",
},
},
defaultOptions: [],
create(context) {
const { src = [process.cwd()], ignoreExports = [], missingExports, unusedExports, ignoreUnusedTypeExports, } = context.options[0] || {};
if (unusedExports) {
doPreparation(src, ignoreExports, context);
}
const filename = context.physicalFilename;
const checkExportPresence = (node) => {
if (!missingExports) {
return;
}
if (ignoreUnusedTypeExports) {
return;
}
if (ignoredFiles.has(filename)) {
return;
}
const exportCount = exportList.get(filename);
const exportAll = exportCount.get(AST_NODE_TYPES.ExportAllDeclaration);
const namespaceImports = exportCount.get(AST_NODE_TYPES.ImportNamespaceSpecifier);
exportCount.delete(AST_NODE_TYPES.ExportAllDeclaration);
exportCount.delete(AST_NODE_TYPES.ImportNamespaceSpecifier);
if (exportCount.size === 0) {
context.report({
node: node.body[0] ?? node,
messageId: 'notFound',
});
}
exportCount.set(AST_NODE_TYPES.ExportAllDeclaration, exportAll);
exportCount.set(AST_NODE_TYPES.ImportNamespaceSpecifier, namespaceImports);
};
const checkUsage = (node, exportedValue, isTypeExport) => {
if (!unusedExports) {
return;
}
if (isTypeExport && ignoreUnusedTypeExports) {
return;
}
if (ignoredFiles.has(filename)) {
return;
}
if (fileIsInPkg(filename)) {
return;
}
if (filesOutsideSrc.has(filename)) {
return;
}
if (!srcFiles.has(filename)) {
srcFiles = resolveFiles(src, ignoreExports, context);
if (!srcFiles.has(filename)) {
filesOutsideSrc.add(filename);
return;
}
}
const exports = exportList.get(filename);
if (!exports) {
console.error(`file \`${filename}\` has no exports. Please update to the latest, and if it still happens, report this on https://github.com/import-js/eslint-plugin-import/issues/2866!`);
return;
}
const exportAll = exports.get(AST_NODE_TYPES.ExportAllDeclaration);
if (exportAll !== undefined &&
exportedValue !== AST_NODE_TYPES.ImportDefaultSpecifier &&
exportAll.whereUsed.size > 0) {
return;
}
const namespaceImports = exports.get(AST_NODE_TYPES.ImportNamespaceSpecifier);
if (namespaceImports !== undefined &&
namespaceImports.whereUsed.size > 0) {
return;
}
const exportsKey = exportedValue === DEFAULT
? AST_NODE_TYPES.ImportDefaultSpecifier
: exportedValue;
const exportStatement = exports.get(exportsKey);
const value = exportsKey === AST_NODE_TYPES.ImportDefaultSpecifier
? DEFAULT
: exportsKey;
if (exportStatement === undefined) {
context.report({
node,
messageId: 'unused',
data: {
value,
},
});
}
else {
if (exportStatement.whereUsed.size === 0) {
context.report({
node,
messageId: 'unused',
data: {
value,
},
});
}
}
};
const updateExportUsage = (node) => {
if (ignoredFiles.has(filename)) {
return;
}
const exports = exportList.get(filename) ??
new Map();
const newExports = new Map();
const newExportIdentifiers = new Set();
for (const s of node.body) {
if (s.type === AST_NODE_TYPES.ExportDefaultDeclaration) {
newExportIdentifiers.add(AST_NODE_TYPES.ImportDefaultSpecifier);
}
if (s.type === AST_NODE_TYPES.ExportNamedDeclaration) {
if (s.specifiers.length > 0) {
for (const specifier of s.specifiers) {
if (specifier.exported) {
newExportIdentifiers.add(getValue(specifier.exported));
}
}
}
forEachDeclarationIdentifier(s.declaration, name => {
newExportIdentifiers.add(name);
});
}
}
for (const [key, value] of exports.entries()) {
if (newExportIdentifiers.has(key)) {
newExports.set(key, value);
}
}
for (const key of newExportIdentifiers) {
if (!exports.has(key)) {
newExports.set(key, { whereUsed: new Set() });
}
}
const exportAll = exports.get(AST_NODE_TYPES.ExportAllDeclaration);
const namespaceImports = exports.get(AST_NODE_TYPES.ImportNamespaceSpecifier) ?? { whereUsed: new Set() };
newExports.set(AST_NODE_TYPES.ExportAllDeclaration, exportAll);
newExports.set(AST_NODE_TYPES.ImportNamespaceSpecifier, namespaceImports);
exportList.set(filename, newExports);
};
const updateImportUsage = (node) => {
if (!unusedExports) {
return;
}
const oldImportPaths = importList.get(filename) ?? 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();
for (const [key, value] of oldImportPaths.entries()) {
if (value.has(AST_NODE_TYPES.ExportAllDeclaration)) {
oldExportAll.add(key);
}
if (value.has(AST_NODE_TYPES.ImportNamespaceSpecifier)) {
oldNamespaceImports.add(key);
}
if (value.has(AST_NODE_TYPES.ImportDefaultSpecifier)) {
oldDefaultImports.add(key);
}
for (const val of value) {
if (val !== AST_NODE_TYPES.ImportNamespaceSpecifier &&
val !== AST_NODE_TYPES.ImportDefaultSpecifier) {
oldImports.set(val, key);
}
}
}
function processDynamicImport(source) {
if (source.type !== 'Literal' || typeof source.value !== 'string') {
return null;
}
const p = resolve(source.value, context);
if (p == null) {
return null;
}
newNamespaceImports.add(p);
}
visit(node, visitorKeyMap.get(filename), {
ImportExpression(child) {
processDynamicImport(child.source);
},
CallExpression(child_) {
const child = child_;
if (child.callee.type === 'Import') {
processDynamicImport(child.arguments[0]);
}
},
});
for (const astNode of node.body) {
let resolvedPath;
if (astNode.type === AST_NODE_TYPES.ExportNamedDeclaration &&
astNode.source) {
resolvedPath = resolve(astNode.source.raw.replaceAll(/('|")/g, ''), context);
for (const specifier of astNode.specifiers) {
const name = getValue(specifier.local);
if (name === DEFAULT) {
newDefaultImports.add(resolvedPath);
}
else {
newImports.set(name, resolvedPath);
}
}
}
if (astNode.type === AST_NODE_TYPES.ExportAllDeclaration) {
resolvedPath = resolve(astNode.source.raw.replaceAll(/('|")/g, ''), context);
newExportAll.add(resolvedPath);
}
if (astNode.type === AST_NODE_TYPES.ImportDeclaration) {
resolvedPath = resolve(astNode.source.raw.replaceAll(/('|")/g, ''), context);
if (!resolvedPath) {
continue;
}
if (isNodeModule(resolvedPath)) {
continue;
}
if (newNamespaceImportExists(astNode.specifiers)) {
newNamespaceImports.add(resolvedPath);
}
if (newDefaultImportExists(astNode.specifiers)) {
newDefaultImports.add(resolvedPath);
}
for (const specifier of astNode.specifiers.filter(specifier => specifier.type !== AST_NODE_TYPES.ImportDefaultSpecifier &&
specifier.type !== AST_NODE_TYPES.ImportNamespaceSpecifier)) {
if ('imported' in specifier) {
newImports.set(getValue(specifier.imported), resolvedPath);
}
}
}
}
for (const value of newExportAll) {
if (!oldExportAll.has(value)) {
const imports = oldImportPaths.get(value) ?? new Set();
imports.add(AST_NODE_TYPES.ExportAllDeclaration);
oldImportPaths.set(value, imports);
let exports = exportList.get(value);
let currentExport;
if (exports === undefined) {
exports = new Map();
exportList.set(value, exports);
}
else {
currentExport = exports.get(AST_NODE_TYPES.ExportAllDeclaration);
}
if (currentExport === undefined) {
const whereUsed = new Set();
whereUsed.add(filename);
exports.set(AST_NODE_TYPES.ExportAllDeclaration, {
whereUsed,
});
}
else {
currentExport.whereUsed.add(filename);
}
}
}
for (const value of oldExportAll) {
if (!newExportAll.has(value)) {
const imports = oldImportPaths.get(value);
imports.delete(AST_NODE_TYPES.ExportAllDeclaration);
const exports = exportList.get(value);
if (exports !== undefined) {
const currentExport = exports.get(AST_NODE_TYPES.ExportAllDeclaration);
if (currentExport !== undefined) {
currentExport.whereUsed.delete(filename);
}
}
}
}
for (const value of newDefaultImports) {
if (!oldDefaultImports.has(value)) {
let imports = oldImportPaths.get(value);
if (imports === undefined) {
imports = new Set();
}
imports.add(AST_NODE_TYPES.ImportDefaultSpecifier);
oldImportPaths.set(value, imports);
let exports = exportList.get(value);
let currentExport;
if (exports === undefined) {
exports = new Map();
exportList.set(value, exports);
}
else {
currentExport = exports.get(AST_NODE_TYPES.ImportDefaultSpecifier);
}
if (currentExport === undefined) {
const whereUsed = new Set();
whereUsed.add(filename);
exports.set(AST_NODE_TYPES.ImportDefaultSpecifier, {
whereUsed,
});
}
else {
currentExport.whereUsed.add(filename);
}
}
}
for (const value of oldDefaultImports) {
if (!newDefaultImports.has(value)) {
const imports = oldImportPaths.get(value);
imports.delete(AST_NODE_TYPES.ImportDefaultSpecifier);
const exports = exportList.get(value);
if (exports !== undefined) {
const currentExport = exports.get(AST_NODE_TYPES.ImportDefaultSpecifier);
if (currentExport !== undefined) {
currentExport.whereUsed.delete(filename);
}
}
}
}
for (const value of newNamespaceImports) {
if (!oldNamespaceImports.has(value)) {
let imports = oldImportPaths.get(value);
if (imports === undefined) {
imports = new Set();
}
imports.add(AST_NODE_TYPES.ImportNamespaceSpecifier);
oldImportPaths.set(value, imports);
let exports = exportList.get(value);
let currentExport;
if (exports === undefined) {
exports = new Map();
exportList.set(value, exports);
}
else {
currentExport = exports.get(AST_NODE_TYPES.ImportNamespaceSpecifier);
}
if (currentExport === undefined) {
const whereUsed = new Set();
whereUsed.add(filename);
exports.set(AST_NODE_TYPES.ImportNamespaceSpecifier, {
whereUsed,
});
}
else {
currentExport.whereUsed.add(filename);
}
}
}
for (const value of oldNamespaceImports) {
if (!newNamespaceImports.has(value)) {
const imports = oldImportPaths.get(value);
imports.delete(AST_NODE_TYPES.ImportNamespaceSpecifier);
const exports = exportList.get(value);
if (exports !== undefined) {
const currentExport = exports.get(AST_NODE_TYPES.ImportNamespaceSpecifier);
if (currentExport !== undefined) {
currentExport.whereUsed.delete(filename);
}
}
}
}
for (const [key, value] of newImports.entries()) {
if (!oldImports.has(key)) {
let imports = oldImportPaths.get(value);
if (imports === undefined) {
imports = new Set();
}
imports.add(key);
oldImportPaths.set(value, imports);
let exports = exportList.get(value);
let currentExport;
if (exports === undefined) {
exports = new Map();
exportList.set(value, exports);
}
else {
currentExport = exports.get(key);
}
if (currentExport === undefined) {
const whereUsed = new Set();
whereUsed.add(filename);
exports.set(key, { whereUsed });
}
else {
currentExport.whereUsed.add(filename);
}
}
}
for (const [key, value] of oldImports.entries()) {
if (!newImports.has(key)) {
const imports = oldImportPaths.get(value);
imports.delete(key);
const exports = exportList.get(value);
if (exports !== undefined) {
const currentExport = exports.get(key);
if (currentExport !== undefined) {
currentExport.whereUsed.delete(filename);
}
}
}
}
};
return {
'Program:exit'(node) {
updateExportUsage(node);
updateImportUsage(node);
checkExportPresence(node);
},
ExportDefaultDeclaration(node) {
checkUsage(node, AST_NODE_TYPES.ImportDefaultSpecifier, false);
},
ExportNamedDeclaration(node) {
for (const specifier of node.specifiers) {
checkUsage(specifier, getValue(specifier.exported), false);
}
forEachDeclarationIdentifier(node.declaration, (name, isTypeExport) => {
checkUsage(node, name, isTypeExport);
});
},
};
},
});
//# sourceMappingURL=no-unused-modules.js.map