@ka1do/kit
Version:
681 lines (605 loc) • 22.4 kB
JavaScript
'use strict';
const { findVariable } = require('eslint-utils');
const { getPropertyName } = require('eslint-utils');
const { getStringIfConstant, ReferenceTracker } = require('eslint-utils');
const { getTestCaseNames, getSuiteNames } = require('../util/names');
const { getAddtionalNames } = require('../util/settings');
const IMPORT_TYPE = /^(?:Import|Export(?:All|Default|Named))Declaration$/u;
const has = Function.call.bind(Object.hasOwnProperty);
const READ = Symbol('read');
const CALL = Symbol('call');
const CONSTRUCT = Symbol('construct');
const ESM = Symbol('esm');
const dynamic = Symbol('dynamic');
const requireCall = { require: { [CALL]: true } };
/**
* Check whether a given variable is modified or not.
* @param {Variable} variable The variable to check.
* @returns {boolean} `true` if the variable is modified.
*/
function isModifiedGlobal(variable) {
return (
variable == null ||
variable.defs.length !== 0 ||
variable.references.some((r) => r.isWrite())
);
}
function getPropertyName2(memberExpresssionNode) {
if (memberExpresssionNode.computed) {
if (memberExpresssionNode.property.type === 'Literal') {
return memberExpresssionNode.property.value;
}
return dynamic;
}
return memberExpresssionNode.property.name;
}
function fooObjectNamePath(node) {
if (node.type === 'MemberExpression') {
if (node.object.type === 'MemberExpression') {
return [ ...fooObjectNamePath(node.object), getPropertyName2(node) ];
}
return [ node.object.name, getPropertyName2(node) ];
}
return [ node.name ];
}
function getParentNodeForIdentifier(node) {
if (node.parent.type === 'MemberExpression') {
return getParentNodeForIdentifier(node.parent);
}
return { node: node.parent, path: fooObjectNamePath(node) };
}
function findGlobalVariableReferences(globalScope, name) {
/* const variable = globalScope.set.get(name);
if (variable && variable.defs.length === 0) {
return variable.references;
}
return globalScope.through.filter((reference) => {
return reference.identifier.name === name;
});*/
const tracker = new utils.ReferenceTracker(globalScope, options);
const it = tracker.iterateGlobalReferences(traceMap);
// ...
}
function xxx(patternNode, path, traceMap) {
if (patternNode.type === 'Identifier') {
const variable = findVariable(this.globalScope, patternNode);
if (variable != null) {
this._iterateVariableReferences(
variable,
path,
traceMap,
false
);
}
return;
}
if (patternNode.type === 'ObjectPattern') {
for (const property of patternNode.properties) {
const key = getPropertyName(property);
if (key == null || !has(traceMap, key)) {
continue;
}
const nextPath = path.concat(key);
const nextTraceMap = traceMap[key];
if (nextTraceMap[READ]) {
return {
node: property,
path: nextPath,
type: READ,
info: nextTraceMap[READ]
};
}
this._iterateLhsReferences(
property.value,
nextPath,
nextTraceMap
);
}
return;
}
if (patternNode.type === 'AssignmentPattern') {
this._iterateLhsReferences(patternNode.left, path, traceMap);
}
}
function lol(rootNode, path, names) {
let node = rootNode;
while (isPassThrough(node)) {
node = node.parent;
}
const parent = node.parent;
if (parent.type === 'MemberExpression') {
if (parent.object === node) {
const key = getPropertyName(parent);
if (!names.include(key)) {
return;
}
path = path.concat(key); // eslint-disable-line no-param-reassign
const nextTraceMap = traceMap[key];
lol(parent, path, nextTraceMap);
}
return;
}
if (parent.type === 'AssignmentExpression') {
if (parent.right === node) {
xxx(parent.left, path, traceMap);
lol(parent, path, traceMap);
}
return;
}
if (parent.type === 'AssignmentPattern') {
if (parent.right === node) {
xxx(parent.left, path, traceMap);
}
return;
}
if (parent.type === 'VariableDeclarator') {
if (parent.init === node) {
xxx(parent.id, path, traceMap);
}
}
}
function findCommonJsMochaReferences(globalScope, name) {
return findGlobalVariableReferences(globalScope, 'require').reduce((memo, requireCall) => {
const key = getStringIfConstant(requireCall.node.arguments[0]);
if (key === 'mocha') {
return [ ...memo, ...lol() ];
}
return memo;
}, []);
}
function nameToPath(name) {
return name.split('.');
}
function pathEncodedNameToPlainName(name) {
const path = nameToPath(name);
return path[0];
}
function normalizePathsInNameList(names) {
return names.map(pathEncodedNameToPlainName);
}
function startsWithPath(fullPath, pathToMatch) {
return pathToMatch.every((segment, index) => segment === fullPath[index]);
}
function findMatchingNameOrPath(names, path) {
return names.map(nameToPath).find((pathToMatch) => {
console.log(pathToMatch, path);
return startsWithPath(path, pathToMatch);
});
}
function findGlobalVariableCalls(globalScope, { names }) {
const references = normalizePathsInNameList(names).flatMap((name) => findGlobalVariableReferences(globalScope, name));
return references.reduce((results, reference) => {
console.log('ref', reference);
const { node: parent, path } = getParentNodeForIdentifier(reference.identifier, []);
const matchingPath = findMatchingNameOrPath(names, path);
if (parent.type === 'CallExpression' && matchingPath !== undefined) {
return [ ...results, { reference, node: parent, path, matchingPath } ];
}
return results;
}, []);
}
/**
* Check if the value of a given node is passed through to the parent syntax as-is.
* For example, `a` and `b` in (`a || b` and `c ? a : b`) are passed through.
* @param {Node} node A node to check.
* @returns {boolean} `true` if the node is passed through.
*/
function isPassThrough(node) {
const parent = node.parent;
switch (parent && parent.type) {
case 'ConditionalExpression':
return parent.consequent === node || parent.alternate === node;
case 'LogicalExpression':
return true;
case 'SequenceExpression':
return parent.expressions[parent.expressions.length - 1] === node;
case 'ChainExpression':
return true;
default:
return false;
}
}
/**
* The reference tracker.
*/
class ReferenceTracker2 {
/**
* Initialize this tracker.
* @param {Scope} globalScope The global scope.
* @param {object} [options] The options.
* @param {"legacy"|"strict"} [options.mode="strict"] The mode to determine the ImportDeclaration's behavior for CJS modules.
* @param {string[]} [options.globalObjectNames=["global","globalThis","self","window"]] The variable names for Global Object.
*/
constructor(
globalScope,
{
mode = 'strict',
globalObjectNames = [ 'global', 'globalThis', 'self', 'window' ]
} = {}
) {
this.variableStack = [];
this.globalScope = globalScope;
this.mode = mode;
this.globalObjectNames = globalObjectNames.slice(0);
}
/**
* Iterate the references of global variables.
* @param {object} traceMap The trace map.
* @returns {IterableIterator<{node:Node,path:string[],type:symbol,info:any}>} The iterator to iterate references.
*/
*iterateGlobalReferences(traceMap) {
for (const key of Object.keys(traceMap)) {
const nextTraceMap = traceMap[key];
const path = [ key ];
let variable = this.globalScope.set.get(key);
if (!variable) {
variable = this.globalScope.through.find((reference) => {
return reference.identifier.name === key;
});
}
console.log('xxx', variable);
if (isModifiedGlobal(variable)) {
continue;
}
yield* this._iterateVariableReferences(
variable,
path,
nextTraceMap,
true
);
}
for (const key of this.globalObjectNames) {
const path = [];
const variable = this.globalScope.set.get(key);
if (isModifiedGlobal(variable)) {
continue;
}
yield* this._iterateVariableReferences(
variable,
path,
traceMap,
false
);
}
}
/**
* Iterate the references of CommonJS modules.
* @param {object} traceMap The trace map.
* @returns {IterableIterator<{node:Node,path:string[],type:symbol,info:any}>} The iterator to iterate references.
*/
*iterateCjsReferences(traceMap) {
for (const { node } of this.iterateGlobalReferences(requireCall)) {
const key = getStringIfConstant(node.arguments[0]);
if (key == null || !has(traceMap, key)) {
continue;
}
const nextTraceMap = traceMap[key];
const path = [ key ];
if (nextTraceMap[READ]) {
yield {
node,
path,
type: READ,
info: nextTraceMap[READ]
};
}
yield* this.lol(node, path, nextTraceMap);
}
}
/**
* Iterate the references of ES modules.
* @param {object} traceMap The trace map.
* @returns {IterableIterator<{node:Node,path:string[],type:symbol,info:any}>} The iterator to iterate references.
*/
*iterateEsmReferences(traceMap) {
const programNode = this.globalScope.block;
for (const node of programNode.body) {
if (!IMPORT_TYPE.test(node.type) || node.source == null) {
continue;
}
const moduleId = node.source.value;
if (!has(traceMap, moduleId)) {
continue;
}
const nextTraceMap = traceMap[moduleId];
const path = [ moduleId ];
if (nextTraceMap[READ]) {
yield { node, path, type: READ, info: nextTraceMap[READ] };
}
if (node.type === 'ExportAllDeclaration') {
for (const key of Object.keys(nextTraceMap)) {
const exportTraceMap = nextTraceMap[key];
if (exportTraceMap[READ]) {
yield {
node,
path: path.concat(key),
type: READ,
info: exportTraceMap[READ]
};
}
}
} else {
for (const specifier of node.specifiers) {
const esm = has(nextTraceMap, ESM);
const it = this._iterateImportReferences(
specifier,
path,
esm ?
nextTraceMap :
this.mode === 'legacy' ?
Object.assign(
{ default: nextTraceMap },
nextTraceMap
) :
{ default: nextTraceMap }
);
if (esm) {
yield* it;
} else {
for (const report of it) {
report.path = report.path.filter(exceptDefault);
if (
report.path.length >= 2 ||
report.type !== READ
) {
yield report;
}
}
}
}
}
}
}
/**
* Iterate the references for a given variable.
* @param {Variable} variable The variable to iterate that references.
* @param {string[]} path The current path.
* @param {object} traceMap The trace map.
* @param {boolean} shouldReport = The flag to report those references.
* @returns {IterableIterator<{node:Node,path:string[],type:symbol,info:any}>} The iterator to iterate references.
*/
*_iterateVariableReferences(variable, path, traceMap, shouldReport) {
console.log('foo');
if (this.variableStack.includes(variable)) {
return;
}
console.log('foo');
this.variableStack.push(variable);
try {
for (const reference of variable.references) {
if (!reference.isRead()) {
continue;
}
const node = reference.identifier;
console.log(reference.identifier);
if (shouldReport && traceMap[READ]) {
yield { node, path, type: READ, info: traceMap[READ] };
}
yield* this._iteratePropertyReferences(node, path, traceMap);
}
} finally {
this.variableStack.pop();
}
}
/**
* Iterate the references for a given AST node.
* @param rootNode The AST node to iterate references.
* @param {string[]} path The current path.
* @param {object} traceMap The trace map.
* @returns {IterableIterator<{node:Node,path:string[],type:symbol,info:any}>} The iterator to iterate references.
*/
// eslint-disable-next-line complexity
*_iteratePropertyReferences(rootNode, path, traceMap) {
let node = rootNode;
while (isPassThrough(node)) {
node = node.parent;
}
const parent = node.parent;
if (parent.type === 'MemberExpression') {
if (parent.object === node) {
const key = getPropertyName(parent);
if (key == null || !has(traceMap, key)) {
return;
}
path = path.concat(key); // eslint-disable-line no-param-reassign
const nextTraceMap = traceMap[key];
if (nextTraceMap[READ]) {
yield {
node: parent,
path,
type: READ,
info: nextTraceMap[READ]
};
}
yield* this._iteratePropertyReferences(
parent,
path,
nextTraceMap
);
}
return;
}
if (parent.type === 'CallExpression') {
if (parent.callee === node && traceMap[CALL]) {
yield { node: parent, path, type: CALL, info: traceMap[CALL] };
}
return;
}
if (parent.type === 'NewExpression') {
if (parent.callee === node && traceMap[CONSTRUCT]) {
yield {
node: parent,
path,
type: CONSTRUCT,
info: traceMap[CONSTRUCT]
};
}
return;
}
if (parent.type === 'AssignmentExpression') {
if (parent.right === node) {
yield* this._iterateLhsReferences(parent.left, path, traceMap);
yield* this._iteratePropertyReferences(parent, path, traceMap);
}
return;
}
if (parent.type === 'AssignmentPattern') {
if (parent.right === node) {
yield* this._iterateLhsReferences(parent.left, path, traceMap);
}
return;
}
if (parent.type === 'VariableDeclarator') {
if (parent.init === node) {
yield* this._iterateLhsReferences(parent.id, path, traceMap);
}
}
}
/**
* Iterate the references for a given Pattern node.
* @param {Node} patternNode The Pattern node to iterate references.
* @param {string[]} path The current path.
* @param {object} traceMap The trace map.
* @returns {IterableIterator<{node:Node,path:string[],type:symbol,info:any}>} The iterator to iterate references.
*/
*_iterateLhsReferences(patternNode, path, traceMap) {
if (patternNode.type === 'Identifier') {
const variable = findVariable(this.globalScope, patternNode);
if (variable != null) {
yield* this._iterateVariableReferences(
variable,
path,
traceMap,
false
);
}
return;
}
if (patternNode.type === 'ObjectPattern') {
for (const property of patternNode.properties) {
const key = getPropertyName(property);
if (key == null || !has(traceMap, key)) {
continue;
}
const nextPath = path.concat(key);
const nextTraceMap = traceMap[key];
if (nextTraceMap[READ]) {
yield {
node: property,
path: nextPath,
type: READ,
info: nextTraceMap[READ]
};
}
yield* this._iterateLhsReferences(
property.value,
nextPath,
nextTraceMap
);
}
return;
}
if (patternNode.type === 'AssignmentPattern') {
yield* this._iterateLhsReferences(patternNode.left, path, traceMap);
}
}
/**
* Iterate the references for a given ModuleSpecifier node.
* @param {Node} specifierNode The ModuleSpecifier node to iterate references.
* @param {string[]} path The current path.
* @param {object} traceMap The trace map.
* @returns {IterableIterator<{node:Node,path:string[],type:symbol,info:any}>} The iterator to iterate references.
*/
*_iterateImportReferences(specifierNode, path, traceMap) {
const type = specifierNode.type;
if (type === 'ImportSpecifier' || type === 'ImportDefaultSpecifier') {
const key =
type === 'ImportDefaultSpecifier' ?
'default' :
specifierNode.imported.name;
if (!has(traceMap, key)) {
return;
}
path = path.concat(key); // eslint-disable-line no-param-reassign
const nextTraceMap = traceMap[key];
if (nextTraceMap[READ]) {
yield {
node: specifierNode,
path,
type: READ,
info: nextTraceMap[READ]
};
}
yield* this._iterateVariableReferences(
findVariable(this.globalScope, specifierNode.local),
path,
nextTraceMap,
false
);
return;
}
if (type === 'ImportNamespaceSpecifier') {
yield* this._iterateVariableReferences(
findVariable(this.globalScope, specifierNode.local),
path,
traceMap,
false
);
return;
}
if (type === 'ExportSpecifier') {
const key = specifierNode.local.name;
if (!has(traceMap, key)) {
return;
}
path = path.concat(key); // eslint-disable-line no-param-reassign
const nextTraceMap = traceMap[key];
if (nextTraceMap[READ]) {
yield {
node: specifierNode,
path,
type: READ,
info: nextTraceMap[READ]
};
}
}
}
}
ReferenceTracker.READ = READ;
ReferenceTracker.CALL = CALL;
ReferenceTracker.CONSTRUCT = CONSTRUCT;
ReferenceTracker.ESM = ESM;
function extractModifier(callee) {
if (callee.type === 'MemberExpression') {
if (callee.property.type === 'Identifier') {
return callee.property.name;
}
return callee.property.value;
}
return null;
}
function createMochaVisitors(context, visitors) {
const globalScope = context.getScope();
const additionalCustomNames = getAddtionalNames(context.settings);
const testCaseNames = getTestCaseNames({ modifiersOnly: false, modifiers: [], additionalCustomNames });
const suiteNames = getSuiteNames({ modifiersOnly: false, modifiers: [], additionalCustomNames });
const names = [ ...testCaseNames, ...suiteNames ];
console.log(names);
// const calls = findGlobalVariableCalls(globalScope, { names: [ 'it', 'suite', 'describe', 'test', 'context', 'specify', 'custom' ] });
const calls = findGlobalVariableCalls(globalScope, { names });
if (typeof visitors.testCase === 'function') {
console.log('found it', calls);
calls.forEach((call) => {
const modifiers = call.path.slice(call.matchingPath.length);
console.log(modifiers);
const testCaseContext = {
name: call.matchingPath.join('.'),
modifiers,
isExclusive: modifiers.includes('only'),
node: call.node
};
visitors.testCase(testCaseContext);
});
}
}
module.exports = { createMochaVisitors, findGlobalVariableCalls };