ScopeAnalyzer.ts 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. import { injectable, } from 'inversify';
  2. import * as eslintScope from 'eslint-scope';
  3. import * as estraverse from 'estraverse';
  4. import * as ESTree from 'estree';
  5. import { IScopeAnalyzer } from '../../interfaces/analyzers/scope-analyzer/IScopeAnalyzer';
  6. import { ecmaVersion } from '../../constants/EcmaVersion';
  7. import { NodeGuards } from '../../node/NodeGuards';
  8. @injectable()
  9. export class ScopeAnalyzer implements IScopeAnalyzer {
  10. /**
  11. * @type {eslintScope.AnalysisOptions}
  12. */
  13. private static readonly eslintScopeOptions: eslintScope.AnalysisOptions = {
  14. ecmaVersion,
  15. optimistic: true
  16. };
  17. /**
  18. * @type {acorn.Options['sourceType'][]}
  19. */
  20. private static readonly sourceTypes: acorn.Options['sourceType'][] = [
  21. 'script',
  22. 'module'
  23. ];
  24. /**
  25. * @type {number}
  26. */
  27. private static readonly emptyRangeValue: number = 0;
  28. /**
  29. * @type {eslintScope.ScopeManager | null}
  30. */
  31. private scopeManager: eslintScope.ScopeManager | null = null;
  32. /**
  33. * `eslint-scope` reads `ranges` property of a nodes
  34. * Should attach that property to the some custom nodes
  35. *
  36. * @param {Node} astTree
  37. */
  38. private static attachMissingRanges (astTree: ESTree.Node): void {
  39. estraverse.replace(astTree, {
  40. enter: (node: ESTree.Node): ESTree.Node => {
  41. if (!node.range) {
  42. node.range = [
  43. node.parentNode?.range?.[0] ?? ScopeAnalyzer.emptyRangeValue,
  44. node.parentNode?.range?.[1] ?? ScopeAnalyzer.emptyRangeValue
  45. ];
  46. }
  47. return node;
  48. }
  49. });
  50. }
  51. /**
  52. * @param {Node} node
  53. * @returns {boolean}
  54. */
  55. private static isRootNode (node: ESTree.Node): boolean {
  56. return NodeGuards.isProgramNode(node) || node.parentNode === node;
  57. }
  58. /**
  59. * @param {Program} astTree
  60. */
  61. public analyze (astTree: ESTree.Node): void {
  62. const sourceTypeLength: number = ScopeAnalyzer.sourceTypes.length;
  63. ScopeAnalyzer.attachMissingRanges(astTree);
  64. for (let i: number = 0; i < sourceTypeLength; i++) {
  65. try {
  66. this.scopeManager = eslintScope.analyze(astTree, {
  67. ...ScopeAnalyzer.eslintScopeOptions,
  68. sourceType: ScopeAnalyzer.sourceTypes[i]
  69. });
  70. return;
  71. } catch (error) {
  72. if (i < sourceTypeLength - 1) {
  73. continue;
  74. }
  75. throw new Error(error);
  76. }
  77. }
  78. throw new Error('Scope analyzing error');
  79. }
  80. /**
  81. * @param {Node} node
  82. * @returns {Scope}
  83. */
  84. public acquireScope (node: ESTree.Node): eslintScope.Scope {
  85. if (!this.scopeManager) {
  86. throw new Error('Scope manager is not defined');
  87. }
  88. const scope: eslintScope.Scope | null = this.scopeManager.acquire(
  89. node,
  90. ScopeAnalyzer.isRootNode(node)
  91. );
  92. if (!scope) {
  93. throw new Error('Cannot acquire scope for node');
  94. }
  95. this.sanitizeScopes(scope);
  96. return scope;
  97. }
  98. /**
  99. * @param {Scope} scope
  100. */
  101. private sanitizeScopes (scope: eslintScope.Scope): void {
  102. scope.childScopes.forEach((childScope: eslintScope.Scope) => {
  103. // fix of class scopes
  104. // trying to move class scope references to the parent scope
  105. if (childScope.type === 'class' && childScope.upper) {
  106. if (!childScope.variables.length) {
  107. return;
  108. }
  109. // class name variable is always first
  110. const classNameVariable: eslintScope.Variable = childScope.variables[0];
  111. const upperVariable: eslintScope.Variable | undefined = childScope.upper.variables
  112. .find((variable: eslintScope.Variable) => {
  113. const isValidClassNameVariable: boolean = classNameVariable.defs
  114. .some((definition: eslintScope.Definition) => definition.type === 'ClassName');
  115. return isValidClassNameVariable && variable.name === classNameVariable.name;
  116. });
  117. upperVariable?.references.push(...childScope.variables[0].references);
  118. }
  119. });
  120. for (const childScope of scope.childScopes) {
  121. this.sanitizeScopes(childScope);
  122. }
  123. }
  124. }