@putout/operator-declare
Version:
🐊Putout operator adds ability to declare referenced variables that was not defined
193 lines (138 loc) • 4.72 kB
JavaScript
;
const {template} = require('@putout/engine-parser');
const {isESM, insertAfter} = require('@putout/operate');
const {compare} = require('@putout/compare');
const {types} = require('@putout/babel');
const {
addDeclarationForESLint,
checkDeclarationForESLint,
getModuleType,
setModuleType,
} = require('./record');
const {
isImportDeclaration,
isVariableDeclaration,
} = types;
const {keys} = Object;
const isString = (a) => typeof a === 'string';
const getLastVarPath = (bodyPath) => bodyPath
.filter(isVariableDeclaration)
.pop();
const isLast = (insertionPath, bodyPath) => bodyPath.at(-1) === insertionPath;
const isLocalImport = (path) => path.node.source.value.includes('.');
const cutName = (a) => a
.split('.')
.shift();
const parseType = (path) => isESM(path) ? 'esm' : 'commonjs';
const TS_EXCLUDE = [
'TSMethodSignature',
'TSParameterProperty',
'TSFunctionType',
'TSDeclareMethod',
'TSDeclareFunction',
'TSTypeAliasDeclaration',
];
module.exports.declare = (declarations) => ({
report,
include,
fix: fix(declarations),
filter: filter(declarations),
});
const report = (path) => {
const {name} = path.node;
const peaceOfName = cutName(name);
return `Declare '${peaceOfName}', it referenced but not defined`;
};
const include = () => [
'ReferencedIdentifier',
];
const filter = (declarations) => (path, {options}) => {
if (TS_EXCLUDE.includes(path.parentPath.type))
return false;
const {dismiss = [], type: typeOverride} = options;
const allDeclarations = {
...declarations,
...options.declarations,
};
const names = keys(allDeclarations);
const {scope, node} = path;
const {name} = node;
if (!names.includes(name))
return false;
if (dismiss.includes(name))
return false;
if (scope.hasBinding(name) || checkDeclarationForESLint(name, path))
return false;
const type = computeType(path, typeOverride);
return parseCode(type, allDeclarations[name]);
};
const fix = (declarations) => (path, {options}) => {
const type = getModuleType(path);
const allDeclarations = {
...declarations,
...options.declarations,
};
const {name} = path.node;
const code = parseCode(type, allDeclarations[name]);
const scope = path.scope.getProgramParent();
const programPath = scope.path;
const bodyPath = programPath.get('body');
const node = template.ast.fresh(code);
insert(node, bodyPath);
addDeclarationForESLint(name, path);
};
const parseCode = (type, current) => {
if (isString(current))
return current;
return current[type];
};
function getInsertionPath(node, bodyPath) {
const lastImportPath = getLastImportPath(bodyPath);
const lastVarPath = getLastVarPath(bodyPath);
if (isVariableDeclaration(node) && lastImportPath)
return lastImportPath;
if ((isImportDeclaration(node) || isRequire(node)) && lastImportPath)
return lastImportPath;
if (isVariableDeclaration(node) && lastVarPath && !isLast(lastVarPath, bodyPath))
return lastVarPath;
return null;
}
const isRequire = (node) => compare(node, 'const __a = require(__b)');
function insert(node, bodyPath) {
const insertionPath = getInsertionPath(node, bodyPath);
const [first] = bodyPath;
if (isVariableDeclaration(node) && isImportDeclaration(insertionPath) || isRequire(insertionPath))
return insertAfter(insertionPath, node);
if (isVariableDeclaration(node)) {
if (isRequire(first))
return first.insertAfter(node);
return first.insertBefore(node);
}
if (!insertionPath)
return first.insertBefore(node);
if (insertionPath.isImportDeclaration() && isLocalImport(insertionPath))
return insertionPath.insertBefore(node);
return insertAfter(insertionPath, node);
}
const getLastImportPath = (bodyPath) => {
const imports = [];
const localImports = [];
for (const path of bodyPath) {
if (!path.isImportDeclaration())
continue;
if (isLocalImport(path)) {
localImports.push(path);
continue;
}
imports.push(path);
}
return imports.pop() || localImports.pop();
};
function computeType(path, type) {
if (type)
return setModuleType(type, path);
const newType = getModuleType(path);
if (newType)
return newType;
return setModuleType(parseType(path), path);
}