index.js 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. const Multimap = require('multimap');
  2. const { parse } = require('@babel/eslint-parser');
  3. const { query } = require('esquery');
  4. const { getMatchKeys } = require('./match-keys');
  5. const { nodesPropertiesEqual } = require('./nodes-properties-equal');
  6. const gensym = () => 'gensym' + Math.random().toString(16).slice(2);
  7. const nodeEquals = (a, b) => {
  8. if (!a || !b) {
  9. return (
  10. (a === null || a === undefined)
  11. && (b === null || b === undefined)
  12. );
  13. }
  14. const { visitorKeys, equalityKeys } = getMatchKeys(a);
  15. return visitorKeys.every(key => {
  16. return nodePropertyEquals(key, a, b);
  17. }) && equalityKeys.every(key => {
  18. return nodesPropertiesEqual(a, b, key);
  19. });
  20. };
  21. const everyNodeEquals = (as, bs) => {
  22. return as.length === bs.length
  23. && as.every((a, index) => {
  24. const b = bs[index];
  25. return nodeEquals(a, b);
  26. });
  27. };
  28. const nodePropertyEquals = (key, a, b) => {
  29. if (Array.isArray(a[key])) {
  30. return a[key].length === b[key].length
  31. && a[key].every((x, i) => nodeEquals(x, b[key][i]));
  32. }
  33. return nodeEquals(a[key], b[key]);
  34. };
  35. class Variable {
  36. constructor() {
  37. this._id = gensym();
  38. }
  39. toString() {
  40. return this._id;
  41. }
  42. }
  43. class SpreadVariable extends Variable {}
  44. class VariableDeclarationVariable extends Variable {
  45. toString() {
  46. return `var ${this._id}, `;
  47. }
  48. }
  49. class TemplateContext {
  50. constructor() {
  51. this._matches = new Multimap();
  52. }
  53. _pushVariableMatch(variableId, node) {
  54. this._matches.set(variableId, node);
  55. }
  56. getMatches(variable) {
  57. return this._matches.get(variable._id) || [];
  58. }
  59. getMatch(variable) {
  60. return this.getMatches(variable)[0];
  61. }
  62. }
  63. class Template {
  64. constructor(ast, options, templates) {
  65. if (typeof ast === 'string') {
  66. let { babelOptions = {}, ...parserOptions } = options.parserOptions || {};
  67. parserOptions = {
  68. ecmaVersion: 2018,
  69. requireConfigFile: false,
  70. babelOptions: {
  71. configFile: false,
  72. ...babelOptions,
  73. },
  74. ...parserOptions,
  75. };
  76. const { body } = parse(ast, parserOptions);
  77. if (body.length !== 1) {
  78. throw new Error('Template must contain a single top-level AST node.');
  79. }
  80. const [ firstNode ] = body;
  81. ast = firstNode.type === 'ExpressionStatement' ? firstNode.expression : firstNode;
  82. }
  83. this._id = gensym();
  84. this._ast = ast;
  85. this._templates = templates;
  86. }
  87. narrow(selector, targetMatchIndex = 0) {
  88. const ast = query(this._ast, selector)[targetMatchIndex];
  89. const template = new Template(ast, null, this._templates);
  90. this._templates._registerTemplate(template);
  91. return template;
  92. }
  93. toString() {
  94. return this._id;
  95. }
  96. }
  97. class TemplateManager {
  98. constructor(options = {}) {
  99. this._options = options;
  100. this._variables = new Map();
  101. this._templates = new Map();
  102. }
  103. _matchTemplate(handler, template, node, ...rest) {
  104. template.context = new TemplateContext();
  105. if (this._nodeMatches(template._ast, node, template.context)) {
  106. return handler(node, ...rest);
  107. }
  108. template.context = null;
  109. }
  110. _getNodeVariable(templateNode) {
  111. if (templateNode.type === 'Identifier') {
  112. return this._variables.get(templateNode.name);
  113. }
  114. if (
  115. templateNode.type === 'VariableDeclaration'
  116. && templateNode.kind === 'var'
  117. && templateNode.declarations.length > 1
  118. ) {
  119. const [ firstDeclarator ] = templateNode.declarations;
  120. return this._variables.get(firstDeclarator.id.name);
  121. }
  122. return null;
  123. }
  124. _getSpreadVariableNode(templateNode) {
  125. if (templateNode.type === 'ExpressionStatement') {
  126. templateNode = templateNode.expression;
  127. }
  128. if (!this._getNodeVariable(templateNode)) {
  129. return undefined;
  130. }
  131. const variable = this._variables.get(templateNode.name);
  132. return variable instanceof SpreadVariable
  133. ? templateNode
  134. : undefined;
  135. }
  136. _nodeMatches(templateNode, node, context) {
  137. if (!templateNode || !node) {
  138. return (
  139. (templateNode === null || templateNode === undefined)
  140. && (node === null || node === undefined)
  141. );
  142. }
  143. const variable = this._getNodeVariable(templateNode);
  144. if (variable) {
  145. if (variable instanceof VariableDeclarationVariable) {
  146. if (node.type !== 'VariableDeclaration') {
  147. return false;
  148. }
  149. }
  150. const previousMatches = context.getMatches(variable);
  151. if (previousMatches.every(previousMatchNode => nodeEquals(previousMatchNode, node))) {
  152. if (variable instanceof VariableDeclarationVariable) {
  153. const templateDeclarations = templateNode.declarations.slice(1);
  154. if (!this._nodeArraysMatch(templateDeclarations, node.declarations, context)) {
  155. return false;
  156. }
  157. }
  158. context._pushVariableMatch(variable._id, node);
  159. return true;
  160. }
  161. return false;
  162. }
  163. const { visitorKeys, equalityKeys } = getMatchKeys(templateNode);
  164. const matches = visitorKeys.every(key => {
  165. return this._nodePropertyMatches(key, templateNode, node, context);
  166. }) && equalityKeys.every(key => {
  167. return nodesPropertiesEqual(templateNode, node, key);
  168. });
  169. return matches;
  170. }
  171. _spreadVariableMatches(templateNode, nodes, context) {
  172. const variable = this._variables.get(templateNode.name);
  173. const previousMatches = context.getMatches(variable);
  174. if (previousMatches.every(previousMatchNodes => everyNodeEquals(previousMatchNodes, nodes))) {
  175. context._pushVariableMatch(templateNode.name, nodes);
  176. return true;
  177. }
  178. return false;
  179. }
  180. _nodePropertyMatches(key, templateNode, node, context) {
  181. if (Array.isArray(templateNode[key])) {
  182. return this._nodeArraysMatch(templateNode[key], node[key], context);
  183. }
  184. return this._nodeMatches(templateNode[key], node[key], context);
  185. }
  186. _nodeArraysMatch(templateNodes, nodes, context) {
  187. if (!nodes) {
  188. return false;
  189. }
  190. if (templateNodes.length === 1) {
  191. const spreadVariableNode = this._getSpreadVariableNode(templateNodes[0]);
  192. if (spreadVariableNode) {
  193. return this._spreadVariableMatches(spreadVariableNode, nodes, context);
  194. }
  195. }
  196. return templateNodes.length === nodes.length
  197. && templateNodes.every((x, i) => this._nodeMatches(x, nodes[i], context));
  198. }
  199. variable() {
  200. const variable = new Variable();
  201. this._variables.set(variable._id, variable);
  202. return variable;
  203. }
  204. spreadVariable() {
  205. const variable = new SpreadVariable();
  206. this._variables.set(variable._id, variable);
  207. return variable;
  208. }
  209. variableDeclarationVariable() {
  210. const variable = new VariableDeclarationVariable();
  211. this._variables.set(variable._id, variable);
  212. return variable;
  213. }
  214. template(strings, ...vars) {
  215. const source = typeof strings === 'string'
  216. ? strings
  217. : strings.map((string, i) => string + (vars[i] || '')).join('');
  218. const template = new Template(source, this._options, this);
  219. this._registerTemplate(template);
  220. return template;
  221. }
  222. _registerTemplate(template) {
  223. this._templates.set(template._id, template);
  224. }
  225. visitor(visitor) {
  226. const newVisitor = {};
  227. for (const key of Object.keys(visitor)) {
  228. const value = visitor[key];
  229. const template = this._templates.get(key);
  230. const newKey = template ? template._ast.type : key;
  231. const newValue = template ? (...args) => {
  232. return this._matchTemplate(value, template, ...args);
  233. } : value;
  234. newVisitor[newKey] = newVisitor[newKey] || [];
  235. newVisitor[newKey].push(newValue);
  236. }
  237. for (const newKey of Object.keys(newVisitor)) {
  238. const newValue = newVisitor[newKey];
  239. newVisitor[newKey] = newValue.length === 1 ? newValue[0] : (...args) => {
  240. newValue.forEach(handler => handler(...args));
  241. };
  242. }
  243. return newVisitor;
  244. }
  245. }
  246. const eslintTemplateVisitor = options => new TemplateManager(options);
  247. module.exports = eslintTemplateVisitor;