scopetools.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. // scopetools.js
  2. // MIT licensed, see LICENSE file
  3. // Copyright (c) 2013-2015 Olov Lassus <olov.lassus@gmail.com>
  4. "use strict";
  5. const assert = require("assert");
  6. const traverse = require("ordered-ast-traverse");
  7. const Scope = require("./scope");
  8. const is = require("simple-is");
  9. module.exports = {
  10. setupScopeAndReferences: setupScopeAndReferences,
  11. isReference: isReference,
  12. };
  13. function setupScopeAndReferences(root) {
  14. traverse(root, {pre: createScopes});
  15. createTopScope(root.$scope);
  16. }
  17. function createScopes(node, parent) {
  18. node.$parent = parent;
  19. node.$scope = parent ? parent.$scope : null; // may be overridden
  20. if (isNonFunctionBlock(node, parent)) {
  21. // A block node is a scope unless parent is a function
  22. node.$scope = new Scope({
  23. kind: "block",
  24. node: node,
  25. parent: parent.$scope,
  26. });
  27. } else if (node.type === "VariableDeclaration") {
  28. // Variable declarations names goes in current scope
  29. node.declarations.forEach(function(declarator) {
  30. const name = declarator.id.name;
  31. node.$scope.add(name, node.kind, declarator.id, declarator.range[1]);
  32. });
  33. } else if (isFunction(node)) {
  34. // Function is a scope, with params in it
  35. // There's no block-scope under it
  36. node.$scope = new Scope({
  37. kind: "hoist",
  38. node: node,
  39. parent: parent.$scope,
  40. });
  41. // function has a name
  42. if (node.id) {
  43. if (node.type === "FunctionDeclaration") {
  44. // Function name goes in parent scope for declared functions
  45. parent.$scope.add(node.id.name, "fun", node.id, null);
  46. } else if (node.type === "FunctionExpression") {
  47. // Function name goes in function's scope for named function expressions
  48. node.$scope.add(node.id.name, "fun", node.id, null);
  49. } else {
  50. assert(false);
  51. }
  52. }
  53. node.params.forEach(function(param) {
  54. node.$scope.add(param.name, "param", param, null);
  55. });
  56. } else if (isForWithConstLet(node) || isForInOfWithConstLet(node)) {
  57. // For(In/Of) loop with const|let declaration is a scope, with declaration in it
  58. // There may be a block-scope under it
  59. node.$scope = new Scope({
  60. kind: "block",
  61. node: node,
  62. parent: parent.$scope,
  63. });
  64. } else if (node.type === "CatchClause") {
  65. const identifier = node.param;
  66. node.$scope = new Scope({
  67. kind: "catch-block",
  68. node: node,
  69. parent: parent.$scope,
  70. });
  71. node.$scope.add(identifier.name, "caught", identifier, null);
  72. // All hoist-scope keeps track of which variables that are propagated through,
  73. // i.e. an reference inside the scope points to a declaration outside the scope.
  74. // This is used to mark "taint" the name since adding a new variable in the scope,
  75. // with a propagated name, would change the meaning of the existing references.
  76. //
  77. // catch(e) is special because even though e is a variable in its own scope,
  78. // we want to make sure that catch(e){let e} is never transformed to
  79. // catch(e){var e} (but rather var e$0). For that reason we taint the use of e
  80. // in the closest hoist-scope, i.e. where var e$0 belongs.
  81. node.$scope.closestHoistScope().markPropagates(identifier.name);
  82. } else if (node.type === "Program") {
  83. // Top-level program is a scope
  84. // There's no block-scope under it
  85. node.$scope = new Scope({
  86. kind: "hoist",
  87. node: node,
  88. parent: null,
  89. });
  90. }
  91. }
  92. function createTopScope(programScope) {
  93. function inject(obj) {
  94. for (let name in obj) {
  95. const writeable = obj[name];
  96. const kind = (writeable ? "var" : "const");
  97. if (topScope.hasOwn(name)) {
  98. topScope.remove(name);
  99. }
  100. topScope.add(name, kind, {loc: {start: {line: -1}}}, -1);
  101. }
  102. }
  103. const topScope = new Scope({
  104. kind: "hoist",
  105. node: {},
  106. parent: null,
  107. });
  108. const complementary = {
  109. undefined: false,
  110. Infinity: false,
  111. console: false,
  112. };
  113. inject(complementary);
  114. // inject(jshint_vars.reservedVars);
  115. // inject(jshint_vars.ecmaIdentifiers);
  116. // link it in
  117. programScope.parent = topScope;
  118. topScope.children.push(programScope);
  119. return topScope;
  120. }
  121. function isConstLet(kind) {
  122. return kind === "const" || kind === "let";
  123. }
  124. function isNonFunctionBlock(node, parent) {
  125. return node.type === "BlockStatement" && parent.type !== "FunctionDeclaration" && parent.type !== "FunctionExpression";
  126. }
  127. function isForWithConstLet(node) {
  128. return node.type === "ForStatement" && node.init && node.init.type === "VariableDeclaration" && isConstLet(node.init.kind);
  129. }
  130. function isForInOfWithConstLet(node) {
  131. return isForInOf(node) && node.left.type === "VariableDeclaration" && isConstLet(node.left.kind);
  132. }
  133. function isForInOf(node) {
  134. return node.type === "ForInStatement" || node.type === "ForOfStatement";
  135. }
  136. function isFunction(node) {
  137. return node.type === "FunctionDeclaration" || node.type === "FunctionExpression";
  138. }
  139. function isReference(node) {
  140. const parent = node.$parent;
  141. return node.$refToScope ||
  142. node.type === "Identifier" &&
  143. !(parent.type === "VariableDeclarator" && parent.id === node) && // var|let|const $
  144. !(parent.type === "MemberExpression" && parent.computed === false && parent.property === node) && // obj.$
  145. !(parent.type === "Property" && parent.key === node) && // {$: ...}
  146. !(parent.type === "LabeledStatement" && parent.label === node) && // $: ...
  147. !(parent.type === "CatchClause" && parent.param === node) && // catch($)
  148. !(isFunction(parent) && parent.id === node) && // function $(..
  149. !(isFunction(parent) && is.someof(node, parent.params)) && // function f($)..
  150. true;
  151. }