OLD | NEW |
| (Empty) |
1 #!/usr/bin/env python | |
2 # Copyright 2011 The Closure Linter Authors. All Rights Reserved. | |
3 # | |
4 # Licensed under the Apache License, Version 2.0 (the "License"); | |
5 # you may not use this file except in compliance with the License. | |
6 # You may obtain a copy of the License at | |
7 # | |
8 # http://www.apache.org/licenses/LICENSE-2.0 | |
9 # | |
10 # Unless required by applicable law or agreed to in writing, software | |
11 # distributed under the License is distributed on an "AS-IS" BASIS, | |
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 # See the License for the specific language governing permissions and | |
14 # limitations under the License. | |
15 | |
16 """Methods for checking JS files for common style guide violations. | |
17 | |
18 These style guide violations should only apply to JavaScript and not an Ecma | |
19 scripting languages. | |
20 """ | |
21 | |
22 __author__ = ('robbyw@google.com (Robert Walker)', | |
23 'ajp@google.com (Andy Perelson)', | |
24 'jacobr@google.com (Jacob Richman)') | |
25 | |
26 import re | |
27 | |
28 from closure_linter import ecmalintrules | |
29 from closure_linter import error_check | |
30 from closure_linter import errors | |
31 from closure_linter import javascripttokenizer | |
32 from closure_linter import javascripttokens | |
33 from closure_linter import requireprovidesorter | |
34 from closure_linter import tokenutil | |
35 from closure_linter.common import error | |
36 from closure_linter.common import position | |
37 | |
38 # Shorthand | |
39 Error = error.Error | |
40 Position = position.Position | |
41 Rule = error_check.Rule | |
42 Type = javascripttokens.JavaScriptTokenType | |
43 | |
44 | |
45 class JavaScriptLintRules(ecmalintrules.EcmaScriptLintRules): | |
46 """JavaScript lint rules that catch JavaScript specific style errors.""" | |
47 | |
48 def __init__(self, namespaces_info): | |
49 """Initializes a JavaScriptLintRules instance.""" | |
50 ecmalintrules.EcmaScriptLintRules.__init__(self) | |
51 self._namespaces_info = namespaces_info | |
52 self._declared_private_member_tokens = {} | |
53 self._declared_private_members = set() | |
54 self._used_private_members = set() | |
55 # A stack of dictionaries, one for each function scope entered. Each | |
56 # dictionary is keyed by an identifier that defines a local variable and has | |
57 # a token as its value. | |
58 self._unused_local_variables_by_scope = [] | |
59 | |
60 def HandleMissingParameterDoc(self, token, param_name): | |
61 """Handle errors associated with a parameter missing a param tag.""" | |
62 self._HandleError(errors.MISSING_PARAMETER_DOCUMENTATION, | |
63 'Missing docs for parameter: "%s"' % param_name, token) | |
64 | |
65 # pylint: disable=too-many-statements | |
66 def CheckToken(self, token, state): | |
67 """Checks a token, given the current parser_state, for warnings and errors. | |
68 | |
69 Args: | |
70 token: The current token under consideration | |
71 state: parser_state object that indicates the current state in the page | |
72 """ | |
73 | |
74 # Call the base class's CheckToken function. | |
75 super(JavaScriptLintRules, self).CheckToken(token, state) | |
76 | |
77 # Store some convenience variables | |
78 namespaces_info = self._namespaces_info | |
79 | |
80 if error_check.ShouldCheck(Rule.UNUSED_LOCAL_VARIABLES): | |
81 self._CheckUnusedLocalVariables(token, state) | |
82 | |
83 if error_check.ShouldCheck(Rule.UNUSED_PRIVATE_MEMBERS): | |
84 # Find all assignments to private members. | |
85 if token.type == Type.SIMPLE_LVALUE: | |
86 identifier = token.string | |
87 if identifier.endswith('_') and not identifier.endswith('__'): | |
88 doc_comment = state.GetDocComment() | |
89 suppressed = doc_comment and ( | |
90 'underscore' in doc_comment.suppressions or | |
91 'unusedPrivateMembers' in doc_comment.suppressions) | |
92 if not suppressed: | |
93 # Look for static members defined on a provided namespace. | |
94 if namespaces_info: | |
95 namespace = namespaces_info.GetClosurizedNamespace(identifier) | |
96 provided_namespaces = namespaces_info.GetProvidedNamespaces() | |
97 else: | |
98 namespace = None | |
99 provided_namespaces = set() | |
100 | |
101 # Skip cases of this.something_.somethingElse_. | |
102 regex = re.compile(r'^this\.[a-zA-Z_]+$') | |
103 if namespace in provided_namespaces or regex.match(identifier): | |
104 variable = identifier.split('.')[-1] | |
105 self._declared_private_member_tokens[variable] = token | |
106 self._declared_private_members.add(variable) | |
107 elif not identifier.endswith('__'): | |
108 # Consider setting public members of private members to be a usage. | |
109 for piece in identifier.split('.'): | |
110 if piece.endswith('_'): | |
111 self._used_private_members.add(piece) | |
112 | |
113 # Find all usages of private members. | |
114 if token.type == Type.IDENTIFIER: | |
115 for piece in token.string.split('.'): | |
116 if piece.endswith('_'): | |
117 self._used_private_members.add(piece) | |
118 | |
119 if token.type == Type.DOC_FLAG: | |
120 flag = token.attached_object | |
121 | |
122 if flag.flag_type == 'param' and flag.name_token is not None: | |
123 self._CheckForMissingSpaceBeforeToken( | |
124 token.attached_object.name_token) | |
125 | |
126 if flag.type is not None and flag.name is not None: | |
127 if error_check.ShouldCheck(Rule.VARIABLE_ARG_MARKER): | |
128 # Check for variable arguments marker in type. | |
129 if flag.jstype.IsVarArgsType() and flag.name != 'var_args': | |
130 self._HandleError(errors.JSDOC_MISSING_VAR_ARGS_NAME, | |
131 'Variable length argument %s must be renamed ' | |
132 'to var_args.' % flag.name, | |
133 token) | |
134 elif not flag.jstype.IsVarArgsType() and flag.name == 'var_args': | |
135 self._HandleError(errors.JSDOC_MISSING_VAR_ARGS_TYPE, | |
136 'Variable length argument %s type must start ' | |
137 'with \'...\'.' % flag.name, | |
138 token) | |
139 | |
140 if error_check.ShouldCheck(Rule.OPTIONAL_TYPE_MARKER): | |
141 # Check for optional marker in type. | |
142 if (flag.jstype.opt_arg and | |
143 not flag.name.startswith('opt_')): | |
144 self._HandleError(errors.JSDOC_MISSING_OPTIONAL_PREFIX, | |
145 'Optional parameter name %s must be prefixed ' | |
146 'with opt_.' % flag.name, | |
147 token) | |
148 elif (not flag.jstype.opt_arg and | |
149 flag.name.startswith('opt_')): | |
150 self._HandleError(errors.JSDOC_MISSING_OPTIONAL_TYPE, | |
151 'Optional parameter %s type must end with =.' % | |
152 flag.name, | |
153 token) | |
154 | |
155 if flag.flag_type in state.GetDocFlag().HAS_TYPE: | |
156 # Check for both missing type token and empty type braces '{}' | |
157 # Missing suppress types are reported separately and we allow enums, | |
158 # const, private, public and protected without types. | |
159 if (flag.flag_type not in state.GetDocFlag().CAN_OMIT_TYPE | |
160 and (not flag.jstype or flag.jstype.IsEmpty())): | |
161 self._HandleError(errors.MISSING_JSDOC_TAG_TYPE, | |
162 'Missing type in %s tag' % token.string, token) | |
163 | |
164 elif flag.name_token and flag.type_end_token and tokenutil.Compare( | |
165 flag.type_end_token, flag.name_token) > 0: | |
166 self._HandleError( | |
167 errors.OUT_OF_ORDER_JSDOC_TAG_TYPE, | |
168 'Type should be immediately after %s tag' % token.string, | |
169 token) | |
170 | |
171 elif token.type == Type.DOUBLE_QUOTE_STRING_START: | |
172 next_token = token.next | |
173 while next_token.type == Type.STRING_TEXT: | |
174 if javascripttokenizer.JavaScriptTokenizer.SINGLE_QUOTE.search( | |
175 next_token.string): | |
176 break | |
177 next_token = next_token.next | |
178 else: | |
179 self._HandleError( | |
180 errors.UNNECESSARY_DOUBLE_QUOTED_STRING, | |
181 'Single-quoted string preferred over double-quoted string.', | |
182 token, | |
183 position=Position.All(token.string)) | |
184 | |
185 elif token.type == Type.END_DOC_COMMENT: | |
186 doc_comment = state.GetDocComment() | |
187 | |
188 # When @externs appears in a @fileoverview comment, it should trigger | |
189 # the same limited doc checks as a special filename like externs.js. | |
190 if doc_comment.HasFlag('fileoverview') and doc_comment.HasFlag('externs'): | |
191 self._SetLimitedDocChecks(True) | |
192 | |
193 if (error_check.ShouldCheck(Rule.BLANK_LINES_AT_TOP_LEVEL) and | |
194 not self._is_html and | |
195 state.InTopLevel() and | |
196 not state.InNonScopeBlock()): | |
197 | |
198 # Check if we're in a fileoverview or constructor JsDoc. | |
199 is_constructor = ( | |
200 doc_comment.HasFlag('constructor') or | |
201 doc_comment.HasFlag('interface')) | |
202 # @fileoverview is an optional tag so if the dosctring is the first | |
203 # token in the file treat it as a file level docstring. | |
204 is_file_level_comment = ( | |
205 doc_comment.HasFlag('fileoverview') or | |
206 not doc_comment.start_token.previous) | |
207 | |
208 # If the comment is not a file overview, and it does not immediately | |
209 # precede some code, skip it. | |
210 # NOTE: The tokenutil methods are not used here because of their | |
211 # behavior at the top of a file. | |
212 next_token = token.next | |
213 if (not next_token or | |
214 (not is_file_level_comment and | |
215 next_token.type in Type.NON_CODE_TYPES)): | |
216 return | |
217 | |
218 # Don't require extra blank lines around suppression of extra | |
219 # goog.require errors. | |
220 if (doc_comment.SuppressionOnly() and | |
221 next_token.type == Type.IDENTIFIER and | |
222 next_token.string in ['goog.provide', 'goog.require']): | |
223 return | |
224 | |
225 # Find the start of this block (include comments above the block, unless | |
226 # this is a file overview). | |
227 block_start = doc_comment.start_token | |
228 if not is_file_level_comment: | |
229 token = block_start.previous | |
230 while token and token.type in Type.COMMENT_TYPES: | |
231 block_start = token | |
232 token = token.previous | |
233 | |
234 # Count the number of blank lines before this block. | |
235 blank_lines = 0 | |
236 token = block_start.previous | |
237 while token and token.type in [Type.WHITESPACE, Type.BLANK_LINE]: | |
238 if token.type == Type.BLANK_LINE: | |
239 # A blank line. | |
240 blank_lines += 1 | |
241 elif token.type == Type.WHITESPACE and not token.line.strip(): | |
242 # A line with only whitespace on it. | |
243 blank_lines += 1 | |
244 token = token.previous | |
245 | |
246 # Log errors. | |
247 error_message = False | |
248 expected_blank_lines = 0 | |
249 | |
250 # Only need blank line before file overview if it is not the beginning | |
251 # of the file, e.g. copyright is first. | |
252 if is_file_level_comment and blank_lines == 0 and block_start.previous: | |
253 error_message = 'Should have a blank line before a file overview.' | |
254 expected_blank_lines = 1 | |
255 elif is_constructor and blank_lines != 3: | |
256 error_message = ( | |
257 'Should have 3 blank lines before a constructor/interface.') | |
258 expected_blank_lines = 3 | |
259 elif (not is_file_level_comment and not is_constructor and | |
260 blank_lines != 2): | |
261 error_message = 'Should have 2 blank lines between top-level blocks.' | |
262 expected_blank_lines = 2 | |
263 | |
264 if error_message: | |
265 self._HandleError( | |
266 errors.WRONG_BLANK_LINE_COUNT, error_message, | |
267 block_start, position=Position.AtBeginning(), | |
268 fix_data=expected_blank_lines - blank_lines) | |
269 | |
270 elif token.type == Type.END_BLOCK: | |
271 if state.InFunction() and state.IsFunctionClose(): | |
272 is_immediately_called = (token.next and | |
273 token.next.type == Type.START_PAREN) | |
274 | |
275 function = state.GetFunction() | |
276 if not self._limited_doc_checks: | |
277 if (function.has_return and function.doc and | |
278 not is_immediately_called and | |
279 not function.doc.HasFlag('return') and | |
280 not function.doc.InheritsDocumentation() and | |
281 not function.doc.HasFlag('constructor')): | |
282 # Check for proper documentation of return value. | |
283 self._HandleError( | |
284 errors.MISSING_RETURN_DOCUMENTATION, | |
285 'Missing @return JsDoc in function with non-trivial return', | |
286 function.doc.end_token, position=Position.AtBeginning()) | |
287 elif (not function.has_return and | |
288 not function.has_throw and | |
289 function.doc and | |
290 function.doc.HasFlag('return') and | |
291 not state.InInterfaceMethod()): | |
292 flag = function.doc.GetFlag('return') | |
293 valid_no_return_names = ['undefined', 'void', '*'] | |
294 invalid_return = flag.jstype is None or not any( | |
295 sub_type.identifier in valid_no_return_names | |
296 for sub_type in flag.jstype.IterTypeGroup()) | |
297 | |
298 if invalid_return: | |
299 self._HandleError( | |
300 errors.UNNECESSARY_RETURN_DOCUMENTATION, | |
301 'Found @return JsDoc on function that returns nothing', | |
302 flag.flag_token, position=Position.AtBeginning()) | |
303 | |
304 # b/4073735. Method in object literal definition of prototype can | |
305 # safely reference 'this'. | |
306 prototype_object_literal = False | |
307 block_start = None | |
308 previous_code = None | |
309 previous_previous_code = None | |
310 | |
311 # Search for cases where prototype is defined as object literal. | |
312 # previous_previous_code | |
313 # | previous_code | |
314 # | | block_start | |
315 # | | | | |
316 # a.b.prototype = { | |
317 # c : function() { | |
318 # this.d = 1; | |
319 # } | |
320 # } | |
321 | |
322 # If in object literal, find first token of block so to find previous | |
323 # tokens to check above condition. | |
324 if state.InObjectLiteral(): | |
325 block_start = state.GetCurrentBlockStart() | |
326 | |
327 # If an object literal then get previous token (code type). For above | |
328 # case it should be '='. | |
329 if block_start: | |
330 previous_code = tokenutil.SearchExcept(block_start, | |
331 Type.NON_CODE_TYPES, | |
332 reverse=True) | |
333 | |
334 # If previous token to block is '=' then get its previous token. | |
335 if previous_code and previous_code.IsOperator('='): | |
336 previous_previous_code = tokenutil.SearchExcept(previous_code, | |
337 Type.NON_CODE_TYPES, | |
338 reverse=True) | |
339 | |
340 # If variable/token before '=' ends with '.prototype' then its above | |
341 # case of prototype defined with object literal. | |
342 prototype_object_literal = (previous_previous_code and | |
343 previous_previous_code.string.endswith( | |
344 '.prototype')) | |
345 | |
346 if (function.has_this and function.doc and | |
347 not function.doc.HasFlag('this') and | |
348 not function.is_constructor and | |
349 not function.is_interface and | |
350 '.prototype.' not in function.name and | |
351 not prototype_object_literal): | |
352 self._HandleError( | |
353 errors.MISSING_JSDOC_TAG_THIS, | |
354 'Missing @this JsDoc in function referencing "this". (' | |
355 'this usually means you are trying to reference "this" in ' | |
356 'a static function, or you have forgotten to mark a ' | |
357 'constructor with @constructor)', | |
358 function.doc.end_token, position=Position.AtBeginning()) | |
359 | |
360 elif token.type == Type.IDENTIFIER: | |
361 if token.string == 'goog.inherits' and not state.InFunction(): | |
362 if state.GetLastNonSpaceToken().line_number == token.line_number: | |
363 self._HandleError( | |
364 errors.MISSING_LINE, | |
365 'Missing newline between constructor and goog.inherits', | |
366 token, | |
367 position=Position.AtBeginning()) | |
368 | |
369 extra_space = state.GetLastNonSpaceToken().next | |
370 while extra_space != token: | |
371 if extra_space.type == Type.BLANK_LINE: | |
372 self._HandleError( | |
373 errors.EXTRA_LINE, | |
374 'Extra line between constructor and goog.inherits', | |
375 extra_space) | |
376 extra_space = extra_space.next | |
377 | |
378 # TODO(robbyw): Test the last function was a constructor. | |
379 # TODO(robbyw): Test correct @extends and @implements documentation. | |
380 | |
381 elif (token.string == 'goog.provide' and | |
382 not state.InFunction() and | |
383 namespaces_info is not None): | |
384 namespace = tokenutil.GetStringAfterToken(token) | |
385 | |
386 # Report extra goog.provide statement. | |
387 if not namespace or namespaces_info.IsExtraProvide(token): | |
388 if not namespace: | |
389 msg = 'Empty namespace in goog.provide' | |
390 else: | |
391 msg = 'Unnecessary goog.provide: ' + namespace | |
392 | |
393 # Hint to user if this is a Test namespace. | |
394 if namespace.endswith('Test'): | |
395 msg += (' *Test namespaces must be mentioned in the ' | |
396 'goog.setTestOnly() call') | |
397 | |
398 self._HandleError( | |
399 errors.EXTRA_GOOG_PROVIDE, | |
400 msg, | |
401 token, position=Position.AtBeginning()) | |
402 | |
403 if namespaces_info.IsLastProvide(token): | |
404 # Report missing provide statements after the last existing provide. | |
405 missing_provides = namespaces_info.GetMissingProvides() | |
406 if missing_provides: | |
407 self._ReportMissingProvides( | |
408 missing_provides, | |
409 tokenutil.GetLastTokenInSameLine(token).next, | |
410 False) | |
411 | |
412 # If there are no require statements, missing requires should be | |
413 # reported after the last provide. | |
414 if not namespaces_info.GetRequiredNamespaces(): | |
415 missing_requires, illegal_alias_statements = ( | |
416 namespaces_info.GetMissingRequires()) | |
417 if missing_requires: | |
418 self._ReportMissingRequires( | |
419 missing_requires, | |
420 tokenutil.GetLastTokenInSameLine(token).next, | |
421 True) | |
422 if illegal_alias_statements: | |
423 self._ReportIllegalAliasStatement(illegal_alias_statements) | |
424 | |
425 elif (token.string == 'goog.require' and | |
426 not state.InFunction() and | |
427 namespaces_info is not None): | |
428 namespace = tokenutil.GetStringAfterToken(token) | |
429 | |
430 # If there are no provide statements, missing provides should be | |
431 # reported before the first require. | |
432 if (namespaces_info.IsFirstRequire(token) and | |
433 not namespaces_info.GetProvidedNamespaces()): | |
434 missing_provides = namespaces_info.GetMissingProvides() | |
435 if missing_provides: | |
436 self._ReportMissingProvides( | |
437 missing_provides, | |
438 tokenutil.GetFirstTokenInSameLine(token), | |
439 True) | |
440 | |
441 # Report extra goog.require statement. | |
442 if not namespace or namespaces_info.IsExtraRequire(token): | |
443 if not namespace: | |
444 msg = 'Empty namespace in goog.require' | |
445 else: | |
446 msg = 'Unnecessary goog.require: ' + namespace | |
447 | |
448 self._HandleError( | |
449 errors.EXTRA_GOOG_REQUIRE, | |
450 msg, | |
451 token, position=Position.AtBeginning()) | |
452 | |
453 # Report missing goog.require statements. | |
454 if namespaces_info.IsLastRequire(token): | |
455 missing_requires, illegal_alias_statements = ( | |
456 namespaces_info.GetMissingRequires()) | |
457 if missing_requires: | |
458 self._ReportMissingRequires( | |
459 missing_requires, | |
460 tokenutil.GetLastTokenInSameLine(token).next, | |
461 False) | |
462 if illegal_alias_statements: | |
463 self._ReportIllegalAliasStatement(illegal_alias_statements) | |
464 | |
465 elif token.type == Type.OPERATOR: | |
466 last_in_line = token.IsLastInLine() | |
467 # If the token is unary and appears to be used in a unary context | |
468 # it's ok. Otherwise, if it's at the end of the line or immediately | |
469 # before a comment, it's ok. | |
470 # Don't report an error before a start bracket - it will be reported | |
471 # by that token's space checks. | |
472 if (not token.metadata.IsUnaryOperator() and not last_in_line | |
473 and not token.next.IsComment() | |
474 and not token.next.IsOperator(',') | |
475 and not tokenutil.IsDot(token) | |
476 and token.next.type not in (Type.WHITESPACE, Type.END_PAREN, | |
477 Type.END_BRACKET, Type.SEMICOLON, | |
478 Type.START_BRACKET)): | |
479 self._HandleError( | |
480 errors.MISSING_SPACE, | |
481 'Missing space after "%s"' % token.string, | |
482 token, | |
483 position=Position.AtEnd(token.string)) | |
484 elif token.type == Type.WHITESPACE: | |
485 first_in_line = token.IsFirstInLine() | |
486 last_in_line = token.IsLastInLine() | |
487 # Check whitespace length if it's not the first token of the line and | |
488 # if it's not immediately before a comment. | |
489 if not last_in_line and not first_in_line and not token.next.IsComment(): | |
490 # Ensure there is no space after opening parentheses. | |
491 if (token.previous.type in (Type.START_PAREN, Type.START_BRACKET, | |
492 Type.FUNCTION_NAME) | |
493 or token.next.type == Type.START_PARAMETERS): | |
494 self._HandleError( | |
495 errors.EXTRA_SPACE, | |
496 'Extra space after "%s"' % token.previous.string, | |
497 token, | |
498 position=Position.All(token.string)) | |
499 elif token.type == Type.SEMICOLON: | |
500 previous_token = tokenutil.SearchExcept(token, Type.NON_CODE_TYPES, | |
501 reverse=True) | |
502 if not previous_token: | |
503 self._HandleError( | |
504 errors.REDUNDANT_SEMICOLON, | |
505 'Semicolon without any statement', | |
506 token, | |
507 position=Position.AtEnd(token.string)) | |
508 elif (previous_token.type == Type.KEYWORD and | |
509 previous_token.string not in ['break', 'continue', 'return']): | |
510 self._HandleError( | |
511 errors.REDUNDANT_SEMICOLON, | |
512 ('Semicolon after \'%s\' without any statement.' | |
513 ' Looks like an error.' % previous_token.string), | |
514 token, | |
515 position=Position.AtEnd(token.string)) | |
516 | |
517 def _CheckUnusedLocalVariables(self, token, state): | |
518 """Checks for unused local variables in function blocks. | |
519 | |
520 Args: | |
521 token: The token to check. | |
522 state: The state tracker. | |
523 """ | |
524 # We don't use state.InFunction because that disregards scope functions. | |
525 in_function = state.FunctionDepth() > 0 | |
526 if token.type == Type.SIMPLE_LVALUE or token.type == Type.IDENTIFIER: | |
527 if in_function: | |
528 identifier = token.string | |
529 # Check whether the previous token was var. | |
530 previous_code_token = tokenutil.CustomSearch( | |
531 token, | |
532 lambda t: t.type not in Type.NON_CODE_TYPES, | |
533 reverse=True) | |
534 if previous_code_token and previous_code_token.IsKeyword('var'): | |
535 # Add local variable declaration to the top of the unused locals | |
536 # stack. | |
537 self._unused_local_variables_by_scope[-1][identifier] = token | |
538 elif token.type == Type.IDENTIFIER: | |
539 # This covers most cases where the variable is used as an identifier. | |
540 self._MarkLocalVariableUsed(token.string) | |
541 elif token.type == Type.SIMPLE_LVALUE and '.' in identifier: | |
542 # This covers cases where a value is assigned to a property of the | |
543 # variable. | |
544 self._MarkLocalVariableUsed(token.string) | |
545 elif token.type == Type.START_BLOCK: | |
546 if in_function and state.IsFunctionOpen(): | |
547 # Push a new map onto the stack | |
548 self._unused_local_variables_by_scope.append({}) | |
549 elif token.type == Type.END_BLOCK: | |
550 if state.IsFunctionClose(): | |
551 # Pop the stack and report any remaining locals as unused. | |
552 unused_local_variables = self._unused_local_variables_by_scope.pop() | |
553 for unused_token in unused_local_variables.values(): | |
554 self._HandleError( | |
555 errors.UNUSED_LOCAL_VARIABLE, | |
556 'Unused local variable: %s.' % unused_token.string, | |
557 unused_token) | |
558 elif token.type == Type.DOC_FLAG: | |
559 # Flags that use aliased symbols should be counted. | |
560 flag = token.attached_object | |
561 js_type = flag and flag.jstype | |
562 if flag and flag.flag_type in state.GetDocFlag().HAS_TYPE and js_type: | |
563 self._MarkAliasUsed(js_type) | |
564 | |
565 def _MarkAliasUsed(self, js_type): | |
566 """Marks aliases in a type as used. | |
567 | |
568 Recursively iterates over all subtypes in a jsdoc type annotation and | |
569 tracks usage of aliased symbols (which may be local variables). | |
570 Marks the local variable as used in the scope nearest to the current | |
571 scope that matches the given token. | |
572 | |
573 Args: | |
574 js_type: The jsdoc type, a typeannotation.TypeAnnotation object. | |
575 """ | |
576 if js_type.alias: | |
577 self._MarkLocalVariableUsed(js_type.identifier) | |
578 for sub_type in js_type.IterTypes(): | |
579 self._MarkAliasUsed(sub_type) | |
580 | |
581 def _MarkLocalVariableUsed(self, identifier): | |
582 """Marks the local variable as used in the relevant scope. | |
583 | |
584 Marks the local variable in the scope nearest to the current scope that | |
585 matches the given identifier as used. | |
586 | |
587 Args: | |
588 identifier: The identifier representing the potential usage of a local | |
589 variable. | |
590 """ | |
591 identifier = identifier.split('.', 1)[0] | |
592 # Find the first instance of the identifier in the stack of function scopes | |
593 # and mark it used. | |
594 for unused_local_variables in reversed( | |
595 self._unused_local_variables_by_scope): | |
596 if identifier in unused_local_variables: | |
597 del unused_local_variables[identifier] | |
598 break | |
599 | |
600 def _ReportMissingProvides(self, missing_provides, token, need_blank_line): | |
601 """Reports missing provide statements to the error handler. | |
602 | |
603 Args: | |
604 missing_provides: A dictionary of string(key) and integer(value) where | |
605 each string(key) is a namespace that should be provided, but is not | |
606 and integer(value) is first line number where it's required. | |
607 token: The token where the error was detected (also where the new provides | |
608 will be inserted. | |
609 need_blank_line: Whether a blank line needs to be inserted after the new | |
610 provides are inserted. May be True, False, or None, where None | |
611 indicates that the insert location is unknown. | |
612 """ | |
613 | |
614 missing_provides_msg = 'Missing the following goog.provide statements:\n' | |
615 missing_provides_msg += '\n'.join(['goog.provide(\'%s\');' % x for x in | |
616 sorted(missing_provides)]) | |
617 missing_provides_msg += '\n' | |
618 | |
619 missing_provides_msg += '\nFirst line where provided: \n' | |
620 missing_provides_msg += '\n'.join( | |
621 [' %s : line %d' % (x, missing_provides[x]) for x in | |
622 sorted(missing_provides)]) | |
623 missing_provides_msg += '\n' | |
624 | |
625 self._HandleError( | |
626 errors.MISSING_GOOG_PROVIDE, | |
627 missing_provides_msg, | |
628 token, position=Position.AtBeginning(), | |
629 fix_data=(missing_provides.keys(), need_blank_line)) | |
630 | |
631 def _ReportMissingRequires(self, missing_requires, token, need_blank_line): | |
632 """Reports missing require statements to the error handler. | |
633 | |
634 Args: | |
635 missing_requires: A dictionary of string(key) and integer(value) where | |
636 each string(key) is a namespace that should be required, but is not | |
637 and integer(value) is first line number where it's required. | |
638 token: The token where the error was detected (also where the new requires | |
639 will be inserted. | |
640 need_blank_line: Whether a blank line needs to be inserted before the new | |
641 requires are inserted. May be True, False, or None, where None | |
642 indicates that the insert location is unknown. | |
643 """ | |
644 | |
645 missing_requires_msg = 'Missing the following goog.require statements:\n' | |
646 missing_requires_msg += '\n'.join(['goog.require(\'%s\');' % x for x in | |
647 sorted(missing_requires)]) | |
648 missing_requires_msg += '\n' | |
649 | |
650 missing_requires_msg += '\nFirst line where required: \n' | |
651 missing_requires_msg += '\n'.join( | |
652 [' %s : line %d' % (x, missing_requires[x]) for x in | |
653 sorted(missing_requires)]) | |
654 missing_requires_msg += '\n' | |
655 | |
656 self._HandleError( | |
657 errors.MISSING_GOOG_REQUIRE, | |
658 missing_requires_msg, | |
659 token, position=Position.AtBeginning(), | |
660 fix_data=(missing_requires.keys(), need_blank_line)) | |
661 | |
662 def _ReportIllegalAliasStatement(self, illegal_alias_statements): | |
663 """Reports alias statements that would need a goog.require.""" | |
664 for namespace, token in illegal_alias_statements.iteritems(): | |
665 self._HandleError( | |
666 errors.ALIAS_STMT_NEEDS_GOOG_REQUIRE, | |
667 'The alias definition would need the namespace \'%s\' which is not ' | |
668 'required through any other symbol.' % namespace, | |
669 token, position=Position.AtBeginning()) | |
670 | |
671 def Finalize(self, state): | |
672 """Perform all checks that need to occur after all lines are processed.""" | |
673 # Call the base class's Finalize function. | |
674 super(JavaScriptLintRules, self).Finalize(state) | |
675 | |
676 if error_check.ShouldCheck(Rule.UNUSED_PRIVATE_MEMBERS): | |
677 # Report an error for any declared private member that was never used. | |
678 unused_private_members = (self._declared_private_members - | |
679 self._used_private_members) | |
680 | |
681 for variable in unused_private_members: | |
682 token = self._declared_private_member_tokens[variable] | |
683 self._HandleError(errors.UNUSED_PRIVATE_MEMBER, | |
684 'Unused private member: %s.' % token.string, | |
685 token) | |
686 | |
687 # Clear state to prepare for the next file. | |
688 self._declared_private_member_tokens = {} | |
689 self._declared_private_members = set() | |
690 self._used_private_members = set() | |
691 | |
692 namespaces_info = self._namespaces_info | |
693 if namespaces_info is not None: | |
694 # If there are no provide or require statements, missing provides and | |
695 # requires should be reported on line 1. | |
696 if (not namespaces_info.GetProvidedNamespaces() and | |
697 not namespaces_info.GetRequiredNamespaces()): | |
698 missing_provides = namespaces_info.GetMissingProvides() | |
699 if missing_provides: | |
700 self._ReportMissingProvides( | |
701 missing_provides, state.GetFirstToken(), None) | |
702 | |
703 missing_requires, illegal_alias = namespaces_info.GetMissingRequires() | |
704 if missing_requires: | |
705 self._ReportMissingRequires( | |
706 missing_requires, state.GetFirstToken(), None) | |
707 if illegal_alias: | |
708 self._ReportIllegalAliasStatement(illegal_alias) | |
709 | |
710 self._CheckSortedRequiresProvides(state.GetFirstToken()) | |
711 | |
712 def _CheckSortedRequiresProvides(self, token): | |
713 """Checks that all goog.require and goog.provide statements are sorted. | |
714 | |
715 Note that this method needs to be run after missing statements are added to | |
716 preserve alphabetical order. | |
717 | |
718 Args: | |
719 token: The first token in the token stream. | |
720 """ | |
721 sorter = requireprovidesorter.RequireProvideSorter() | |
722 first_provide_token = sorter.CheckProvides(token) | |
723 if first_provide_token: | |
724 new_order = sorter.GetFixedProvideString(first_provide_token) | |
725 self._HandleError( | |
726 errors.GOOG_PROVIDES_NOT_ALPHABETIZED, | |
727 'goog.provide classes must be alphabetized. The correct code is:\n' + | |
728 new_order, | |
729 first_provide_token, | |
730 position=Position.AtBeginning(), | |
731 fix_data=first_provide_token) | |
732 | |
733 first_require_token = sorter.CheckRequires(token) | |
734 if first_require_token: | |
735 new_order = sorter.GetFixedRequireString(first_require_token) | |
736 self._HandleError( | |
737 errors.GOOG_REQUIRES_NOT_ALPHABETIZED, | |
738 'goog.require classes must be alphabetized. The correct code is:\n' + | |
739 new_order, | |
740 first_require_token, | |
741 position=Position.AtBeginning(), | |
742 fix_data=first_require_token) | |
743 | |
744 def GetLongLineExceptions(self): | |
745 """Gets a list of regexps for lines which can be longer than the limit. | |
746 | |
747 Returns: | |
748 A list of regexps, used as matches (rather than searches). | |
749 """ | |
750 return [ | |
751 re.compile(r'((var|let|const) .+\s*=\s*)?goog\.require\(.+\);?\s*$'), | |
752 re.compile(r'goog\.(forwardDeclare|module|provide|setTestOnly)' | |
753 r'\(.+\);?\s*$'), | |
754 re.compile(r'[\s/*]*@visibility\s*{.*}[\s*/]*$'), | |
755 ] | |
OLD | NEW |