detect-acorn.mjs 7.5 KB

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