123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318 |
- const Multimap = require('multimap');
- const { parse } = require('@babel/eslint-parser');
- const { query } = require('esquery');
- const { getMatchKeys } = require('./match-keys');
- const { nodesPropertiesEqual } = require('./nodes-properties-equal');
- const gensym = () => 'gensym' + Math.random().toString(16).slice(2);
- const nodeEquals = (a, b) => {
- if (!a || !b) {
- return (
- (a === null || a === undefined)
- && (b === null || b === undefined)
- );
- }
- const { visitorKeys, equalityKeys } = getMatchKeys(a);
- return visitorKeys.every(key => {
- return nodePropertyEquals(key, a, b);
- }) && equalityKeys.every(key => {
- return nodesPropertiesEqual(a, b, key);
- });
- };
- const everyNodeEquals = (as, bs) => {
- return as.length === bs.length
- && as.every((a, index) => {
- const b = bs[index];
- return nodeEquals(a, b);
- });
- };
- const nodePropertyEquals = (key, a, b) => {
- if (Array.isArray(a[key])) {
- return a[key].length === b[key].length
- && a[key].every((x, i) => nodeEquals(x, b[key][i]));
- }
- return nodeEquals(a[key], b[key]);
- };
- class Variable {
- constructor() {
- this._id = gensym();
- }
- toString() {
- return this._id;
- }
- }
- class SpreadVariable extends Variable {}
- class VariableDeclarationVariable extends Variable {
- toString() {
- return `var ${this._id}, `;
- }
- }
- 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(ast, options, templates) {
- if (typeof ast === 'string') {
- let { babelOptions = {}, ...parserOptions } = options.parserOptions || {};
- parserOptions = {
- ecmaVersion: 2018,
- requireConfigFile: false,
- babelOptions: {
- configFile: false,
- ...babelOptions,
- },
- ...parserOptions,
- };
- const { body } = parse(ast, parserOptions);
- if (body.length !== 1) {
- throw new Error('Template must contain a single top-level AST node.');
- }
- const [ firstNode ] = body;
- ast = firstNode.type === 'ExpressionStatement' ? firstNode.expression : firstNode;
- }
- this._id = gensym();
- this._ast = ast;
- this._templates = templates;
- }
- narrow(selector, targetMatchIndex = 0) {
- const ast = query(this._ast, selector)[targetMatchIndex];
- const template = new Template(ast, null, this._templates);
- this._templates._registerTemplate(template);
- return template;
- }
- 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;
- }
- _getNodeVariable(templateNode) {
- if (templateNode.type === 'Identifier') {
- return this._variables.get(templateNode.name);
- }
- if (
- templateNode.type === 'VariableDeclaration'
- && templateNode.kind === 'var'
- && templateNode.declarations.length > 1
- ) {
- const [ firstDeclarator ] = templateNode.declarations;
- return this._variables.get(firstDeclarator.id.name);
- }
- return null;
- }
- _getSpreadVariableNode(templateNode) {
- if (templateNode.type === 'ExpressionStatement') {
- templateNode = templateNode.expression;
- }
- if (!this._getNodeVariable(templateNode)) {
- return undefined;
- }
- const variable = this._variables.get(templateNode.name);
- return variable instanceof SpreadVariable
- ? templateNode
- : undefined;
- }
- _nodeMatches(templateNode, node, context) {
- if (!templateNode || !node) {
- return (
- (templateNode === null || templateNode === undefined)
- && (node === null || node === undefined)
- );
- }
- const variable = this._getNodeVariable(templateNode);
- if (variable) {
- if (variable instanceof VariableDeclarationVariable) {
- if (node.type !== 'VariableDeclaration') {
- return false;
- }
- }
- const previousMatches = context.getMatches(variable);
- if (previousMatches.every(previousMatchNode => nodeEquals(previousMatchNode, node))) {
- if (variable instanceof VariableDeclarationVariable) {
- const templateDeclarations = templateNode.declarations.slice(1);
- if (!this._nodeArraysMatch(templateDeclarations, node.declarations, context)) {
- return false;
- }
- }
- context._pushVariableMatch(variable._id, 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 => everyNodeEquals(previousMatchNodes, nodes))) {
- context._pushVariableMatch(templateNode.name, nodes);
- return true;
- }
- return false;
- }
- _nodePropertyMatches(key, templateNode, node, context) {
- if (Array.isArray(templateNode[key])) {
- return this._nodeArraysMatch(templateNode[key], node[key], context);
- }
- return this._nodeMatches(templateNode[key], node[key], context);
- }
- _nodeArraysMatch(templateNodes, nodes, context) {
- if (!nodes) {
- return false;
- }
- if (templateNodes.length === 1) {
- const spreadVariableNode = this._getSpreadVariableNode(templateNodes[0]);
- if (spreadVariableNode) {
- return this._spreadVariableMatches(spreadVariableNode, nodes, context);
- }
- }
- return templateNodes.length === nodes.length
- && templateNodes.every((x, i) => this._nodeMatches(x, nodes[i], 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;
- }
- variableDeclarationVariable() {
- const variable = new VariableDeclarationVariable();
- 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);
- this._registerTemplate(template);
- return template;
- }
- _registerTemplate(template) {
- this._templates.set(template._id, 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;
|