Permalink
Browse files

Breaking: set `parent` of AST nodes before rules run (fixes #9122) (#…

  • Loading branch information...
not-an-aardvark committed Mar 22, 2018
1 parent 91ece32 commit 4eaebe5ae37d14dd61831443de727266bdae0908
Showing with 62 additions and 101 deletions.
  1. +1 −1 docs/developer-guide/working-with-plugins.md
  2. +40 −19 lib/linter.js
  3. +3 −5 lib/util/source-code.js
  4. +18 −37 tests/lib/linter.js
  5. +0 −39 tests/lib/util/source-code.js
@@ -213,7 +213,7 @@ All nodes must have `range` property.
* `range` (`number[]`) is an array of two numbers. Both numbers are a 0-based index which is the position in the array of source code characters. The first is the start position of the node, the second is the end position of the node. `code.slice(node.range[0], node.range[1])` must be the text of the node. This range does not include spaces/parentheses which are around the node.
* `loc` (`SourceLocation`) must not be `null`. [The `loc` property is defined as nullable by ESTree](https://github.com/estree/estree/blob/25834f7247d44d3156030f8e8a2d07644d771fdb/es5.md#node-objects), but ESLint requires this property. On the other hand, `SourceLocation#source` property can be `undefined`. ESLint does not use the `SourceLocation#source` property.
The `parent` property of all nodes must be rewriteable. ESLint sets each node's parent properties to its parent node while traversing.
The `parent` property of all nodes must be rewriteable. ESLint sets each node's `parent` property to its parent node while traversing, before any rules have access to the AST.
#### The `Program` node:
View
@@ -680,6 +680,22 @@ function createRuleListeners(rule, ruleContext) {
}
}
/**
* Gets all the ancestors of a given node
* @param {ASTNode} node The node
* @returns {ASTNode[]} All the ancestor nodes in the AST, not including the provided node, starting
* from the root node and going inwards to the parent node.
*/
function getAncestors(node) {
const ancestorsStartingAtParent = [];
for (let ancestor = node.parent; ancestor; ancestor = ancestor.parent) {
ancestorsStartingAtParent.push(ancestor);
}
return ancestorsStartingAtParent.reverse();
}
// methods that exist on SourceCode object
const DEPRECATED_SOURCECODE_PASSTHROUGHS = {
getSource: "getText",
@@ -731,7 +747,19 @@ const BASE_TRAVERSAL_CONTEXT = Object.freeze(
*/
function runRules(sourceCode, configuredRules, ruleMapper, parserOptions, parserName, settings, filename) {
const emitter = createEmitter();
const traverser = new Traverser();
const nodeQueue = [];
let currentNode = sourceCode.ast;
Traverser.traverse(sourceCode.ast, {
enter(node, parent) {
node.parent = parent;
nodeQueue.push({ isEntering: true, node });
},
leave(node) {
nodeQueue.push({ isEntering: false, node });
},
visitorKeys: sourceCode.visitorKeys
});
/*
* Create a frozen object with the ruleContext properties and methods that are shared by all rules.
@@ -742,12 +770,12 @@ function runRules(sourceCode, configuredRules, ruleMapper, parserOptions, parser
Object.assign(
Object.create(BASE_TRAVERSAL_CONTEXT),
{
getAncestors: () => traverser.parents(),
getAncestors: () => getAncestors(currentNode),
getDeclaredVariables: sourceCode.scopeManager.getDeclaredVariables.bind(sourceCode.scopeManager),
getFilename: () => filename,
getScope: () => getScope(sourceCode.scopeManager, traverser.current(), parserOptions.ecmaVersion),
getScope: () => getScope(sourceCode.scopeManager, currentNode, parserOptions.ecmaVersion),
getSourceCode: () => sourceCode,
markVariableAsUsed: name => markVariableAsUsed(sourceCode.scopeManager, traverser.current(), parserOptions, name),
markVariableAsUsed: name => markVariableAsUsed(sourceCode.scopeManager, currentNode, parserOptions, name),
parserOptions,
parserPath: parserName,
parserServices: sourceCode.parserServices,
@@ -847,21 +875,14 @@ function runRules(sourceCode, configuredRules, ruleMapper, parserOptions, parser
const eventGenerator = new CodePathAnalyzer(new NodeEventGenerator(emitter));
/*
* Each node has a type property. Whenever a particular type of
* node is found, an event is fired. This allows any listeners to
* automatically be informed that this type of node has been found
* and react accordingly.
*/
traverser.traverse(sourceCode.ast, {
enter(node, parent) {
node.parent = parent;
eventGenerator.enterNode(node);
},
leave(node) {
eventGenerator.leaveNode(node);
},
visitorKeys: sourceCode.visitorKeys
nodeQueue.forEach(traversalInfo => {
currentNode = traversalInfo.node;
if (traversalInfo.isEntering) {
eventGenerator.enterNode(currentNode);
} else {
eventGenerator.leaveNode(currentNode);
}
});
return lintingProblems;
View
@@ -387,15 +387,13 @@ class SourceCode extends TokenStore {
* @public
*/
getNodeByRangeIndex(index) {
let result = null,
resultParent = null;
let result = null;
Traverser.traverse(this.ast, {
visitorKeys: this.visitorKeys,
enter(node, parent) {
enter(node) {
if (node.range[0] <= index && index < node.range[1]) {
result = node;
resultParent = parent;
} else {
this.skip();
}
@@ -407,7 +405,7 @@ class SourceCode extends TokenStore {
}
});
return result ? Object.assign({ parent: resultParent }, result) : null;
return result;
}
/**
View
@@ -118,6 +118,24 @@ describe("Linter", () => {
linter.verify("foo", { rules: { checker: "error", "no-undef": "error" } });
assert(spy.notCalled);
});
it("has all the `parent` properties on nodes when the rule listeners are created", () => {
const spy = sandbox.spy(context => {
const ast = context.getSourceCode().ast;
assert.strictEqual(ast.body[0].parent, ast);
assert.strictEqual(ast.body[0].expression.parent, ast.body[0]);
assert.strictEqual(ast.body[0].expression.left.parent, ast.body[0].expression);
assert.strictEqual(ast.body[0].expression.right.parent, ast.body[0].expression);
return {};
});
linter.defineRule("checker", spy);
linter.verify("foo + bar", { rules: { checker: "error" } });
assert(spy.calledOnce);
});
});
describe("context.getSourceLines()", () => {
@@ -450,43 +468,6 @@ describe("Linter", () => {
linter.verify(code, config);
assert(spy.calledOnce);
});
it("should attach the node's parent", () => {
const config = { rules: { checker: "error" } };
const spy = sandbox.spy(context => {
const node = context.getNodeByRangeIndex(14);
assert.property(node, "parent");
assert.strictEqual(node.parent.type, "VariableDeclarator");
return {};
});
linter.defineRule("checker", spy);
linter.verify(code, config);
assert(spy.calledOnce);
});
it("should not modify the node when attaching the parent", () => {
const config = { rules: { checker: "error" } };
const spy = sandbox.spy(context => {
const node1 = context.getNodeByRangeIndex(10);
assert.strictEqual(node1.type, "VariableDeclarator");
const node2 = context.getNodeByRangeIndex(4);
assert.strictEqual(node2.type, "Identifier");
assert.property(node2, "parent");
assert.strictEqual(node2.parent.type, "VariableDeclarator");
assert.notProperty(node2.parent, "parent");
return {};
});
linter.defineRule("checker", spy);
linter.verify(code, config);
assert(spy.calledOnce);
});
});
@@ -16,7 +16,6 @@ const fs = require("fs"),
leche = require("leche"),
Linter = require("../../../lib/linter"),
SourceCode = require("../../../lib/util/source-code"),
Traverser = require("../../../lib/util/traverser"),
astUtils = require("../../../lib/ast-utils");
//------------------------------------------------------------------------------
@@ -1791,44 +1790,6 @@ describe("SourceCode", () => {
node = sourceCode.getNodeByRangeIndex(-99);
assert.isNull(node);
});
it("should attach the node's parent", () => {
const node = sourceCode.getNodeByRangeIndex(14);
assert.property(node, "parent");
assert.strictEqual(node.parent.type, "VariableDeclarator");
});
it("should not modify the node when attaching the parent", () => {
let node = sourceCode.getNodeByRangeIndex(10);
assert.strictEqual(node.type, "VariableDeclarator");
node = sourceCode.getNodeByRangeIndex(4);
assert.strictEqual(node.type, "Identifier");
assert.property(node, "parent");
assert.strictEqual(node.parent.type, "VariableDeclarator");
assert.notProperty(node.parent, "parent");
});
it("should use visitorKeys", () => {
const text = "a + b";
const ast = espree.parse(text, DEFAULT_CONFIG);
// no traverse BinaryExpression#left
sourceCode = new SourceCode({
text,
ast,
parserServices: null,
scopeManager: null,
visitorKeys: Object.assign({}, Traverser.DEFAULT_VISITOR_KEYS, {
BinaryExpression: ["right"]
})
});
const node = sourceCode.getNodeByRangeIndex(0);
assert.strictEqual(node.type, "BinaryExpression"); // This is Identifier if 'BinaryExpression#left' was traversed.
});
});
describe("isSpaceBetweenTokens()", () => {

0 comments on commit 4eaebe5

Please sign in to comment.