index.test.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515
  1. const test = require('ava');
  2. const sinon = require('sinon');
  3. const { omit, times } = require('ramda');
  4. const { default: fuzzProgram, FuzzerState } = require('shift-fuzzer');
  5. const { default: shiftCodegen, FormattedCodeGen } = require('shift-codegen');
  6. const { parse: babelEslintParse } = require('@babel/eslint-parser');
  7. const seedrandom = require('seedrandom');
  8. const shiftToEspreeSafe = require('../test/_shift-to-espree-safe');
  9. const dropExtraTopLevelNodes = require('../test/_drop-extra-top-level-nodes');
  10. const babelEslintWorkarounds = require('../test/_babel-eslint-parser-workarounds');
  11. const { parse, PARSER } = require('../test/_parser');
  12. const recurse = require('./recurse');
  13. const eslintTemplateVisitor = require('.');
  14. const SEED = process.env.SEED || Math.random().toString(16).slice(2);
  15. console.log(`
  16. Reproduce the randomized fuzzing test by running:
  17. \`\`\`bash
  18. SEED=${JSON.stringify(SEED)} npm test
  19. \`\`\`
  20. `);
  21. const parserOptions = {
  22. sourceType: 'module',
  23. ecmaVersion: 2018,
  24. requireConfigFile: false,
  25. };
  26. test.beforeEach(t => {
  27. t.context.rng = seedrandom(SEED);
  28. });
  29. test('mixing templates into a visitor', t => {
  30. const templates = eslintTemplateVisitor();
  31. const a = templates.variable();
  32. const template = templates.template`${a}.parentNode.removeChild(${a})`;
  33. const ast = parse(`
  34. foo.parentNode.removeChild(foo);
  35. foo.parentNode.removeChild(bar);
  36. `, parserOptions);
  37. const visitorA = {
  38. [template]: sinon.spy(),
  39. CallExpression: sinon.spy(),
  40. MemberExpression: sinon.spy(),
  41. };
  42. const visitorB = {
  43. [template]: sinon.spy(),
  44. MemberExpression: sinon.spy(),
  45. };
  46. recurse.visit(ast, visitorA);
  47. recurse.visit(ast, templates.visitor(visitorB));
  48. t.false(visitorA[template].called);
  49. t.true(visitorA.CallExpression.called);
  50. t.true(visitorA.MemberExpression.called);
  51. t.true(visitorB[template].called);
  52. t.true(visitorB.MemberExpression.called);
  53. t.deepEqual(
  54. visitorA.MemberExpression.getCalls().map(call => call.args),
  55. visitorB.MemberExpression.getCalls().map(call => call.args),
  56. );
  57. t.deepEqual(
  58. visitorA.CallExpression.getCalls().map(call => call.args).slice(0, 1),
  59. visitorB[template].getCalls().map(call => call.args),
  60. );
  61. });
  62. test('variable matching', t => {
  63. const templates = eslintTemplateVisitor();
  64. const a = templates.variable();
  65. const template = templates.template`${a}.foo()`;
  66. const visitor = {
  67. [template]: sinon.spy(),
  68. };
  69. recurse.visit(parse('foo.bar()', parserOptions), templates.visitor(visitor));
  70. t.false(visitor[template].called);
  71. recurse.visit(parse('bar.foo()', parserOptions), templates.visitor(visitor));
  72. t.true(visitor[template].called);
  73. });
  74. const templateFoundInMacro = (t, templateSource, source, expectedToMatch = true) => {
  75. const templates = eslintTemplateVisitor({ parserOptions });
  76. const template = templates.template(templateSource);
  77. const visitor = {
  78. [template]: sinon.spy(),
  79. };
  80. recurse.visit(parse(source, parserOptions), templates.visitor(visitor));
  81. t.is(visitor[template].called, expectedToMatch);
  82. };
  83. templateFoundInMacro.title = (_, templateSource, source, expectedToMatch = true) => {
  84. return `\`${templateSource}\` ${expectedToMatch ? 'should be found in' : 'should not be found in'} \`${source}\``;
  85. };
  86. const templateMatchesMacro = (t, templateSource, source, expectedToMatch = true) => {
  87. const wrap = s => `uniqueEnoughIdentifier((${s}))`;
  88. templateFoundInMacro(t, wrap(templateSource), wrap(source), expectedToMatch);
  89. };
  90. templateMatchesMacro.title = (_, templateSource, source, expectedToMatch = true) => {
  91. return `\`${templateSource}\` ${expectedToMatch ? 'should match' : 'should not match'} \`${source}\``;
  92. };
  93. test(templateMatchesMacro, 'foo', 'bar', false);
  94. test(templateMatchesMacro, 'foo', 'foo');
  95. test(templateMatchesMacro, 'foo.bar', 'foo.bar');
  96. test(templateMatchesMacro, 'foo.bar', 'foo["bar"]', false);
  97. test(templateMatchesMacro, 'foo.bar', 'foo.quz', false);
  98. test(templateMatchesMacro, 'foo.bar', 'quz.bar', false);
  99. test(templateMatchesMacro, 'foo.bar()', 'foo.bar()');
  100. test(templateMatchesMacro, 'foo.bar()', 'foo["bar"]()', false);
  101. test(templateMatchesMacro, 'foo.bar()', 'foo.quz()', false);
  102. test(templateMatchesMacro, 'foo.bar()', 'quz.bar()', false);
  103. test(templateFoundInMacro, 'x', '[a, b, c]', false);
  104. test(templateFoundInMacro, 'b', '[a, b, c]');
  105. test(templateMatchesMacro, '1', '2', false);
  106. test(templateMatchesMacro, '1', '1');
  107. test(templateFoundInMacro, '9', '[1, 2, 3]', false);
  108. test(templateFoundInMacro, '2', '[1, 2, 3]');
  109. test(templateFoundInMacro, '({})', '({a:[]})', false);
  110. test(templateFoundInMacro, '({})', '[{}]');
  111. test(templateMatchesMacro, '(() => {})', '(function() {})', false);
  112. test(templateMatchesMacro, '(( ) => { })', '(()=>{})');
  113. test(templateMatchesMacro, 'NaN', '-NaN', false);
  114. test(templateMatchesMacro, 'NaN', 'NaN');
  115. test(templateFoundInMacro, 'NaN', 'NaN');
  116. test(templateFoundInMacro, 'NaN', '-NaN');
  117. test(templateFoundInMacro, '-NaN', '+NaN', false);
  118. test(templateFoundInMacro, '+NaN', '-NaN', false);
  119. test(templateMatchesMacro, '/a/', '/a/g', false);
  120. test(templateMatchesMacro, '/a/', '/a/');
  121. test(templateFoundInMacro, '/x/', 'foo(/x/)');
  122. test(templateFoundInMacro, '/x/', 'foo(/x/y)', false);
  123. test(templateMatchesMacro, '0', '+0', false);
  124. test(templateMatchesMacro, '0', '-0', false);
  125. test(templateMatchesMacro, '0', '0');
  126. test(templateFoundInMacro, '0', '+0');
  127. test(templateFoundInMacro, '0', '-0');
  128. test(templateFoundInMacro, '0', '0');
  129. test(templateFoundInMacro, '-0', '0', false);
  130. test(templateFoundInMacro, '+0', '0', false);
  131. test('variable values', t => {
  132. t.plan(6);
  133. const templates = eslintTemplateVisitor();
  134. const receiver = templates.variable();
  135. const method = templates.variable();
  136. const template = templates.template`${receiver}.${method}()`;
  137. const visitor = {
  138. [template](node) {
  139. const receiverNode = template.context.getMatch(receiver);
  140. const methodNode = template.context.getMatch(method);
  141. t.is(node.type, 'CallExpression');
  142. t.is(node.arguments.length, 0);
  143. t.is(receiverNode.type, 'Identifier');
  144. t.is(receiverNode.name, 'bar');
  145. t.is(methodNode.type, 'Identifier');
  146. t.is(methodNode.name, 'foo');
  147. },
  148. };
  149. // Should match
  150. recurse.visit(parse('bar.foo()', parserOptions), templates.visitor(visitor));
  151. // Should not match
  152. recurse.visit(parse('bar.foo(argument)', parserOptions), templates.visitor(visitor));
  153. recurse.visit(parse('bar.foo(...arguments)', parserOptions), templates.visitor(visitor));
  154. });
  155. test('`spreadVariable` matching arguments', t => {
  156. const templates = eslintTemplateVisitor();
  157. const argumentsVariable = templates.spreadVariable();
  158. const template = templates.template`receiver.method(${argumentsVariable})`;
  159. const recordedArguments = [];
  160. const visitor = {
  161. [template](node) {
  162. const argumentNodes = template.context.getMatch(argumentsVariable);
  163. recordedArguments.push(argumentNodes);
  164. t.is(node.type, 'CallExpression');
  165. t.is(node.arguments, argumentNodes);
  166. },
  167. };
  168. recurse.visit(parse('receiver.method()', parserOptions), templates.visitor(visitor));
  169. t.is(recordedArguments.length, 1);
  170. t.deepEqual(recordedArguments[0], []);
  171. recurse.visit(parse('receiver.method(onlyArgument)', parserOptions), templates.visitor(visitor));
  172. t.is(recordedArguments.length, 2);
  173. t.is(recordedArguments[1].length, 1);
  174. recurse.visit(parse('receiver.method(argument1, argument2)', parserOptions), templates.visitor(visitor));
  175. t.is(recordedArguments.length, 3);
  176. t.is(recordedArguments[2].length, 2);
  177. recurse.visit(parse('receiver.method(...arguments)', parserOptions), templates.visitor(visitor));
  178. t.is(recordedArguments.length, 4);
  179. t.is(recordedArguments[3].length, 1);
  180. t.is(recordedArguments[3][0].type, 'SpreadElement');
  181. });
  182. test('`spreadVariable` matching statements', t => {
  183. const templates = eslintTemplateVisitor({ parserOptions });
  184. const statementsVariable = templates.spreadVariable();
  185. const template = templates.template`() => {${statementsVariable}}`;
  186. const recordedStatements = [];
  187. const visitor = {
  188. [template](node) {
  189. const statementNodes = template.context.getMatch(statementsVariable);
  190. recordedStatements.push(statementNodes);
  191. t.is(node.type, 'ArrowFunctionExpression');
  192. t.is(node.body.type, 'BlockStatement');
  193. t.is(node.body.body, statementNodes);
  194. },
  195. };
  196. recurse.visit(parse('() => {}', parserOptions), templates.visitor(visitor));
  197. t.is(recordedStatements.length, 1);
  198. t.deepEqual(recordedStatements[0], []);
  199. recurse.visit(parse('() => { onlyStatement; }', parserOptions), templates.visitor(visitor));
  200. t.is(recordedStatements.length, 2);
  201. t.is(recordedStatements[1].length, 1);
  202. recurse.visit(parse('() => { statement1; statement2 }', parserOptions), templates.visitor(visitor));
  203. t.is(recordedStatements.length, 3);
  204. t.is(recordedStatements[2].length, 2);
  205. });
  206. test('`variableDeclarationVariable` matching variable declarations', t => {
  207. const templates = eslintTemplateVisitor({ parserOptions });
  208. const variableDeclarationVariable = templates.variableDeclarationVariable();
  209. const template = templates.template`() => {
  210. ${variableDeclarationVariable} x = y;
  211. }`;
  212. const recordedVariableDeclarations = [];
  213. const visitor = {
  214. [template](node) {
  215. const variableDeclarationNode = template.context.getMatch(variableDeclarationVariable);
  216. recordedVariableDeclarations.push(variableDeclarationNode);
  217. t.is(node.type, 'ArrowFunctionExpression');
  218. t.is(node.body.type, 'BlockStatement');
  219. t.deepEqual(node.body.body[0], variableDeclarationNode);
  220. },
  221. };
  222. recurse.visit(parse('() => {}', parserOptions), templates.visitor(visitor));
  223. t.deepEqual(recordedVariableDeclarations, []);
  224. recurse.visit(parse('() => { x = y; }', parserOptions), templates.visitor(visitor));
  225. t.deepEqual(recordedVariableDeclarations, []);
  226. recurse.visit(parse('() => { const x = y; }', parserOptions), templates.visitor(visitor));
  227. t.is(recordedVariableDeclarations.length, 1);
  228. t.is(recordedVariableDeclarations[0].kind, 'const');
  229. recurse.visit(parse('() => { let x = y; }', parserOptions), templates.visitor(visitor));
  230. t.is(recordedVariableDeclarations.length, 2);
  231. t.is(recordedVariableDeclarations[1].kind, 'let');
  232. recurse.visit(parse('() => { var x = y; }', parserOptions), templates.visitor(visitor));
  233. t.is(recordedVariableDeclarations.length, 3);
  234. t.is(recordedVariableDeclarations[2].kind, 'var');
  235. });
  236. test('`variableDeclarationVariable` unification', t => {
  237. const templates = eslintTemplateVisitor({ parserOptions });
  238. const variableDeclarationVariable = templates.variableDeclarationVariable();
  239. const variableVariable = templates.variable();
  240. const template = templates.template`{
  241. ${variableDeclarationVariable} ${variableVariable};
  242. ${variableDeclarationVariable} ${variableVariable};
  243. }`;
  244. const recordedVariableDeclarations = [];
  245. const recordedVariables = [];
  246. const visitor = {
  247. [template](node) {
  248. const variableDeclarationNode = template.context.getMatch(variableDeclarationVariable);
  249. const variableNode = template.context.getMatch(variableVariable);
  250. recordedVariableDeclarations.push(variableDeclarationNode);
  251. recordedVariables.push(variableNode);
  252. t.is(node.type, 'BlockStatement');
  253. },
  254. };
  255. recurse.visit(parse('() => { var x; var x; }', parserOptions), templates.visitor(visitor));
  256. t.is(recordedVariableDeclarations.length, 1);
  257. t.is(recordedVariableDeclarations[0].kind, 'var');
  258. t.is(recordedVariables.length, 1);
  259. t.is(recordedVariables[0].type, 'Identifier');
  260. t.is(recordedVariables[0].name, 'x');
  261. });
  262. const omitLocation = omit([
  263. 'start',
  264. 'end',
  265. 'loc',
  266. 'range',
  267. ]);
  268. test('variable unification', t => {
  269. t.plan(6);
  270. const templates = eslintTemplateVisitor();
  271. const x = templates.variable();
  272. const template = templates.template`${x} + ${x}`;
  273. const visitor = {
  274. [template](node) {
  275. t.is(node.type, 'BinaryExpression');
  276. const xNodes = template.context.getMatches(x);
  277. t.is(xNodes.length, 2);
  278. const [ x1, x2 ] = xNodes;
  279. t.is(x1.type, 'Identifier');
  280. t.is(x1.name, 'foo');
  281. t.not(x1, x2);
  282. t.deepEqual(omitLocation(x1), omitLocation(x2));
  283. },
  284. };
  285. // Should match
  286. recurse.visit(parse('foo + foo', parserOptions), templates.visitor(visitor));
  287. // Should not match
  288. recurse.visit(parse('foo + bar', parserOptions), templates.visitor(visitor));
  289. recurse.visit(parse('bar + foo', parserOptions), templates.visitor(visitor));
  290. });
  291. test('throws on multiple top-level nodes in a template', t => {
  292. const templates = eslintTemplateVisitor({ parserOptions });
  293. t.throws(() => {
  294. return templates.template`a; b;`;
  295. });
  296. });
  297. test('narrow to a statement with await', t => {
  298. const templates = eslintTemplateVisitor({ parserOptions });
  299. const template = templates.template`
  300. async () => { await 1; }
  301. `.narrow('BlockStatement > :has(AwaitExpression)');
  302. const source = 'async () => { await 0; await 1; await 2; }';
  303. const visitor = {
  304. [template]: sinon.spy(),
  305. };
  306. recurse.visit(parse(source, parserOptions), templates.visitor(visitor));
  307. t.true(visitor[template].called);
  308. t.is(visitor[template].callCount, 1);
  309. });
  310. if (PARSER === '@babel/eslint-parser') {
  311. test('dynamic import', t => {
  312. const templates = eslintTemplateVisitor({ parserOptions });
  313. const template = templates.template`import('foo')`;
  314. const source = 'async () => { await import(\'bar\'); await import(\'foo\'); await import(\'qux\'); }';
  315. const visitor = {
  316. [template]: sinon.spy(),
  317. };
  318. recurse.visit(parse(source, parserOptions), templates.visitor(visitor));
  319. t.true(visitor[template].called);
  320. t.is(visitor[template].callCount, 1);
  321. });
  322. }
  323. test('fuzzing', t => {
  324. const { rng } = t.context;
  325. const templates = eslintTemplateVisitor({ parserOptions });
  326. const totalTests = 2 ** 13;
  327. let skippedTests = 0;
  328. times(() => {
  329. const randomShiftAST = fuzzProgram(new FuzzerState({ rng, maxDepth: 3 }));
  330. const randomEspreeSafeShiftAST = shiftToEspreeSafe(dropExtraTopLevelNodes(randomShiftAST));
  331. const randomJS = shiftCodegen(randomEspreeSafeShiftAST, new FormattedCodeGen()) || '"empty program";';
  332. try {
  333. const { shouldSkip } = babelEslintWorkarounds(babelEslintParse(randomJS, parserOptions));
  334. if (shouldSkip) {
  335. console.warn('Ignored a random script due to a `@babel/eslint-parser` bug (this is fine):', randomJS);
  336. skippedTests += 1;
  337. return;
  338. }
  339. } catch (error) {
  340. if (error.name === 'SyntaxError') {
  341. // TODO: `shiftToEspreeSafe` or `fuzzProgram` should do a better job ensuring program is valid
  342. console.warn('Ignored error (this is fine):', error.name + ':', error.message);
  343. skippedTests += 1;
  344. return;
  345. }
  346. throw error;
  347. }
  348. const randomTemplate = templates.template(randomJS);
  349. const randomAST = parse(randomJS, parserOptions);
  350. const visitor = {
  351. [randomTemplate]: sinon.spy(),
  352. };
  353. recurse.visit(randomAST, templates.visitor(visitor));
  354. const { called } = visitor[randomTemplate];
  355. t.true(called);
  356. if (!called) {
  357. console.dir({
  358. randomJS,
  359. randomEspreeSafeShiftAST,
  360. randomAST,
  361. }, { depth: null });
  362. throw new Error('Fuzzing test failed. This error is thrown just to stop this long test early.');
  363. }
  364. }, totalTests);
  365. console.log({
  366. skippedTests,
  367. totalTests,
  368. });
  369. });