eslint-plugin-import-x
Version:
Import with sanity.
207 lines • 9.66 kB
JavaScript
import path from 'node:path';
import isGlob from 'is-glob';
import { Minimatch } from 'minimatch';
import { importType, createRule, moduleVisitor, resolve, } from '../utils/index.js';
const containsPath = (filepath, target) => {
const relative = path.relative(target, filepath);
return relative === '' || !relative.startsWith('..');
};
function isMatchingTargetPath(filename, targetPath) {
if (isGlob(targetPath)) {
const mm = new Minimatch(targetPath, { windowsPathsNoEscape: true });
return mm.match(filename);
}
return containsPath(filename, targetPath);
}
function areBothGlobPatternAndAbsolutePath(areGlobPatterns) {
return (areGlobPatterns.some(Boolean) && areGlobPatterns.some(isGlob => !isGlob));
}
export default createRule({
name: 'no-restricted-paths',
meta: {
type: 'problem',
docs: {
category: 'Static analysis',
description: 'Enforce which files can be imported in a given folder.',
},
schema: [
{
type: 'object',
properties: {
zones: {
type: 'array',
minItems: 1,
items: {
type: 'object',
properties: {
target: {
anyOf: [
{ type: 'string' },
{
type: 'array',
items: { type: 'string' },
uniqueItems: true,
minItems: 1,
},
],
},
from: {
anyOf: [
{ type: 'string' },
{
type: 'array',
items: { type: 'string' },
uniqueItems: true,
minItems: 1,
},
],
},
except: {
type: 'array',
items: {
type: 'string',
},
uniqueItems: true,
},
message: { type: 'string' },
},
additionalProperties: false,
},
},
basePath: { type: 'string' },
},
additionalProperties: false,
},
],
messages: {
path: 'Restricted path exceptions must be descendants of the configured `from` path for that zone.',
mixedGlob: 'Restricted path `from` must contain either only glob patterns or none',
glob: 'Restricted path exceptions must be glob patterns when `from` contains glob patterns',
zone: 'Unexpected path "{{importPath}}" imported in restricted zone.{{extra}}',
},
},
defaultOptions: [],
create(context) {
const options = context.options[0] || {};
const restrictedPaths = options.zones || [];
const basePath = options.basePath || process.cwd();
const filename = context.physicalFilename;
const matchingZones = restrictedPaths.filter(zone => [zone.target]
.flat()
.map(target => path.resolve(basePath, target))
.some(targetPath => isMatchingTargetPath(filename, targetPath)));
function isValidExceptionPath(absoluteFromPath, absoluteExceptionPath) {
const relativeExceptionPath = path.relative(absoluteFromPath, absoluteExceptionPath);
return importType(relativeExceptionPath, context) !== 'parent';
}
function reportInvalidExceptionPath(node) {
context.report({
node,
messageId: 'path',
});
}
function reportInvalidExceptionMixedGlobAndNonGlob(node) {
context.report({
node,
messageId: 'mixedGlob',
});
}
function reportInvalidExceptionGlob(node) {
context.report({
node,
messageId: 'glob',
});
}
function computeMixedGlobAndAbsolutePathValidator() {
return {
isPathRestricted: () => true,
hasValidExceptions: false,
reportInvalidException: reportInvalidExceptionMixedGlobAndNonGlob,
};
}
function computeGlobPatternPathValidator(absoluteFrom, zoneExcept) {
let isPathException;
const mm = new Minimatch(absoluteFrom, { windowsPathsNoEscape: true });
const isPathRestricted = (absoluteImportPath) => mm.match(absoluteImportPath);
const hasValidExceptions = zoneExcept.every(it => isGlob(it));
if (hasValidExceptions) {
const exceptionsMm = zoneExcept.map(except => new Minimatch(except, { windowsPathsNoEscape: true }));
isPathException = (absoluteImportPath) => exceptionsMm.some(mm => mm.match(absoluteImportPath));
}
const reportInvalidException = reportInvalidExceptionGlob;
return {
isPathRestricted,
hasValidExceptions,
isPathException,
reportInvalidException,
};
}
function computeAbsolutePathValidator(absoluteFrom, zoneExcept) {
let isPathException;
const isPathRestricted = (absoluteImportPath) => containsPath(absoluteImportPath, absoluteFrom);
const absoluteExceptionPaths = zoneExcept.map(exceptionPath => path.resolve(absoluteFrom, exceptionPath));
const hasValidExceptions = absoluteExceptionPaths.every(absoluteExceptionPath => isValidExceptionPath(absoluteFrom, absoluteExceptionPath));
if (hasValidExceptions) {
isPathException = absoluteImportPath => absoluteExceptionPaths.some(absoluteExceptionPath => containsPath(absoluteImportPath, absoluteExceptionPath));
}
const reportInvalidException = reportInvalidExceptionPath;
return {
isPathRestricted,
hasValidExceptions,
isPathException,
reportInvalidException,
};
}
function reportInvalidExceptions(validators, node) {
for (const validator of validators)
validator.reportInvalidException(node);
}
function reportImportsInRestrictedZone(validators, node, importPath, customMessage) {
for (const _ of validators) {
context.report({
node,
messageId: 'zone',
data: {
importPath,
extra: customMessage ? ` ${customMessage}` : '',
},
});
}
}
const makePathValidators = (zoneFrom, zoneExcept = []) => {
const allZoneFrom = [zoneFrom].flat();
const areGlobPatterns = allZoneFrom.map(it => isGlob(it));
if (areBothGlobPatternAndAbsolutePath(areGlobPatterns)) {
return [computeMixedGlobAndAbsolutePathValidator()];
}
const isGlobPattern = areGlobPatterns.every(Boolean);
return allZoneFrom.map(singleZoneFrom => {
const absoluteFrom = path.resolve(basePath, singleZoneFrom);
if (isGlobPattern) {
return computeGlobPatternPathValidator(absoluteFrom, zoneExcept);
}
return computeAbsolutePathValidator(absoluteFrom, zoneExcept);
});
};
const validators = [];
return moduleVisitor(source => {
const importPath = source.value;
const absoluteImportPath = resolve(importPath, context);
if (!absoluteImportPath) {
return;
}
for (const [index, zone] of matchingZones.entries()) {
if (!validators[index]) {
validators[index] = makePathValidators(zone.from, zone.except);
}
const applicableValidatorsForImportPath = validators[index].filter(validator => validator.isPathRestricted(absoluteImportPath));
const validatorsWithInvalidExceptions = applicableValidatorsForImportPath.filter(validator => !validator.hasValidExceptions);
reportInvalidExceptions(validatorsWithInvalidExceptions, source);
const applicableValidatorsForImportPathExcludingExceptions = applicableValidatorsForImportPath.filter(validator => validator.hasValidExceptions &&
!validator.isPathException(absoluteImportPath));
reportImportsInRestrictedZone(applicableValidatorsForImportPathExcludingExceptions, source, importPath, zone.message);
}
}, { commonjs: true });
},
});
//# sourceMappingURL=no-restricted-paths.js.map