detect-acorn.cjs 7.7 KB

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