detect-acorn.mjs 7.4 KB

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