207 lines
No EOL
9.4 KiB
JavaScript
207 lines
No EOL
9.4 KiB
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
|