commonjs-exports.test.ts 2.7 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071
  1. /**
  2. * CommonJS export assignments name the function they hold (#1675).
  3. *
  4. * `exports.getItems = async (req, res) => {…}` and `module.exports.x =
  5. * function () {…}` are how Express controllers are commonly written. The
  6. * arrow is anonymous only syntactically — the export property is the name
  7. * every `router.get('/items', getItems)` resolves — so it gets the same
  8. * treatment `const getItems = () => {}` already has: a function node, exported,
  9. * with its calls attributed to it rather than to the file.
  10. */
  11. import { describe, it, expect, beforeAll } from 'vitest';
  12. import { extractFromSource } from '../src/extraction';
  13. import { initGrammars, loadAllGrammars } from '../src/extraction/grammars';
  14. beforeAll(async () => {
  15. await initGrammars();
  16. await loadAllGrammars();
  17. });
  18. const refsFrom = (result: ReturnType<typeof extractFromSource>, id: string) =>
  19. result.unresolvedReferences.filter((r) => r.fromNodeId === id).map((r) => r.referenceName);
  20. describe('CommonJS export assignments', () => {
  21. it('indexes exports.X / module.exports.X functions as exported function nodes', () => {
  22. const code = `
  23. const { findItems, removeItem } = require('./db');
  24. exports.getItems = async (req, res) => {
  25. res.json(await findItems());
  26. };
  27. module.exports.deleteItem = function (req, res) {
  28. removeItem(req.params.id);
  29. res.end();
  30. };
  31. exports.plain = 42;
  32. module.exports = { legacy: 1 };
  33. `;
  34. const result = extractFromSource('src/controller.js', code);
  35. const fns = result.nodes.filter((n) => n.kind === 'function');
  36. expect(fns.map((n) => n.name).sort()).toEqual(['deleteItem', 'getItems']);
  37. const getItems = fns.find((n) => n.name === 'getItems')!;
  38. const deleteItem = fns.find((n) => n.name === 'deleteItem')!;
  39. expect(getItems.startLine).toBe(4);
  40. expect(getItems.isExported).toBe(true);
  41. expect(deleteItem.isExported).toBe(true);
  42. expect(getItems.isAsync).toBe(true);
  43. // The handlers' calls are their own, not the file's.
  44. expect(refsFrom(result, getItems.id)).toContain('findItems');
  45. expect(refsFrom(result, deleteItem.id)).toContain('removeItem');
  46. const file = result.nodes.find((n) => n.kind === 'file')!;
  47. expect(refsFrom(result, file.id)).not.toContain('findItems');
  48. expect(refsFrom(result, file.id)).not.toContain('removeItem');
  49. // A non-function export is not a function, and nothing is left anonymous.
  50. expect(result.nodes.map((n) => n.name)).not.toContain('<anonymous>');
  51. });
  52. it('leaves other member assignments alone', () => {
  53. const code = `
  54. const handlers = {};
  55. handlers.onSave = () => { persist(); };
  56. app.locals.format = function () { return 1; };
  57. `;
  58. const result = extractFromSource('src/other.js', code);
  59. expect(result.nodes.filter((n) => n.kind === 'function')).toEqual([]);
  60. });
  61. });