237 lines
5.5 KiB
JavaScript
237 lines
5.5 KiB
JavaScript
|
|
const Multimap = require('multimap');
|
|
|
|
const espree = require('espree');
|
|
|
|
const { getMatchKeys } = require('./match-keys');
|
|
const { nodesPropertiesEqual } = require('./nodes-properties-equal');
|
|
|
|
const gensym = () => 'gensym' + Math.random().toString(16).slice(2);
|
|
|
|
class Variable {
|
|
constructor() {
|
|
this._id = gensym();
|
|
}
|
|
|
|
toString() {
|
|
return this._id;
|
|
}
|
|
}
|
|
|
|
class SpreadVariable extends Variable {}
|
|
|
|
class TemplateContext {
|
|
constructor() {
|
|
this._matches = new Multimap();
|
|
}
|
|
|
|
_pushVariableMatch(variableId, node) {
|
|
this._matches.set(variableId, node);
|
|
}
|
|
|
|
getMatches(variable) {
|
|
return this._matches.get(variable._id) || [];
|
|
}
|
|
|
|
getMatch(variable) {
|
|
return this.getMatches(variable)[0];
|
|
}
|
|
}
|
|
|
|
class Template {
|
|
constructor(source, options) {
|
|
const parserOptions = options.parserOptions || {
|
|
ecmaVersion: 2018,
|
|
};
|
|
|
|
this._id = gensym();
|
|
|
|
const { body: [ firstNode ] } = espree.parse(source, parserOptions);
|
|
|
|
this._ast = firstNode.type === 'ExpressionStatement' ? firstNode.expression : firstNode;
|
|
}
|
|
|
|
toString() {
|
|
return this._id;
|
|
}
|
|
}
|
|
|
|
class TemplateManager {
|
|
constructor(options = {}) {
|
|
this._options = options;
|
|
this._variables = new Map();
|
|
this._templates = new Map();
|
|
}
|
|
|
|
_matchTemplate(handler, template, node, ...rest) {
|
|
template.context = new TemplateContext();
|
|
|
|
if (this._nodeMatches(template._ast, node, template.context)) {
|
|
return handler(node, ...rest);
|
|
}
|
|
|
|
template.context = null;
|
|
}
|
|
|
|
_isNodeVariable(templateNode) {
|
|
return templateNode.type === 'Identifier'
|
|
&& this._variables.has(templateNode.name);
|
|
}
|
|
|
|
_getSpreadVariableNode(templateNode) {
|
|
if (templateNode.type === 'ExpressionStatement') {
|
|
templateNode = templateNode.expression;
|
|
}
|
|
|
|
if (!this._isNodeVariable(templateNode)) {
|
|
return undefined;
|
|
}
|
|
|
|
const variable = this._variables.get(templateNode.name);
|
|
|
|
return variable instanceof SpreadVariable
|
|
? templateNode
|
|
: undefined;
|
|
}
|
|
|
|
_nodeEquals(a, b) {
|
|
const { visitorKeys, equalityKeys } = getMatchKeys(a);
|
|
|
|
return visitorKeys.every(key => {
|
|
return this._nodePropertyEquals(key, a, b);
|
|
}) && equalityKeys.every(key => {
|
|
return nodesPropertiesEqual(a, b, key);
|
|
});
|
|
}
|
|
|
|
_everyNodeEquals(as, bs) {
|
|
return as.length === bs.length
|
|
&& as.every((a, index) => {
|
|
const b = bs[index];
|
|
return this._nodeEquals(a, b);
|
|
});
|
|
}
|
|
|
|
_nodePropertyEquals(key, a, b) {
|
|
if (Array.isArray(a[key])) {
|
|
return a[key].length === b[key].length
|
|
&& a[key].every((x, i) => this._nodeEquals(x, b[key][i]));
|
|
}
|
|
|
|
return this._nodeEquals(a[key], b[key]);
|
|
}
|
|
|
|
_nodeMatches(templateNode, node, context) {
|
|
if (!templateNode || !node) {
|
|
return templateNode === node;
|
|
}
|
|
|
|
if (this._isNodeVariable(templateNode)) {
|
|
const variable = this._variables.get(templateNode.name);
|
|
const previousMatches = context.getMatches(variable);
|
|
|
|
if (previousMatches.every(previousMatchNode => this._nodeEquals(previousMatchNode, node))) {
|
|
context._pushVariableMatch(templateNode.name, node);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
const { visitorKeys, equalityKeys } = getMatchKeys(templateNode);
|
|
|
|
const matches = visitorKeys.every(key => {
|
|
return this._nodePropertyMatches(key, templateNode, node, context);
|
|
}) && equalityKeys.every(key => {
|
|
return nodesPropertiesEqual(templateNode, node, key);
|
|
});
|
|
|
|
return matches;
|
|
}
|
|
|
|
_spreadVariableMatches(templateNode, nodes, context) {
|
|
const variable = this._variables.get(templateNode.name);
|
|
const previousMatches = context.getMatches(variable);
|
|
|
|
if (previousMatches.every(previousMatchNodes => this._everyNodeEquals(previousMatchNodes, nodes))) {
|
|
context._pushVariableMatch(templateNode.name, nodes);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
_nodePropertyMatches(key, templateNode, node, context) {
|
|
if (Array.isArray(templateNode[key])) {
|
|
if (!node[key]) {
|
|
return false;
|
|
}
|
|
|
|
if (templateNode[key].length === 1) {
|
|
const spreadVariableNode = this._getSpreadVariableNode(templateNode[key][0]);
|
|
|
|
if (spreadVariableNode) {
|
|
return this._spreadVariableMatches(spreadVariableNode, node[key], context);
|
|
}
|
|
}
|
|
|
|
return templateNode[key].length === node[key].length
|
|
&& templateNode[key].every((x, i) => this._nodeMatches(x, node[key][i], context));
|
|
}
|
|
|
|
return this._nodeMatches(templateNode[key], node[key], context);
|
|
}
|
|
|
|
variable() {
|
|
const variable = new Variable();
|
|
this._variables.set(variable._id, variable);
|
|
return variable;
|
|
}
|
|
|
|
spreadVariable() {
|
|
const variable = new SpreadVariable();
|
|
this._variables.set(variable._id, variable);
|
|
return variable;
|
|
}
|
|
|
|
template(strings, ...vars) {
|
|
const source = typeof strings === 'string'
|
|
? strings
|
|
: strings.map((string, i) => string + (vars[i] || '')).join('');
|
|
const template = new Template(source, this._options);
|
|
this._templates.set(template._id, template);
|
|
return template;
|
|
}
|
|
|
|
visitor(visitor) {
|
|
const newVisitor = {};
|
|
|
|
for (const key of Object.keys(visitor)) {
|
|
const value = visitor[key];
|
|
const template = this._templates.get(key);
|
|
|
|
const newKey = template ? template._ast.type : key;
|
|
|
|
const newValue = template ? (...args) => {
|
|
return this._matchTemplate(value, template, ...args);
|
|
} : value;
|
|
|
|
newVisitor[newKey] = newVisitor[newKey] || [];
|
|
newVisitor[newKey].push(newValue);
|
|
}
|
|
|
|
for (const newKey of Object.keys(newVisitor)) {
|
|
const newValue = newVisitor[newKey];
|
|
newVisitor[newKey] = newValue.length === 1 ? newValue[0] : (...args) => {
|
|
newValue.forEach(handler => handler(...args));
|
|
};
|
|
}
|
|
|
|
return newVisitor;
|
|
}
|
|
}
|
|
|
|
const eslintTemplateVisitor = options => new TemplateManager(options);
|
|
|
|
module.exports = eslintTemplateVisitor;
|