eslint 1.9.0 → 1.10.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +1 -1
  2. package/bin/eslint.js +1 -2
  3. package/lib/ast-utils.js +22 -1
  4. package/lib/cli-engine.js +15 -7
  5. package/lib/cli.js +2 -1
  6. package/lib/config/config-file.js +440 -0
  7. package/lib/config/config-initializer.js +241 -0
  8. package/lib/config/config-ops.js +186 -0
  9. package/lib/{config-validator.js → config/config-validator.js} +10 -2
  10. package/lib/config.js +39 -183
  11. package/lib/eslint.js +49 -41
  12. package/lib/file-finder.js +34 -15
  13. package/lib/ignored-paths.js +1 -1
  14. package/lib/options.js +7 -1
  15. package/lib/rules/block-spacing.js +7 -2
  16. package/lib/rules/brace-style.js +3 -1
  17. package/lib/rules/comma-spacing.js +25 -66
  18. package/lib/rules/consistent-this.js +25 -29
  19. package/lib/rules/curly.js +37 -7
  20. package/lib/rules/eqeqeq.js +17 -2
  21. package/lib/rules/id-length.js +2 -2
  22. package/lib/rules/indent.js +7 -10
  23. package/lib/rules/lines-around-comment.js +32 -12
  24. package/lib/rules/new-cap.js +11 -1
  25. package/lib/rules/no-alert.js +2 -3
  26. package/lib/rules/no-catch-shadow.js +7 -13
  27. package/lib/rules/no-extend-native.js +1 -2
  28. package/lib/rules/no-fallthrough.js +1 -2
  29. package/lib/rules/no-implicit-coercion.js +19 -0
  30. package/lib/rules/no-label-var.js +9 -23
  31. package/lib/rules/no-multiple-empty-lines.js +1 -1
  32. package/lib/rules/no-sequences.js +2 -1
  33. package/lib/rules/no-shadow.js +31 -58
  34. package/lib/rules/no-spaced-func.js +16 -12
  35. package/lib/rules/no-undef-init.js +5 -3
  36. package/lib/rules/no-undef.js +10 -13
  37. package/lib/rules/no-use-before-define.js +7 -21
  38. package/lib/rules/operator-linebreak.js +3 -2
  39. package/lib/rules/quotes.js +34 -15
  40. package/lib/rules/require-jsdoc.js +61 -2
  41. package/lib/rules/space-after-keywords.js +2 -0
  42. package/lib/rules/space-before-function-paren.js +5 -26
  43. package/lib/rules/space-before-keywords.js +5 -2
  44. package/lib/rules/spaced-comment.js +1 -3
  45. package/lib/rules/valid-jsdoc.js +8 -4
  46. package/lib/rules/vars-on-top.js +2 -2
  47. package/lib/testers/rule-tester.js +48 -7
  48. package/lib/util/source-code.js +4 -0
  49. package/lib/util.js +0 -92
  50. package/package.json +4 -6
  51. package/lib/config-initializer.js +0 -146
package/lib/options.js CHANGED
@@ -112,7 +112,7 @@ module.exports = optionator({
112
112
  },
113
113
  {
114
114
  option: "ignore-pattern",
115
- type: "String",
115
+ type: "[String]",
116
116
  description: "Pattern of files to ignore (in addition to those in .eslintignore)"
117
117
  },
118
118
  {
@@ -198,6 +198,12 @@ module.exports = optionator({
198
198
  alias: "v",
199
199
  type: "Boolean",
200
200
  description: "Outputs the version number"
201
+ },
202
+ {
203
+ option: "inline-config",
204
+ type: "Boolean",
205
+ default: "true",
206
+ description: "Allow comments to change eslint config/rules"
201
207
  }
202
208
  ]
203
209
  });
@@ -59,8 +59,8 @@ module.exports = function(context) {
59
59
  // Gets braces and the first/last token of content.
60
60
  var openBrace = getOpenBrace(node);
61
61
  var closeBrace = context.getLastToken(node);
62
- var firstToken = context.getTokenAfter(openBrace);
63
- var lastToken = context.getTokenBefore(closeBrace);
62
+ var firstToken = sourceCode.getTokenOrCommentAfter(openBrace);
63
+ var lastToken = sourceCode.getTokenOrCommentBefore(closeBrace);
64
64
 
65
65
  // Skip if the node is invalid or empty.
66
66
  if (openBrace.type !== "Punctuator" ||
@@ -72,6 +72,11 @@ module.exports = function(context) {
72
72
  return;
73
73
  }
74
74
 
75
+ // Skip line comments for option never
76
+ if (!always && firstToken.type === "Line") {
77
+ return;
78
+ }
79
+
75
80
  // Check.
76
81
  if (!isValid(openBrace, firstToken)) {
77
82
  context.report({
@@ -109,7 +109,9 @@ module.exports = function(context) {
109
109
  tokens = context.getTokensBefore(node.alternate, 2);
110
110
 
111
111
  if (style === "1tbs") {
112
- if (tokens[0].loc.start.line !== tokens[1].loc.start.line && isCurlyPunctuator(tokens[0]) ) {
112
+ if (tokens[0].loc.start.line !== tokens[1].loc.start.line &&
113
+ node.consequent.type === "BlockStatement" &&
114
+ isCurlyPunctuator(tokens[0]) ) {
113
115
  context.report(node.alternate, CLOSE_MESSAGE);
114
116
  }
115
117
  } else if (tokens[0].loc.start.line === tokens[1].loc.start.line) {
@@ -14,6 +14,7 @@ var astUtils = require("../ast-utils");
14
14
  module.exports = function(context) {
15
15
 
16
16
  var sourceCode = context.getSourceCode();
17
+ var tokensAndComments = sourceCode.tokensAndComments;
17
18
 
18
19
  var options = {
19
20
  before: context.options[0] ? !!context.options[0].before : false,
@@ -24,10 +25,6 @@ module.exports = function(context) {
24
25
  // Helpers
25
26
  //--------------------------------------------------------------------------
26
27
 
27
- // the index of the last comment that was checked
28
- var lastCommentIndex = 0;
29
- var allComments;
30
-
31
28
  // list of comma tokens to ignore for the check of leading whitespace
32
29
  var commaTokensToIgnore = [];
33
30
 
@@ -41,39 +38,6 @@ module.exports = function(context) {
41
38
  return !!token && (token.type === "Punctuator") && (token.value === ",");
42
39
  }
43
40
 
44
- /**
45
- * Determines if a given source index is in a comment or not by checking
46
- * the index against the comment range. Since the check goes straight
47
- * through the file, once an index is passed a certain comment, we can
48
- * go to the next comment to check that.
49
- * @param {int} index The source index to check.
50
- * @param {ASTNode[]} comments An array of comment nodes.
51
- * @returns {boolean} True if the index is within a comment, false if not.
52
- * @private
53
- */
54
- function isIndexInComment(index, comments) {
55
-
56
- var comment;
57
- lastCommentIndex = 0;
58
-
59
- while (lastCommentIndex < comments.length) {
60
-
61
- comment = comments[lastCommentIndex];
62
-
63
- if (comment.range[0] <= index && index < comment.range[1]) {
64
- return true;
65
- } else if (index > comment.range[1]) {
66
- lastCommentIndex++;
67
- } else {
68
- break;
69
- }
70
-
71
- }
72
-
73
- return false;
74
- }
75
-
76
-
77
41
  /**
78
42
  * Reports a spacing error with an appropriate message.
79
43
  * @param {ASTNode} node The binary expression node to report.
@@ -93,9 +57,6 @@ module.exports = function(context) {
93
57
  return fixer.insertTextAfter(node, " ");
94
58
  }
95
59
  } else {
96
- /*
97
- * Comments handling
98
- */
99
60
  var start, end;
100
61
  var newText = "";
101
62
 
@@ -107,11 +68,6 @@ module.exports = function(context) {
107
68
  end = otherNode.range[0];
108
69
  }
109
70
 
110
- for (var i = start; i < end; i++) {
111
- if (isIndexInComment(i, allComments)) {
112
- newText += context.getSource()[i];
113
- }
114
- }
115
71
  return fixer.replaceTextRange([start, end], newText);
116
72
  }
117
73
  },
@@ -137,6 +93,11 @@ module.exports = function(context) {
137
93
  ) {
138
94
  report(reportItem, "before", tokens.left);
139
95
  }
96
+
97
+ if (tokens.right && !options.after && tokens.right.type === "Line") {
98
+ return false;
99
+ }
100
+
140
101
  if (tokens.right && astUtils.isTokenOnSameLine(tokens.comma, tokens.right) &&
141
102
  (options.after !== sourceCode.isSpaceBetweenTokens(tokens.comma, tokens.right))
142
103
  ) {
@@ -176,30 +137,28 @@ module.exports = function(context) {
176
137
  return {
177
138
  "Program:exit": function() {
178
139
 
179
- var source = context.getSource(),
180
- pattern = /,/g,
181
- commaToken,
182
- previousToken,
140
+ var previousToken,
183
141
  nextToken;
184
142
 
185
- allComments = context.getAllComments();
186
- while (pattern.test(source)) {
187
-
188
- // do not flag anything inside of comments
189
- if (!isIndexInComment(pattern.lastIndex, allComments)) {
190
- commaToken = context.getTokenByRangeStart(pattern.lastIndex - 1);
191
-
192
- if (commaToken && commaToken.type !== "JSXText") {
193
- previousToken = context.getTokenBefore(commaToken);
194
- nextToken = context.getTokenAfter(commaToken);
195
- validateCommaItemSpacing({
196
- comma: commaToken,
197
- left: isComma(previousToken) || commaTokensToIgnore.indexOf(commaToken) > -1 ? null : previousToken,
198
- right: isComma(nextToken) ? null : nextToken
199
- }, commaToken);
200
- }
143
+ tokensAndComments.forEach(function(token, i) {
144
+
145
+ if (!isComma(token)) {
146
+ return;
201
147
  }
202
- }
148
+
149
+ if (token && token.type === "JSXText") {
150
+ return;
151
+ }
152
+
153
+ previousToken = tokensAndComments[i - 1];
154
+ nextToken = tokensAndComments[i + 1];
155
+
156
+ validateCommaItemSpacing({
157
+ comma: token,
158
+ left: isComma(previousToken) || commaTokensToIgnore.indexOf(token) > -1 ? null : previousToken,
159
+ right: isComma(nextToken) ? null : nextToken
160
+ }, token);
161
+ });
203
162
  },
204
163
  "ArrayExpression": addNullElementsToIgnoreList,
205
164
  "ArrayPattern": addNullElementsToIgnoreList
@@ -53,39 +53,35 @@ module.exports = function(context) {
53
53
  */
54
54
  function ensureWasAssigned() {
55
55
  var scope = context.getScope();
56
+ var variable = scope.set.get(alias);
57
+ if (!variable) {
58
+ return;
59
+ }
60
+
61
+ if (variable.defs.some(function(def) {
62
+ return def.node.type === "VariableDeclarator" &&
63
+ def.node.init !== null;
64
+ })) {
65
+ return;
66
+ }
56
67
 
57
- scope.variables.some(function(variable) {
58
- var lookup;
59
-
60
- if (variable.name === alias) {
61
- if (variable.defs.some(function(def) {
62
- return def.node.type === "VariableDeclarator" &&
63
- def.node.init !== null;
64
- })) {
65
- return true;
66
- }
67
-
68
- lookup = scope.type === "global" ? scope : variable;
69
-
70
- // The alias has been declared and not assigned: check it was
71
- // assigned later in the same scope.
72
- if (!lookup.references.some(function(reference) {
73
- var write = reference.writeExpr;
74
-
75
- if (reference.from === scope &&
76
- write && write.type === "ThisExpression" &&
77
- write.parent.operator === "=") {
78
- return true;
79
- }
80
- })) {
81
- variable.defs.map(function(def) {
82
- return def.node;
83
- }).forEach(reportBadAssignment);
84
- }
68
+ var lookup = (variable.references.length === 0 && scope.type === "global") ? scope : variable;
85
69
 
70
+ // The alias has been declared and not assigned: check it was
71
+ // assigned later in the same scope.
72
+ if (!lookup.references.some(function(reference) {
73
+ var write = reference.writeExpr;
74
+
75
+ if (reference.from === scope &&
76
+ write && write.type === "ThisExpression" &&
77
+ write.parent.operator === "=") {
86
78
  return true;
87
79
  }
88
- });
80
+ })) {
81
+ variable.defs.map(function(def) {
82
+ return def.node;
83
+ }).forEach(reportBadAssignment);
84
+ }
89
85
  }
90
86
 
91
87
  return {
@@ -51,6 +51,22 @@ module.exports = function(context) {
51
51
  return first.loc.start.line === last.loc.end.line;
52
52
  }
53
53
 
54
+ /**
55
+ * Gets the `else` keyword token of a given `IfStatement` node.
56
+ * @param {ASTNode} node - A `IfStatement` node to get.
57
+ * @returns {Token} The `else` keyword token.
58
+ */
59
+ function getElseKeyword(node) {
60
+ var sourceCode = context.getSourceCode();
61
+ var token = sourceCode.getTokenAfter(node.consequent);
62
+
63
+ while (token.type !== "Keyword" || token.value !== "else") {
64
+ token = sourceCode.getTokenAfter(token);
65
+ }
66
+
67
+ return token;
68
+ }
69
+
54
70
  /**
55
71
  * Checks a given IfStatement node requires braces of the consequent chunk.
56
72
  * This returns `true` when below:
@@ -89,11 +105,15 @@ module.exports = function(context) {
89
105
  * @private
90
106
  */
91
107
  function reportExpectedBraceError(node, name, suffix) {
92
- context.report(node, "Expected { after '{{name}}'{{suffix}}.",
93
- {
108
+ context.report({
109
+ node: node,
110
+ loc: (name !== "else" ? node : getElseKeyword(node)).loc.start,
111
+ message: "Expected { after '{{name}}'{{suffix}}.",
112
+ data: {
94
113
  name: name,
95
114
  suffix: (suffix ? " " + suffix : "")
96
- });
115
+ }
116
+ });
97
117
  }
98
118
 
99
119
  /**
@@ -105,12 +125,15 @@ module.exports = function(context) {
105
125
  * @private
106
126
  */
107
127
  function reportUnnecessaryBraceError(node, name, suffix) {
108
- context.report(node, "Unnecessary { after '{{name}}'{{suffix}}.",
109
- {
128
+ context.report({
129
+ node: node,
130
+ loc: (name !== "else" ? node : getElseKeyword(node)).loc.start,
131
+ message: "Unnecessary { after '{{name}}'{{suffix}}.",
132
+ data: {
110
133
  name: name,
111
134
  suffix: (suffix ? " " + suffix : "")
112
135
  }
113
- );
136
+ });
114
137
  }
115
138
 
116
139
  /**
@@ -224,9 +247,16 @@ module.exports = function(context) {
224
247
 
225
248
  "ForStatement": function(node) {
226
249
  prepareCheck(node, node.body, "for", "condition").check();
250
+ },
251
+
252
+ "ForInStatement": function(node) {
253
+ prepareCheck(node, node.body, "for-in").check();
254
+ },
255
+
256
+ "ForOfStatement": function(node) {
257
+ prepareCheck(node, node.body, "for-of").check();
227
258
  }
228
259
  };
229
-
230
260
  };
231
261
 
232
262
  module.exports.schema = {
@@ -13,7 +13,11 @@
13
13
 
14
14
  module.exports = function(context) {
15
15
 
16
- var sourceCode = context.getSourceCode();
16
+ var sourceCode = context.getSourceCode(),
17
+ replacements = {
18
+ "==": "===",
19
+ "!=": "!=="
20
+ };
17
21
 
18
22
  /**
19
23
  * Checks if an expression is a typeof expression
@@ -89,7 +93,18 @@ module.exports = function(context) {
89
93
  message: "Expected '{{op}}=' and instead saw '{{op}}'.",
90
94
  data: { op: node.operator },
91
95
  fix: function(fixer) {
92
- return fixer.insertTextAfter(sourceCode.getTokenAfter(node.left), "=");
96
+ var tokens = sourceCode.getTokensBetween(node.left, node.right),
97
+ opToken,
98
+ i;
99
+
100
+ for (i = 0; i < tokens.length; ++i) {
101
+ if (tokens[i].value === node.operator) {
102
+ opToken = tokens[i];
103
+ break;
104
+ }
105
+ }
106
+
107
+ return fixer.replaceTextRange(opToken.range, replacements[node.operator]);
93
108
  }
94
109
  });
95
110
 
@@ -25,12 +25,12 @@ module.exports = function(context) {
25
25
  }, {});
26
26
 
27
27
  var SUPPORTED_EXPRESSIONS = {
28
- "MemberExpression": function(parent) {
28
+ "MemberExpression": properties && function(parent) {
29
29
  return !parent.computed && (
30
30
  // regular property assignment
31
31
  parent.parent.left === parent || (
32
32
  // or the last identifier in an ObjectPattern destructuring
33
- parent.parent.type === "Property" && properties && parent.parent.value === parent &&
33
+ parent.parent.type === "Property" && parent.parent.value === parent &&
34
34
  parent.parent.parent.type === "ObjectPattern" && parent.parent.parent.parent.left === parent.parent.parent
35
35
  )
36
36
  );
@@ -75,6 +75,11 @@ module.exports = function(context) {
75
75
  }
76
76
  }
77
77
 
78
+ var indentPattern = {
79
+ normal: indentType === "space" ? /^ +/ : /^\t+/,
80
+ excludeCommas: indentType === "space" ? /^[ ,]+/ : /^[\t,]+/
81
+ };
82
+
78
83
  var caseIndentStore = {};
79
84
 
80
85
  /**
@@ -168,17 +173,9 @@ module.exports = function(context) {
168
173
  function getNodeIndent(node, byLastLine, excludeCommas) {
169
174
  var token = byLastLine ? context.getLastToken(node) : context.getFirstToken(node);
170
175
  var src = context.getSource(token, token.loc.start.column);
171
-
172
- var skip = excludeCommas ? "," : "";
173
-
174
- var regExp;
175
- if (indentType === "space") {
176
- regExp = new RegExp("^[ " + skip + "]+");
177
- } else {
178
- regExp = new RegExp("^[\t" + skip + "]+");
179
- }
180
-
176
+ var regExp = excludeCommas ? indentPattern.excludeCommas : indentPattern.normal;
181
177
  var indent = regExp.exec(src);
178
+
182
179
  return indent ? indent[0].length : 0;
183
180
  }
184
181
 
@@ -7,6 +7,16 @@
7
7
  */
8
8
  "use strict";
9
9
 
10
+ //------------------------------------------------------------------------------
11
+ // Requirements
12
+ //------------------------------------------------------------------------------
13
+
14
+ var assign = require("object-assign");
15
+
16
+ //------------------------------------------------------------------------------
17
+ // Helpers
18
+ //------------------------------------------------------------------------------
19
+
10
20
  /**
11
21
  * Return an array with with any line numbers that are empty.
12
22
  * @param {Array} lines An array of each line of the file.
@@ -57,7 +67,7 @@ function contains(val, array) {
57
67
 
58
68
  module.exports = function(context) {
59
69
 
60
- var options = context.options[0] || {};
70
+ var options = context.options[0] ? assign({}, context.options[0]) : {};
61
71
  options.beforeLineComment = options.beforeLineComment || false;
62
72
  options.afterLineComment = options.afterLineComment || false;
63
73
  options.beforeBlockComment = typeof options.beforeBlockComment !== "undefined" ? options.beforeBlockComment : true;
@@ -65,26 +75,34 @@ module.exports = function(context) {
65
75
  options.allowBlockStart = options.allowBlockStart || false;
66
76
  options.allowBlockEnd = options.allowBlockEnd || false;
67
77
 
78
+ var sourceCode = context.getSourceCode();
68
79
  /**
69
- * Returns whether or not comments are not on lines starting with or ending with code
80
+ * Returns whether or not comments are on lines starting with or ending with code
70
81
  * @param {ASTNode} node The comment node to check.
71
82
  * @returns {boolean} True if the comment is not alone.
72
83
  */
73
84
  function codeAroundComment(node) {
85
+ var token;
74
86
 
75
- var lines = context.getSourceLines();
87
+ token = node;
88
+ do {
89
+ token = sourceCode.getTokenOrCommentBefore(token);
90
+ } while (token && (token.type === "Block" || token.type === "Line"));
76
91
 
77
- // Get the whole line and cut it off at the start of the comment
78
- var startLine = lines[node.loc.start.line - 1];
79
- var endLine = lines[node.loc.end.line - 1];
92
+ if (token && token.loc.end.line === node.loc.start.line) {
93
+ return true;
94
+ }
80
95
 
81
- var preamble = startLine.slice(0, node.loc.start.column).trim();
96
+ token = node;
97
+ do {
98
+ token = sourceCode.getTokenOrCommentAfter(token);
99
+ } while (token && (token.type === "Block" || token.type === "Line"));
82
100
 
83
- // Also check after the comment
84
- var postamble = endLine.slice(node.loc.end.column).trim();
101
+ if (token && token.loc.start.line === node.loc.end.line) {
102
+ return true;
103
+ }
85
104
 
86
- // Should be false if there was only whitespace around the comment
87
- return !!(preamble || postamble);
105
+ return false;
88
106
  }
89
107
 
90
108
  /**
@@ -95,7 +113,9 @@ module.exports = function(context) {
95
113
  * @returns {boolean} True if the comment is inside nodeType.
96
114
  */
97
115
  function isCommentInsideNodeType(node, parent, nodeType) {
98
- return parent.type === nodeType || (parent.body && parent.body.type === nodeType);
116
+ return parent.type === nodeType ||
117
+ (parent.body && parent.body.type === nodeType) ||
118
+ (parent.consequent && parent.consequent.type === nodeType);
99
119
  }
100
120
 
101
121
  /**
@@ -7,6 +7,16 @@
7
7
 
8
8
  "use strict";
9
9
 
10
+ //------------------------------------------------------------------------------
11
+ // Requirements
12
+ //------------------------------------------------------------------------------
13
+
14
+ var assign = require("object-assign");
15
+
16
+ //------------------------------------------------------------------------------
17
+ // Helpers
18
+ //------------------------------------------------------------------------------
19
+
10
20
  var CAPS_ALLOWED = [
11
21
  "Array",
12
22
  "Boolean",
@@ -67,7 +77,7 @@ function calculateCapIsNewExceptions(config) {
67
77
 
68
78
  module.exports = function(context) {
69
79
 
70
- var config = context.options[0] || {};
80
+ var config = context.options[0] ? assign({}, context.options[0]) : {};
71
81
  config.newIsCap = config.newIsCap !== false;
72
82
  config.capIsNew = config.capIsNew !== false;
73
83
  var skipProperties = config.properties === false;
@@ -69,9 +69,8 @@ function findReference(scope, node) {
69
69
  * @returns {boolean} Whether or not the name is shadowed globally.
70
70
  */
71
71
  function isGloballyShadowed(globalScope, identifierName) {
72
- return globalScope.variables.some(function(variable) {
73
- return variable.name === identifierName && variable.defs.length > 0;
74
- });
72
+ var variable = globalScope.set.get(identifierName);
73
+ return Boolean(variable && variable.defs.length > 0);
75
74
  }
76
75
 
77
76
  /**
@@ -5,6 +5,12 @@
5
5
 
6
6
  "use strict";
7
7
 
8
+ //------------------------------------------------------------------------------
9
+ // Requirements
10
+ //------------------------------------------------------------------------------
11
+
12
+ var astUtils = require("../ast-utils");
13
+
8
14
  //------------------------------------------------------------------------------
9
15
  // Rule Definition
10
16
  //------------------------------------------------------------------------------
@@ -22,19 +28,7 @@ module.exports = function(context) {
22
28
  * @returns {boolean} True is its been shadowed
23
29
  */
24
30
  function paramIsShadowing(scope, name) {
25
- var found = scope.variables.some(function(variable) {
26
- return variable.name === name;
27
- });
28
-
29
- if (found) {
30
- return true;
31
- }
32
-
33
- if (scope.upper) {
34
- return paramIsShadowing(scope.upper, name);
35
- }
36
-
37
- return false;
31
+ return astUtils.getVariableByName(scope, name) !== null;
38
32
  }
39
33
 
40
34
  //--------------------------------------------------------------------------
@@ -68,8 +68,7 @@ module.exports = function(context) {
68
68
 
69
69
  // verify the object being added to is a native prototype
70
70
  subject = node.arguments[0];
71
- object = subject.object;
72
-
71
+ object = subject && subject.object;
73
72
  if (object &&
74
73
  object.type === "Identifier" &&
75
74
  (modifiedBuiltins.indexOf(object.name) > -1) &&
@@ -48,8 +48,7 @@ module.exports = function(context) {
48
48
 
49
49
  // check for comment
50
50
  if (!comment || !FALLTHROUGH_COMMENT.test(comment.value)) {
51
-
52
- context.report(switchData.lastCase,
51
+ context.report(node,
53
52
  "Expected a \"break\" statement before \"{{code}}\".",
54
53
  { code: node.test ? "case" : "default" });
55
54
  }
@@ -113,6 +113,15 @@ function isConcatWithEmptyString(node) {
113
113
  );
114
114
  }
115
115
 
116
+ /**
117
+ * Checks whether or not a node is appended with an empty string.
118
+ * @param {ASTNode} node - An AssignmentExpression node to check.
119
+ * @returns {boolean} Whether or not the node is appended with an empty string.
120
+ */
121
+ function isAppendEmptyString(node) {
122
+ return node.operator === "+=" && node.right.type === "Literal" && node.right.value === "";
123
+ }
124
+
116
125
  /**
117
126
  * Gets a node that is the left or right operand of a node, is not the specified literal.
118
127
  * @param {ASTNode} node - A BinaryExpression node to get.
@@ -178,6 +187,16 @@ module.exports = function(context) {
178
187
  "use `String({{code}})` instead.",
179
188
  {code: context.getSource(getOtherOperand(node, ""))});
180
189
  }
190
+ },
191
+
192
+ "AssignmentExpression": function(node) {
193
+ // foo += ""
194
+ if (options.string && isAppendEmptyString(node)) {
195
+ context.report(
196
+ node,
197
+ "use `{{code}} = String({{code}})` instead.",
198
+ {code: context.getSource(getOtherOperand(node, ""))});
199
+ }
181
200
  }
182
201
  };
183
202
  };