Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Fix: dot-location errors with parenthesized objects (fixes #11868) (#…
  • Loading branch information
mdjermanovic authored and mysticatea committed Jul 21, 2019
1 parent 79e8d09 commit bfcf8b2
Show file tree
Hide file tree
Showing 5 changed files with 265 additions and 24 deletions.
8 changes: 7 additions & 1 deletion docs/rules/dot-location.md
Expand Up @@ -43,7 +43,13 @@ Examples of **correct** code for the default `"object"` option:

var foo = object.
property;
var bar = object.property;

var bar = (
object
).
property;

var baz = object.property;
```

### property
Expand Down
38 changes: 21 additions & 17 deletions lib/rules/dot-location.js
Expand Up @@ -54,29 +54,31 @@ module.exports = {
*/
function checkDotLocation(obj, prop, node) {
const dot = sourceCode.getTokenBefore(prop);
const textBeforeDot = sourceCode.getText().slice(obj.range[1], dot.range[0]);

// `obj` expression can be parenthesized, but those paren tokens are not a part of the `obj` node.
const tokenBeforeDot = sourceCode.getTokenBefore(dot);

const textBeforeDot = sourceCode.getText().slice(tokenBeforeDot.range[1], dot.range[0]);
const textAfterDot = sourceCode.getText().slice(dot.range[1], prop.range[0]);

if (dot.type === "Punctuator" && dot.value === ".") {
if (onObject) {
if (!astUtils.isTokenOnSameLine(obj, dot)) {
const neededTextAfterObj = astUtils.isDecimalInteger(obj) ? " " : "";

context.report({
node,
loc: dot.loc.start,
messageId: "expectedDotAfterObject",
fix: fixer => fixer.replaceTextRange([obj.range[1], prop.range[0]], `${neededTextAfterObj}.${textBeforeDot}${textAfterDot}`)
});
}
} else if (!astUtils.isTokenOnSameLine(dot, prop)) {
if (onObject) {
if (!astUtils.isTokenOnSameLine(tokenBeforeDot, dot)) {
const neededTextAfterToken = astUtils.isDecimalIntegerNumericToken(tokenBeforeDot) ? " " : "";

context.report({
node,
loc: dot.loc.start,
messageId: "expectedDotBeforeProperty",
fix: fixer => fixer.replaceTextRange([obj.range[1], prop.range[0]], `${textBeforeDot}${textAfterDot}.`)
messageId: "expectedDotAfterObject",
fix: fixer => fixer.replaceTextRange([tokenBeforeDot.range[1], prop.range[0]], `${neededTextAfterToken}.${textBeforeDot}${textAfterDot}`)
});
}
} else if (!astUtils.isTokenOnSameLine(dot, prop)) {
context.report({
node,
loc: dot.loc.start,
messageId: "expectedDotBeforeProperty",
fix: fixer => fixer.replaceTextRange([tokenBeforeDot.range[1], prop.range[0]], `${textBeforeDot}${textAfterDot}.`)
});
}
}

Expand All @@ -86,7 +88,9 @@ module.exports = {
* @returns {void}
*/
function checkNode(node) {
checkDotLocation(node.object, node.property, node);
if (!node.computed) {
checkDotLocation(node.object, node.property, node);
}
}

return {
Expand Down
27 changes: 26 additions & 1 deletion lib/rules/utils/ast-utils.js
Expand Up @@ -37,6 +37,8 @@ const LINEBREAKS = new Set(["\r\n", "\r", "\n", "\u2028", "\u2029"]);
// A set of node types that can contain a list of statements
const STATEMENT_LIST_PARENTS = new Set(["Program", "BlockStatement", "SwitchCase"]);

const DECIMAL_INTEGER_PATTERN = /^(0|[1-9]\d*)$/u;

/**
* Checks reference if is non initializer and writable.
* @param {Reference} reference - A reference to check.
Expand Down Expand Up @@ -283,6 +285,16 @@ function isCommaToken(token) {
return token.value === "," && token.type === "Punctuator";
}

/**
* Checks if the given token is a dot token or not.
*
* @param {Token} token - The token to check.
* @returns {boolean} `true` if the token is a dot token.
*/
function isDotToken(token) {
return token.value === "." && token.type === "Punctuator";
}

/**
* Checks if the given token is a semicolon token or not.
*
Expand Down Expand Up @@ -462,12 +474,14 @@ module.exports = {
isColonToken,
isCommaToken,
isCommentToken,
isDotToken,
isKeywordToken,
isNotClosingBraceToken: negate(isClosingBraceToken),
isNotClosingBracketToken: negate(isClosingBracketToken),
isNotClosingParenToken: negate(isClosingParenToken),
isNotColonToken: negate(isColonToken),
isNotCommaToken: negate(isCommaToken),
isNotDotToken: negate(isDotToken),
isNotOpeningBraceToken: negate(isOpeningBraceToken),
isNotOpeningBracketToken: negate(isOpeningBracketToken),
isNotOpeningParenToken: negate(isOpeningParenToken),
Expand Down Expand Up @@ -988,7 +1002,18 @@ module.exports = {
* '5' // false
*/
isDecimalInteger(node) {
return node.type === "Literal" && typeof node.value === "number" && /^(0|[1-9]\d*)$/u.test(node.raw);
return node.type === "Literal" && typeof node.value === "number" &&
DECIMAL_INTEGER_PATTERN.test(node.raw);
},

/**
* Determines whether this token is a decimal integer numeric token.
* This is similar to isDecimalInteger(), but for tokens.
* @param {Token} token - The token to check.
* @returns {boolean} `true` if this token is a decimal integer.
*/
isDecimalIntegerNumericToken(token) {
return token.type === "Numeric" && DECIMAL_INTEGER_PATTERN.test(token.value);
},

/**
Expand Down
174 changes: 174 additions & 0 deletions tests/lib/rules/dot-location.js
Expand Up @@ -37,6 +37,105 @@ ruleTester.run("dot-location", rule, {
{
code: "(obj)\n.prop",
options: ["property"]
},
{
code: "obj . prop",
options: ["object"]
},
{
code: "obj /* a */ . prop",
options: ["object"]
},
{
code: "obj . \nprop",
options: ["object"]
},
{
code: "obj . prop",
options: ["property"]
},
{
code: "obj . /* a */ prop",
options: ["property"]
},
{
code: "obj\n. prop",
options: ["property"]
},
{
code: "f(a\n).prop",
options: ["object"]
},
{
code: "`\n`.prop",
options: ["object"],
parserOptions: { ecmaVersion: 6 }
},
{
code: "obj[prop]",
options: ["object"]
},
{
code: "obj\n[prop]",
options: ["object"]
},
{
code: "obj[\nprop]",
options: ["object"]
},
{
code: "obj\n[\nprop\n]",
options: ["object"]
},
{
code: "obj[prop]",
options: ["property"]
},
{
code: "obj\n[prop]",
options: ["property"]
},
{
code: "obj[\nprop]",
options: ["property"]
},
{
code: "obj\n[\nprop\n]",
options: ["property"]
},

// https://github.com/eslint/eslint/issues/11868 (also in invalid)
{
code: "(obj).prop",
options: ["object"]
},
{
code: "(obj).\nprop",
options: ["object"]
},
{
code: "(obj\n).\nprop",
options: ["object"]
},
{
code: "(\nobj\n).\nprop",
options: ["object"]
},
{
code: "((obj\n)).\nprop",
options: ["object"]
},
{
code: "(f(a)\n).\nprop",
options: ["object"]
},
{
code: "((obj\n)\n).\nprop",
options: ["object"]
},
{
code: "(\na &&\nb()\n).toString()",
options: ["object"]
}
],
invalid: [
Expand Down Expand Up @@ -81,6 +180,81 @@ ruleTester.run("dot-location", rule, {
output: "foo. /* a */ \n /* b */ /* c */ bar",
options: ["object"],
errors: [{ messageId: "expectedDotAfterObject", type: "MemberExpression", line: 2, column: 10 }]
},
{
code: "f(a\n)\n.prop",
output: "f(a\n).\nprop",
options: ["object"],
errors: [{ messageId: "expectedDotAfterObject", type: "MemberExpression", line: 3, column: 1 }]
},
{
code: "`\n`\n.prop",
output: "`\n`.\nprop",
options: ["object"],
parserOptions: { ecmaVersion: 6 },
errors: [{ messageId: "expectedDotAfterObject", type: "MemberExpression", line: 3, column: 1 }]
},

// https://github.com/eslint/eslint/issues/11868 (also in valid)
{
code: "(a\n)\n.prop",
output: "(a\n).\nprop",
options: ["object"],
errors: [{ messageId: "expectedDotAfterObject", type: "MemberExpression", line: 3, column: 1 }]
},
{
code: "(a\n)\n.\nprop",
output: "(a\n).\n\nprop",
options: ["object"],
errors: [{ messageId: "expectedDotAfterObject", type: "MemberExpression", line: 3, column: 1 }]
},
{
code: "(f(a)\n)\n.prop",
output: "(f(a)\n).\nprop",
options: ["object"],
errors: [{ messageId: "expectedDotAfterObject", type: "MemberExpression", line: 3, column: 1 }]
},
{
code: "(f(a\n)\n)\n.prop",
output: "(f(a\n)\n).\nprop",
options: ["object"],
errors: [{ messageId: "expectedDotAfterObject", type: "MemberExpression", line: 4, column: 1 }]
},
{
code: "((obj\n))\n.prop",
output: "((obj\n)).\nprop",
options: ["object"],
errors: [{ messageId: "expectedDotAfterObject", type: "MemberExpression", line: 3, column: 1 }]
},
{
code: "((obj\n)\n)\n.prop",
output: "((obj\n)\n).\nprop",
options: ["object"],
errors: [{ messageId: "expectedDotAfterObject", type: "MemberExpression", line: 4, column: 1 }]
},
{
code: "(a\n) /* a */ \n.prop",
output: "(a\n). /* a */ \nprop",
options: ["object"],
errors: [{ messageId: "expectedDotAfterObject", type: "MemberExpression", line: 3, column: 1 }]
},
{
code: "(a\n)\n/* a */\n.prop",
output: "(a\n).\n/* a */\nprop",
options: ["object"],
errors: [{ messageId: "expectedDotAfterObject", type: "MemberExpression", line: 4, column: 1 }]
},
{
code: "(a\n)\n/* a */.prop",
output: "(a\n).\n/* a */prop",
options: ["object"],
errors: [{ messageId: "expectedDotAfterObject", type: "MemberExpression", line: 3, column: 8 }]
},
{
code: "(5)\n.toExponential()",
output: "(5).\ntoExponential()",
options: ["object"],
errors: [{ messageId: "expectedDotAfterObject", type: "MemberExpression", line: 2, column: 1 }]
}
]
});
42 changes: 37 additions & 5 deletions tests/lib/rules/utils/ast-utils.js
Expand Up @@ -629,7 +629,7 @@ describe("ast-utils", () => {
});
});

describe("isDecimalInteger", () => {
{
const expectedResults = {
5: true,
0: true,
Expand All @@ -642,12 +642,22 @@ describe("ast-utils", () => {
"'5'": false
};

Object.keys(expectedResults).forEach(key => {
it(`should return ${expectedResults[key]} for ${key}`, () => {
assert.strictEqual(astUtils.isDecimalInteger(espree.parse(key).body[0].expression), expectedResults[key]);
describe("isDecimalInteger", () => {
Object.keys(expectedResults).forEach(key => {
it(`should return ${expectedResults[key]} for ${key}`, () => {
assert.strictEqual(astUtils.isDecimalInteger(espree.parse(key).body[0].expression), expectedResults[key]);
});
});
});
});

describe("isDecimalIntegerNumericToken", () => {
Object.keys(expectedResults).forEach(key => {
it(`should return ${expectedResults[key]} for ${key}`, () => {
assert.strictEqual(astUtils.isDecimalIntegerNumericToken(espree.tokenize(key)[0]), expectedResults[key]);
});
});
});
}

describe("getFunctionNameWithKind", () => {
const expectedResults = {
Expand Down Expand Up @@ -993,6 +1003,28 @@ describe("ast-utils", () => {
});
}

{
const code = "const obj = {foo: 1.5, bar: a.b};";
const tokens = espree.parse(code, { ecmaVersion: 6, tokens: true }).tokens;
const expected = [false, false, false, false, false, false, false, false, false, false, false, true, false, false, false];

describe("isDotToken", () => {
tokens.forEach((token, index) => {
it(`should return ${expected[index]} for '${token.value}'.`, () => {
assert.strictEqual(astUtils.isDotToken(token), expected[index]);
});
});
});

describe("isNotDotToken", () => {
tokens.forEach((token, index) => {
it(`should return ${!expected[index]} for '${token.value}'.`, () => {
assert.strictEqual(astUtils.isNotDotToken(token), !expected[index]);
});
});
});
}

describe("isCommentToken", () => {
const code = "const obj = /*block*/ {foo: 1, bar: 2}; //line";
const ast = espree.parse(code, { ecmaVersion: 6, tokens: true, comment: true });
Expand Down

0 comments on commit bfcf8b2

Please sign in to comment.