170 lines
3.9 KiB
JavaScript
170 lines
3.9 KiB
JavaScript
import path from "path";
|
|
|
|
const REL_PATH_REGEX = /^\.{1,2}/;
|
|
|
|
function isRelativePath(filePath) {
|
|
return REL_PATH_REGEX.test(filePath);
|
|
}
|
|
|
|
function resolveAbsolutePath(currentFilePath, moduleToResolve) {
|
|
return isRelativePath(moduleToResolve)
|
|
? path.resolve(path.dirname(currentFilePath), moduleToResolve)
|
|
: moduleToResolve;
|
|
}
|
|
|
|
function isSourceErrorModule(currentFilePath, targetModulePath, src) {
|
|
for (const srcPath of [src, `${src}.js`, `${src}/index`, `${src}/index.js`]) {
|
|
if (
|
|
path.normalize(resolveAbsolutePath(currentFilePath, targetModulePath)) ===
|
|
path.normalize(resolveAbsolutePath(currentFilePath, srcPath))
|
|
) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function isCurrentFileErrorModule(currentFilePath, errorModule) {
|
|
return currentFilePath === errorModule;
|
|
}
|
|
|
|
function findIdNode(node) {
|
|
if (node.type === "Identifier") {
|
|
return node;
|
|
}
|
|
|
|
if (node.type === "MemberExpression" && node.object.type === "Identifier") {
|
|
return node.object;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function findIdNodes(node) {
|
|
if (node.type === "ConditionalExpression") {
|
|
const consequent = findIdNode(node.consequent);
|
|
const alternate = findIdNode(node.alternate);
|
|
|
|
if (consequent && alternate) {
|
|
return [consequent, alternate];
|
|
}
|
|
}
|
|
|
|
const idNode = findIdNode(node);
|
|
|
|
if (idNode) {
|
|
return [idNode];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function findReference(node, scope) {
|
|
let currentScope = scope;
|
|
|
|
while (currentScope) {
|
|
const ref = currentScope.set.get(node.name);
|
|
|
|
if (ref) {
|
|
return ref;
|
|
}
|
|
|
|
currentScope = currentScope.upper;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function referencesImportedBinding(node, scope, bindings) {
|
|
const ref = findReference(node, scope);
|
|
|
|
if (ref) {
|
|
const topLevelDef = ref.defs[0];
|
|
|
|
if (topLevelDef.type === "ImportBinding") {
|
|
const defNode = topLevelDef.node;
|
|
|
|
for (const spec of bindings) {
|
|
if (
|
|
spec.loc.start === defNode.loc.start &&
|
|
spec.loc.end === defNode.loc.end
|
|
) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
export default {
|
|
meta: {
|
|
type: "suggestion",
|
|
docs: {
|
|
description:
|
|
"enforce @babel/parser's error messages to be consolidated in one module",
|
|
},
|
|
schema: [
|
|
{
|
|
type: "object",
|
|
properties: {
|
|
errorModule: { type: "string" },
|
|
},
|
|
additionalProperties: false,
|
|
required: ["errorModule"],
|
|
},
|
|
],
|
|
messages: {
|
|
mustBeImported: 'Error messages must be imported from "{{errorModule}}".',
|
|
},
|
|
},
|
|
create({ options, report, getFilename, getScope }) {
|
|
const [{ errorModule = "" } = {}] = options;
|
|
const filename = getFilename();
|
|
const importedBindings = new Set();
|
|
|
|
if (
|
|
// Do not run check if errorModule config option is not given.
|
|
!errorModule.length ||
|
|
// Do not check the target error module file.
|
|
isCurrentFileErrorModule(filename, errorModule)
|
|
) {
|
|
return {};
|
|
}
|
|
|
|
return {
|
|
// Check imports up front so that we don't have to check them for every ThrowStatement.
|
|
ImportDeclaration(node) {
|
|
if (isSourceErrorModule(filename, errorModule, node.source.value)) {
|
|
for (const spec of node.specifiers) {
|
|
importedBindings.add(spec);
|
|
}
|
|
}
|
|
},
|
|
"CallExpression[callee.type='MemberExpression'][callee.object.type='ThisExpression'][callee.property.name='raise'][arguments.length>=2]"(
|
|
node,
|
|
) {
|
|
const [, errorMsgNode] = node.arguments;
|
|
const nodesToCheck = findIdNodes(errorMsgNode);
|
|
|
|
if (
|
|
Array.isArray(nodesToCheck) &&
|
|
nodesToCheck.every(node =>
|
|
referencesImportedBinding(node, getScope(), importedBindings),
|
|
)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
report({
|
|
node: errorMsgNode,
|
|
messageId: "mustBeImported",
|
|
data: { errorModule },
|
|
});
|
|
},
|
|
};
|
|
},
|
|
};
|