detect-acorn.cjs 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. 'use strict';
  2. const acorn = require('acorn');
  3. const estreeWalker = require('estree-walker');
  4. const addons = require('../shared/unimport.a78aa044.cjs');
  5. require('pathe');
  6. require('mlly');
  7. require('magic-string');
  8. require('strip-literal');
  9. async function detectImportsAcorn(code, ctx, options) {
  10. const s = addons.getMagicString(code);
  11. const map = await ctx.getImportMap();
  12. let matchedImports = [];
  13. const enableAutoImport = options?.autoImport !== false;
  14. const enableTransformVirtualImports = options?.transformVirtualImports !== false && ctx.options.virtualImports?.length;
  15. if (enableAutoImport || enableTransformVirtualImports) {
  16. const ast = acorn.parse(s.original, {
  17. sourceType: "module",
  18. ecmaVersion: "latest",
  19. locations: true
  20. });
  21. const occurrenceMap = /* @__PURE__ */ new Map();
  22. const virtualImports = createVirtualImportsAcronWalker(map, ctx.options.virtualImports);
  23. const scopes = traveseScopes(
  24. ast,
  25. enableTransformVirtualImports ? virtualImports.walk : {}
  26. );
  27. if (enableAutoImport) {
  28. const identifiers = new Set(occurrenceMap.keys());
  29. matchedImports.push(
  30. ...Array.from(scopes.unmatched).map((name) => {
  31. const item = map.get(name);
  32. if (item && !item.disabled)
  33. return item;
  34. occurrenceMap.delete(name);
  35. return null;
  36. }).filter(Boolean)
  37. );
  38. for (const addon of ctx.addons)
  39. matchedImports = await addon.matchImports?.call(ctx, identifiers, matchedImports) || matchedImports;
  40. }
  41. virtualImports.ranges.forEach(([start, end]) => {
  42. s.remove(start, end);
  43. });
  44. matchedImports.push(...virtualImports.imports);
  45. }
  46. return {
  47. s,
  48. strippedCode: code.toString(),
  49. matchedImports,
  50. isCJSContext: false,
  51. firstOccurrence: 0
  52. // TODO:
  53. };
  54. }
  55. function traveseScopes(ast, additionalWalk) {
  56. const scopes = [];
  57. let scopeCurrent = void 0;
  58. const scopesStack = [];
  59. function pushScope(node) {
  60. scopeCurrent = {
  61. node,
  62. parent: scopeCurrent,
  63. declarations: /* @__PURE__ */ new Set(),
  64. references: /* @__PURE__ */ new Set()
  65. };
  66. scopes.push(scopeCurrent);
  67. scopesStack.push(scopeCurrent);
  68. }
  69. function popScope(node) {
  70. const scope = scopesStack.pop();
  71. if (scope?.node !== node)
  72. throw new Error("Scope mismatch");
  73. scopeCurrent = scopesStack[scopesStack.length - 1];
  74. }
  75. pushScope(void 0);
  76. estreeWalker.walk(ast, {
  77. enter(node, parent, prop, index) {
  78. additionalWalk?.enter?.call(this, node, parent, prop, index);
  79. switch (node.type) {
  80. case "ImportSpecifier":
  81. case "ImportDefaultSpecifier":
  82. case "ImportNamespaceSpecifier":
  83. scopeCurrent.declarations.add(node.local.name);
  84. return;
  85. case "FunctionDeclaration":
  86. case "ClassDeclaration":
  87. if (node.id)
  88. scopeCurrent.declarations.add(node.id.name);
  89. return;
  90. case "VariableDeclarator":
  91. if (node.id.type === "Identifier") {
  92. scopeCurrent.declarations.add(node.id.name);
  93. } else {
  94. estreeWalker.walk(node.id, {
  95. enter(node2) {
  96. if (node2.type === "ObjectPattern") {
  97. node2.properties.forEach((i) => {
  98. if (i.type === "Property" && i.value.type === "Identifier")
  99. scopeCurrent.declarations.add(i.value.name);
  100. else if (i.type === "RestElement" && i.argument.type === "Identifier")
  101. scopeCurrent.declarations.add(i.argument.name);
  102. });
  103. } else if (node2.type === "ArrayPattern") {
  104. node2.elements.forEach((i) => {
  105. if (i?.type === "Identifier")
  106. scopeCurrent.declarations.add(i.name);
  107. if (i?.type === "RestElement" && i.argument.type === "Identifier")
  108. scopeCurrent.declarations.add(i.argument.name);
  109. });
  110. }
  111. }
  112. });
  113. }
  114. return;
  115. case "BlockStatement":
  116. pushScope(node);
  117. return;
  118. case "Identifier":
  119. switch (parent?.type) {
  120. case "CallExpression":
  121. if (parent.callee === node || parent.arguments.includes(node))
  122. scopeCurrent.references.add(node.name);
  123. return;
  124. case "MemberExpression":
  125. if (parent.object === node)
  126. scopeCurrent.references.add(node.name);
  127. return;
  128. case "VariableDeclarator":
  129. if (parent.init === node)
  130. scopeCurrent.references.add(node.name);
  131. return;
  132. case "SpreadElement":
  133. if (parent.argument === node)
  134. scopeCurrent.references.add(node.name);
  135. return;
  136. case "ClassDeclaration":
  137. if (parent.superClass === node)
  138. scopeCurrent.references.add(node.name);
  139. return;
  140. case "Property":
  141. if (parent.value === node)
  142. scopeCurrent.references.add(node.name);
  143. return;
  144. case "TemplateLiteral":
  145. if (parent.expressions.includes(node))
  146. scopeCurrent.references.add(node.name);
  147. return;
  148. case "AssignmentExpression":
  149. if (parent.right === node)
  150. scopeCurrent.references.add(node.name);
  151. return;
  152. case "IfStatement":
  153. case "WhileStatement":
  154. case "DoWhileStatement":
  155. if (parent.test === node)
  156. scopeCurrent.references.add(node.name);
  157. return;
  158. case "SwitchStatement":
  159. if (parent.discriminant === node)
  160. scopeCurrent.references.add(node.name);
  161. return;
  162. }
  163. if (parent?.type.includes("Expression"))
  164. scopeCurrent.references.add(node.name);
  165. }
  166. },
  167. leave(node, parent, prop, index) {
  168. additionalWalk?.leave?.call(this, node, parent, prop, index);
  169. switch (node.type) {
  170. case "BlockStatement":
  171. popScope(node);
  172. }
  173. }
  174. });
  175. const unmatched = /* @__PURE__ */ new Set();
  176. for (const scope of scopes) {
  177. for (const name of scope.references) {
  178. let defined = false;
  179. let parent = scope;
  180. while (parent) {
  181. if (parent.declarations.has(name)) {
  182. defined = true;
  183. break;
  184. }
  185. parent = parent?.parent;
  186. }
  187. if (!defined)
  188. unmatched.add(name);
  189. }
  190. }
  191. return {
  192. unmatched,
  193. scopes
  194. };
  195. }
  196. function createVirtualImportsAcronWalker(importMap, virtualImports = []) {
  197. const imports = [];
  198. const ranges = [];
  199. return {
  200. imports,
  201. ranges,
  202. walk: {
  203. enter(node) {
  204. if (node.type === "ImportDeclaration") {
  205. if (virtualImports.includes(node.source.value)) {
  206. ranges.push([node.start, node.end]);
  207. node.specifiers.forEach((i) => {
  208. if (i.type === "ImportSpecifier") {
  209. const original = importMap.get(i.imported.name);
  210. if (!original)
  211. throw new Error(`[unimport] failed to find "${i.imported.name}" imported from "${node.source.value}"`);
  212. imports.push({
  213. from: original.from,
  214. name: original.name,
  215. as: i.local.name
  216. });
  217. }
  218. });
  219. }
  220. }
  221. }
  222. }
  223. };
  224. }
  225. exports.createVirtualImportsAcronWalker = createVirtualImportsAcronWalker;
  226. exports.detectImportsAcorn = detectImportsAcorn;
  227. exports.traveseScopes = traveseScopes;