| OLD | NEW |
| (Empty) |
| 1 #!/usr/bin/env python | |
| 2 # | |
| 3 # Copyright 2008 The Closure Linter Authors. All Rights Reserved. | |
| 4 # | |
| 5 # Licensed under the Apache License, Version 2.0 (the "License"); | |
| 6 # you may not use this file except in compliance with the License. | |
| 7 # You may obtain a copy of the License at | |
| 8 # | |
| 9 # http://www.apache.org/licenses/LICENSE-2.0 | |
| 10 # | |
| 11 # Unless required by applicable law or agreed to in writing, software | |
| 12 # distributed under the License is distributed on an "AS-IS" BASIS, | |
| 13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 14 # See the License for the specific language governing permissions and | |
| 15 # limitations under the License. | |
| 16 | |
| 17 """Core methods for checking EcmaScript files for common style guide violations. | |
| 18 """ | |
| 19 | |
| 20 __author__ = ('robbyw@google.com (Robert Walker)', | |
| 21 'ajp@google.com (Andy Perelson)', | |
| 22 'jacobr@google.com (Jacob Richman)') | |
| 23 | |
| 24 import re | |
| 25 | |
| 26 import gflags as flags | |
| 27 | |
| 28 from closure_linter import checkerbase | |
| 29 from closure_linter import ecmametadatapass | |
| 30 from closure_linter import error_check | |
| 31 from closure_linter import errorrules | |
| 32 from closure_linter import errors | |
| 33 from closure_linter import indentation | |
| 34 from closure_linter import javascripttokenizer | |
| 35 from closure_linter import javascripttokens | |
| 36 from closure_linter import statetracker | |
| 37 from closure_linter import tokenutil | |
| 38 from closure_linter.common import error | |
| 39 from closure_linter.common import position | |
| 40 | |
| 41 | |
| 42 FLAGS = flags.FLAGS | |
| 43 flags.DEFINE_list('custom_jsdoc_tags', '', 'Extra jsdoc tags to allow') | |
| 44 # TODO(user): When flipping this to True, remove logic from unit tests | |
| 45 # that overrides this flag. | |
| 46 flags.DEFINE_boolean('dot_on_next_line', False, 'Require dots to be' | |
| 47 'placed on the next line for wrapped expressions') | |
| 48 | |
| 49 flags.DEFINE_boolean('check_trailing_comma', False, 'Check trailing commas' | |
| 50 ' (ES3, not needed from ES5 onwards)') | |
| 51 | |
| 52 # TODO(robbyw): Check for extra parens on return statements | |
| 53 # TODO(robbyw): Check for 0px in strings | |
| 54 # TODO(robbyw): Ensure inline jsDoc is in {} | |
| 55 # TODO(robbyw): Check for valid JS types in parameter docs | |
| 56 | |
| 57 # Shorthand | |
| 58 Context = ecmametadatapass.EcmaContext | |
| 59 Error = error.Error | |
| 60 Modes = javascripttokenizer.JavaScriptModes | |
| 61 Position = position.Position | |
| 62 Rule = error_check.Rule | |
| 63 Type = javascripttokens.JavaScriptTokenType | |
| 64 | |
| 65 | |
| 66 class EcmaScriptLintRules(checkerbase.LintRulesBase): | |
| 67 """EmcaScript lint style checking rules. | |
| 68 | |
| 69 Can be used to find common style errors in JavaScript, ActionScript and other | |
| 70 Ecma like scripting languages. Style checkers for Ecma scripting languages | |
| 71 should inherit from this style checker. | |
| 72 Please do not add any state to EcmaScriptLintRules or to any subclasses. | |
| 73 | |
| 74 All state should be added to the StateTracker subclass used for a particular | |
| 75 language. | |
| 76 """ | |
| 77 | |
| 78 # It will be initialized in constructor so the flags are initialized. | |
| 79 max_line_length = -1 | |
| 80 | |
| 81 # Static constants. | |
| 82 MISSING_PARAMETER_SPACE = re.compile(r',\S') | |
| 83 | |
| 84 EXTRA_SPACE = re.compile(r'(\(\s|\s\))') | |
| 85 | |
| 86 ENDS_WITH_SPACE = re.compile(r'\s$') | |
| 87 | |
| 88 ILLEGAL_TAB = re.compile(r'\t') | |
| 89 | |
| 90 # Regex used to split up complex types to check for invalid use of ? and |. | |
| 91 TYPE_SPLIT = re.compile(r'[,<>()]') | |
| 92 | |
| 93 # Regex for form of author lines after the @author tag. | |
| 94 AUTHOR_SPEC = re.compile(r'(\s*)[^\s]+@[^(\s]+(\s*)\(.+\)') | |
| 95 | |
| 96 # Acceptable tokens to remove for line too long testing. | |
| 97 LONG_LINE_IGNORE = frozenset( | |
| 98 ['*', '//', '@see'] + | |
| 99 ['@%s' % tag for tag in statetracker.DocFlag.HAS_TYPE]) | |
| 100 | |
| 101 JSDOC_FLAGS_DESCRIPTION_NOT_REQUIRED = frozenset([ | |
| 102 '@fileoverview', '@param', '@return', '@returns']) | |
| 103 | |
| 104 def __init__(self): | |
| 105 """Initialize this lint rule object.""" | |
| 106 checkerbase.LintRulesBase.__init__(self) | |
| 107 if EcmaScriptLintRules.max_line_length == -1: | |
| 108 EcmaScriptLintRules.max_line_length = errorrules.GetMaxLineLength() | |
| 109 | |
| 110 def Initialize(self, checker, limited_doc_checks, is_html): | |
| 111 """Initialize this lint rule object before parsing a new file.""" | |
| 112 checkerbase.LintRulesBase.Initialize(self, checker, limited_doc_checks, | |
| 113 is_html) | |
| 114 self._indentation = indentation.IndentationRules() | |
| 115 | |
| 116 def HandleMissingParameterDoc(self, token, param_name): | |
| 117 """Handle errors associated with a parameter missing a @param tag.""" | |
| 118 raise TypeError('Abstract method HandleMissingParameterDoc not implemented') | |
| 119 | |
| 120 def _CheckLineLength(self, last_token, state): | |
| 121 """Checks whether the line is too long. | |
| 122 | |
| 123 Args: | |
| 124 last_token: The last token in the line. | |
| 125 state: parser_state object that indicates the current state in the page | |
| 126 """ | |
| 127 # Start from the last token so that we have the flag object attached to | |
| 128 # and DOC_FLAG tokens. | |
| 129 line_number = last_token.line_number | |
| 130 token = last_token | |
| 131 | |
| 132 # Build a representation of the string where spaces indicate potential | |
| 133 # line-break locations. | |
| 134 line = [] | |
| 135 while token and token.line_number == line_number: | |
| 136 if state.IsTypeToken(token): | |
| 137 line.insert(0, 'x' * len(token.string)) | |
| 138 elif token.type in (Type.IDENTIFIER, Type.OPERATOR): | |
| 139 # Dots are acceptable places to wrap (may be tokenized as identifiers). | |
| 140 line.insert(0, token.string.replace('.', ' ')) | |
| 141 else: | |
| 142 line.insert(0, token.string) | |
| 143 token = token.previous | |
| 144 | |
| 145 line = ''.join(line) | |
| 146 line = line.rstrip('\n\r\f') | |
| 147 try: | |
| 148 length = len(unicode(line, 'utf-8')) | |
| 149 except (LookupError, UnicodeDecodeError): | |
| 150 # Unknown encoding. The line length may be wrong, as was originally the | |
| 151 # case for utf-8 (see bug 1735846). For now just accept the default | |
| 152 # length, but as we find problems we can either add test for other | |
| 153 # possible encodings or return without an error to protect against | |
| 154 # false positives at the cost of more false negatives. | |
| 155 length = len(line) | |
| 156 | |
| 157 if length > EcmaScriptLintRules.max_line_length: | |
| 158 | |
| 159 # If the line matches one of the exceptions, then it's ok. | |
| 160 for long_line_regexp in self.GetLongLineExceptions(): | |
| 161 if long_line_regexp.match(last_token.line): | |
| 162 return | |
| 163 | |
| 164 # If the line consists of only one "word", or multiple words but all | |
| 165 # except one are ignoreable, then it's ok. | |
| 166 parts = set(line.split()) | |
| 167 | |
| 168 # We allow two "words" (type and name) when the line contains @param | |
| 169 max_parts = 1 | |
| 170 if '@param' in parts: | |
| 171 max_parts = 2 | |
| 172 | |
| 173 # Custom tags like @requires may have url like descriptions, so ignore | |
| 174 # the tag, similar to how we handle @see. | |
| 175 custom_tags = set(['@%s' % f for f in FLAGS.custom_jsdoc_tags]) | |
| 176 if (len(parts.difference(self.LONG_LINE_IGNORE | custom_tags)) | |
| 177 > max_parts): | |
| 178 self._HandleError( | |
| 179 errors.LINE_TOO_LONG, | |
| 180 'Line too long (%d characters).' % len(line), last_token) | |
| 181 | |
| 182 def _CheckJsDocType(self, token, js_type): | |
| 183 """Checks the given type for style errors. | |
| 184 | |
| 185 Args: | |
| 186 token: The DOC_FLAG token for the flag whose type to check. | |
| 187 js_type: The flag's typeannotation.TypeAnnotation instance. | |
| 188 """ | |
| 189 if not js_type: return | |
| 190 | |
| 191 if js_type.type_group and len(js_type.sub_types) == 2: | |
| 192 identifiers = [t.identifier for t in js_type.sub_types] | |
| 193 if 'null' in identifiers: | |
| 194 # Don't warn if the identifier is a template type (e.g. {TYPE|null}. | |
| 195 if not identifiers[0].isupper() and not identifiers[1].isupper(): | |
| 196 self._HandleError( | |
| 197 errors.JSDOC_PREFER_QUESTION_TO_PIPE_NULL, | |
| 198 'Prefer "?Type" to "Type|null": "%s"' % js_type, token) | |
| 199 | |
| 200 # TODO(user): We should report an error for wrong usage of '?' and '|' | |
| 201 # e.g. {?number|string|null} etc. | |
| 202 | |
| 203 for sub_type in js_type.IterTypes(): | |
| 204 self._CheckJsDocType(token, sub_type) | |
| 205 | |
| 206 def _CheckForMissingSpaceBeforeToken(self, token): | |
| 207 """Checks for a missing space at the beginning of a token. | |
| 208 | |
| 209 Reports a MISSING_SPACE error if the token does not begin with a space or | |
| 210 the previous token doesn't end with a space and the previous token is on the | |
| 211 same line as the token. | |
| 212 | |
| 213 Args: | |
| 214 token: The token being checked | |
| 215 """ | |
| 216 # TODO(user): Check if too many spaces? | |
| 217 if (len(token.string) == len(token.string.lstrip()) and | |
| 218 token.previous and token.line_number == token.previous.line_number and | |
| 219 len(token.previous.string) - len(token.previous.string.rstrip()) == 0): | |
| 220 self._HandleError( | |
| 221 errors.MISSING_SPACE, | |
| 222 'Missing space before "%s"' % token.string, | |
| 223 token, | |
| 224 position=Position.AtBeginning()) | |
| 225 | |
| 226 def _CheckOperator(self, token): | |
| 227 """Checks an operator for spacing and line style. | |
| 228 | |
| 229 Args: | |
| 230 token: The operator token. | |
| 231 """ | |
| 232 last_code = token.metadata.last_code | |
| 233 | |
| 234 if not self._ExpectSpaceBeforeOperator(token): | |
| 235 if (token.previous and token.previous.type == Type.WHITESPACE and | |
| 236 last_code and last_code.type in (Type.NORMAL, Type.IDENTIFIER) and | |
| 237 last_code.line_number == token.line_number): | |
| 238 self._HandleError( | |
| 239 errors.EXTRA_SPACE, 'Extra space before "%s"' % token.string, | |
| 240 token.previous, position=Position.All(token.previous.string)) | |
| 241 | |
| 242 elif (token.previous and | |
| 243 not token.previous.IsComment() and | |
| 244 not tokenutil.IsDot(token) and | |
| 245 token.previous.type in Type.EXPRESSION_ENDER_TYPES): | |
| 246 self._HandleError(errors.MISSING_SPACE, | |
| 247 'Missing space before "%s"' % token.string, token, | |
| 248 position=Position.AtBeginning()) | |
| 249 | |
| 250 # Check wrapping of operators. | |
| 251 next_code = tokenutil.GetNextCodeToken(token) | |
| 252 | |
| 253 is_dot = tokenutil.IsDot(token) | |
| 254 wrapped_before = last_code and last_code.line_number != token.line_number | |
| 255 wrapped_after = next_code and next_code.line_number != token.line_number | |
| 256 | |
| 257 if FLAGS.dot_on_next_line and is_dot and wrapped_after: | |
| 258 self._HandleError( | |
| 259 errors.LINE_ENDS_WITH_DOT, | |
| 260 '"." must go on the following line', | |
| 261 token) | |
| 262 if (not is_dot and wrapped_before and | |
| 263 not token.metadata.IsUnaryOperator()): | |
| 264 self._HandleError( | |
| 265 errors.LINE_STARTS_WITH_OPERATOR, | |
| 266 'Binary operator must go on previous line "%s"' % token.string, | |
| 267 token) | |
| 268 | |
| 269 def _IsLabel(self, token): | |
| 270 # A ':' token is considered part of a label if it occurs in a case | |
| 271 # statement, a plain label, or an object literal, i.e. is not part of a | |
| 272 # ternary. | |
| 273 | |
| 274 return (token.string == ':' and | |
| 275 token.metadata.context.type in (Context.LITERAL_ELEMENT, | |
| 276 Context.CASE_BLOCK, | |
| 277 Context.STATEMENT)) | |
| 278 | |
| 279 def _ExpectSpaceBeforeOperator(self, token): | |
| 280 """Returns whether a space should appear before the given operator token. | |
| 281 | |
| 282 Args: | |
| 283 token: The operator token. | |
| 284 | |
| 285 Returns: | |
| 286 Whether there should be a space before the token. | |
| 287 """ | |
| 288 if token.string == ',' or token.metadata.IsUnaryPostOperator(): | |
| 289 return False | |
| 290 | |
| 291 if tokenutil.IsDot(token): | |
| 292 return False | |
| 293 | |
| 294 # Colons should appear in labels, object literals, the case of a switch | |
| 295 # statement, and ternary operator. Only want a space in the case of the | |
| 296 # ternary operator. | |
| 297 if self._IsLabel(token): | |
| 298 return False | |
| 299 | |
| 300 if token.metadata.IsUnaryOperator() and token.IsFirstInLine(): | |
| 301 return False | |
| 302 | |
| 303 return True | |
| 304 | |
| 305 def CheckToken(self, token, state): | |
| 306 """Checks a token, given the current parser_state, for warnings and errors. | |
| 307 | |
| 308 Args: | |
| 309 token: The current token under consideration | |
| 310 state: parser_state object that indicates the current state in the page | |
| 311 """ | |
| 312 # Store some convenience variables | |
| 313 first_in_line = token.IsFirstInLine() | |
| 314 last_in_line = token.IsLastInLine() | |
| 315 last_non_space_token = state.GetLastNonSpaceToken() | |
| 316 | |
| 317 token_type = token.type | |
| 318 | |
| 319 # Process the line change. | |
| 320 if not self._is_html and error_check.ShouldCheck(Rule.INDENTATION): | |
| 321 # TODO(robbyw): Support checking indentation in HTML files. | |
| 322 indentation_errors = self._indentation.CheckToken(token, state) | |
| 323 for indentation_error in indentation_errors: | |
| 324 self._HandleError(*indentation_error) | |
| 325 | |
| 326 if last_in_line: | |
| 327 self._CheckLineLength(token, state) | |
| 328 | |
| 329 if token_type == Type.PARAMETERS: | |
| 330 # Find missing spaces in parameter lists. | |
| 331 if self.MISSING_PARAMETER_SPACE.search(token.string): | |
| 332 fix_data = ', '.join([s.strip() for s in token.string.split(',')]) | |
| 333 self._HandleError(errors.MISSING_SPACE, 'Missing space after ","', | |
| 334 token, position=None, fix_data=fix_data.strip()) | |
| 335 | |
| 336 # Find extra spaces at the beginning of parameter lists. Make sure | |
| 337 # we aren't at the beginning of a continuing multi-line list. | |
| 338 if not first_in_line: | |
| 339 space_count = len(token.string) - len(token.string.lstrip()) | |
| 340 if space_count: | |
| 341 self._HandleError(errors.EXTRA_SPACE, 'Extra space after "("', | |
| 342 token, position=Position(0, space_count)) | |
| 343 | |
| 344 elif (token_type == Type.START_BLOCK and | |
| 345 token.metadata.context.type == Context.BLOCK): | |
| 346 self._CheckForMissingSpaceBeforeToken(token) | |
| 347 | |
| 348 elif token_type == Type.END_BLOCK: | |
| 349 last_code = token.metadata.last_code | |
| 350 | |
| 351 if FLAGS.check_trailing_comma: | |
| 352 if last_code.IsOperator(','): | |
| 353 self._HandleError( | |
| 354 errors.COMMA_AT_END_OF_LITERAL, | |
| 355 'Illegal comma at end of object literal', last_code, | |
| 356 position=Position.All(last_code.string)) | |
| 357 | |
| 358 if state.InFunction() and state.IsFunctionClose(): | |
| 359 if state.InTopLevelFunction(): | |
| 360 # A semicolons should not be included at the end of a function | |
| 361 # declaration. | |
| 362 if not state.InAssignedFunction(): | |
| 363 if not last_in_line and token.next.type == Type.SEMICOLON: | |
| 364 self._HandleError( | |
| 365 errors.ILLEGAL_SEMICOLON_AFTER_FUNCTION, | |
| 366 'Illegal semicolon after function declaration', | |
| 367 token.next, position=Position.All(token.next.string)) | |
| 368 | |
| 369 # A semicolon should be included at the end of a function expression | |
| 370 # that is not immediately called or used by a dot operator. | |
| 371 if (state.InAssignedFunction() and token.next | |
| 372 and token.next.type != Type.SEMICOLON): | |
| 373 next_token = tokenutil.GetNextCodeToken(token) | |
| 374 is_immediately_used = next_token and ( | |
| 375 next_token.type == Type.START_PAREN or | |
| 376 tokenutil.IsDot(next_token)) | |
| 377 if not is_immediately_used: | |
| 378 self._HandleError( | |
| 379 errors.MISSING_SEMICOLON_AFTER_FUNCTION, | |
| 380 'Missing semicolon after function assigned to a variable', | |
| 381 token, position=Position.AtEnd(token.string)) | |
| 382 | |
| 383 if state.InInterfaceMethod() and last_code.type != Type.START_BLOCK: | |
| 384 self._HandleError(errors.INTERFACE_METHOD_CANNOT_HAVE_CODE, | |
| 385 'Interface methods cannot contain code', last_code) | |
| 386 | |
| 387 elif (state.IsBlockClose() and | |
| 388 token.next and token.next.type == Type.SEMICOLON): | |
| 389 if (last_code.metadata.context.parent.type != Context.OBJECT_LITERAL | |
| 390 and last_code.metadata.context.type != Context.OBJECT_LITERAL): | |
| 391 self._HandleError( | |
| 392 errors.REDUNDANT_SEMICOLON, | |
| 393 'No semicolon is required to end a code block', | |
| 394 token.next, position=Position.All(token.next.string)) | |
| 395 | |
| 396 elif token_type == Type.SEMICOLON: | |
| 397 if token.previous and token.previous.type == Type.WHITESPACE: | |
| 398 self._HandleError( | |
| 399 errors.EXTRA_SPACE, 'Extra space before ";"', | |
| 400 token.previous, position=Position.All(token.previous.string)) | |
| 401 | |
| 402 if token.next and token.next.line_number == token.line_number: | |
| 403 if token.metadata.context.type != Context.FOR_GROUP_BLOCK: | |
| 404 # TODO(robbyw): Error about no multi-statement lines. | |
| 405 pass | |
| 406 | |
| 407 elif token.next.type not in ( | |
| 408 Type.WHITESPACE, Type.SEMICOLON, Type.END_PAREN): | |
| 409 self._HandleError( | |
| 410 errors.MISSING_SPACE, | |
| 411 'Missing space after ";" in for statement', | |
| 412 token.next, | |
| 413 position=Position.AtBeginning()) | |
| 414 | |
| 415 last_code = token.metadata.last_code | |
| 416 if last_code and last_code.type == Type.SEMICOLON: | |
| 417 # Allow a single double semi colon in for loops for cases like: | |
| 418 # for (;;) { }. | |
| 419 # NOTE(user): This is not a perfect check, and will not throw an error | |
| 420 # for cases like: for (var i = 0;; i < n; i++) {}, but then your code | |
| 421 # probably won't work either. | |
| 422 for_token = tokenutil.CustomSearch( | |
| 423 last_code, | |
| 424 lambda token: token.type == Type.KEYWORD and token.string == 'for', | |
| 425 end_func=lambda token: token.type == Type.SEMICOLON, | |
| 426 distance=None, | |
| 427 reverse=True) | |
| 428 | |
| 429 if not for_token: | |
| 430 self._HandleError(errors.REDUNDANT_SEMICOLON, 'Redundant semicolon', | |
| 431 token, position=Position.All(token.string)) | |
| 432 | |
| 433 elif token_type == Type.START_PAREN: | |
| 434 # Ensure that opening parentheses have a space before any keyword | |
| 435 # that is not being invoked like a member function. | |
| 436 if (token.previous and token.previous.type == Type.KEYWORD and | |
| 437 (not token.previous.metadata or | |
| 438 not token.previous.metadata.last_code or | |
| 439 not token.previous.metadata.last_code.string or | |
| 440 token.previous.metadata.last_code.string[-1:] != '.')): | |
| 441 self._HandleError(errors.MISSING_SPACE, 'Missing space before "("', | |
| 442 token, position=Position.AtBeginning()) | |
| 443 elif token.previous and token.previous.type == Type.WHITESPACE: | |
| 444 before_space = token.previous.previous | |
| 445 # Ensure that there is no extra space before a function invocation, | |
| 446 # even if the function being invoked happens to be a keyword. | |
| 447 if (before_space and before_space.line_number == token.line_number and | |
| 448 before_space.type == Type.IDENTIFIER or | |
| 449 (before_space.type == Type.KEYWORD and before_space.metadata and | |
| 450 before_space.metadata.last_code and | |
| 451 before_space.metadata.last_code.string and | |
| 452 before_space.metadata.last_code.string[-1:] == '.')): | |
| 453 self._HandleError( | |
| 454 errors.EXTRA_SPACE, 'Extra space before "("', | |
| 455 token.previous, position=Position.All(token.previous.string)) | |
| 456 | |
| 457 elif token_type == Type.START_BRACKET: | |
| 458 self._HandleStartBracket(token, last_non_space_token) | |
| 459 elif token_type in (Type.END_PAREN, Type.END_BRACKET): | |
| 460 # Ensure there is no space before closing parentheses, except when | |
| 461 # it's in a for statement with an omitted section, or when it's at the | |
| 462 # beginning of a line. | |
| 463 | |
| 464 last_code = token.metadata.last_code | |
| 465 if FLAGS.check_trailing_comma and token_type == Type.END_BRACKET: | |
| 466 if last_code.IsOperator(','): | |
| 467 self._HandleError( | |
| 468 errors.COMMA_AT_END_OF_LITERAL, | |
| 469 'Illegal comma at end of array literal', last_code, | |
| 470 position=Position.All(last_code.string)) | |
| 471 | |
| 472 if (token.previous and token.previous.type == Type.WHITESPACE and | |
| 473 not token.previous.IsFirstInLine() and | |
| 474 not (last_non_space_token and last_non_space_token.line_number == | |
| 475 token.line_number and | |
| 476 last_non_space_token.type == Type.SEMICOLON)): | |
| 477 self._HandleError( | |
| 478 errors.EXTRA_SPACE, 'Extra space before "%s"' % | |
| 479 token.string, token.previous, | |
| 480 position=Position.All(token.previous.string)) | |
| 481 | |
| 482 elif token_type == Type.WHITESPACE: | |
| 483 if self.ILLEGAL_TAB.search(token.string): | |
| 484 if token.IsFirstInLine(): | |
| 485 if token.next: | |
| 486 self._HandleError( | |
| 487 errors.ILLEGAL_TAB, | |
| 488 'Illegal tab in whitespace before "%s"' % token.next.string, | |
| 489 token, position=Position.All(token.string)) | |
| 490 else: | |
| 491 self._HandleError( | |
| 492 errors.ILLEGAL_TAB, | |
| 493 'Illegal tab in whitespace', | |
| 494 token, position=Position.All(token.string)) | |
| 495 else: | |
| 496 self._HandleError( | |
| 497 errors.ILLEGAL_TAB, | |
| 498 'Illegal tab in whitespace after "%s"' % token.previous.string, | |
| 499 token, position=Position.All(token.string)) | |
| 500 | |
| 501 # Check whitespace length if it's not the first token of the line and | |
| 502 # if it's not immediately before a comment. | |
| 503 if last_in_line: | |
| 504 # Check for extra whitespace at the end of a line. | |
| 505 self._HandleError(errors.EXTRA_SPACE, 'Extra space at end of line', | |
| 506 token, position=Position.All(token.string)) | |
| 507 elif not first_in_line and not token.next.IsComment(): | |
| 508 if token.length > 1: | |
| 509 self._HandleError( | |
| 510 errors.EXTRA_SPACE, 'Extra space after "%s"' % | |
| 511 token.previous.string, token, | |
| 512 position=Position(1, len(token.string) - 1)) | |
| 513 | |
| 514 elif token_type == Type.OPERATOR: | |
| 515 self._CheckOperator(token) | |
| 516 elif token_type == Type.DOC_FLAG: | |
| 517 flag = token.attached_object | |
| 518 | |
| 519 if flag.flag_type == 'bug': | |
| 520 # TODO(robbyw): Check for exactly 1 space on the left. | |
| 521 string = token.next.string.lstrip() | |
| 522 string = string.split(' ', 1)[0] | |
| 523 | |
| 524 if not string.isdigit(): | |
| 525 self._HandleError(errors.NO_BUG_NUMBER_AFTER_BUG_TAG, | |
| 526 '@bug should be followed by a bug number', token) | |
| 527 | |
| 528 elif flag.flag_type == 'suppress': | |
| 529 if flag.type is None: | |
| 530 # A syntactically invalid suppress tag will get tokenized as a normal | |
| 531 # flag, indicating an error. | |
| 532 self._HandleError( | |
| 533 errors.INCORRECT_SUPPRESS_SYNTAX, | |
| 534 'Invalid suppress syntax: should be @suppress {errortype}. ' | |
| 535 'Spaces matter.', token) | |
| 536 else: | |
| 537 for suppress_type in flag.jstype.IterIdentifiers(): | |
| 538 if suppress_type not in state.GetDocFlag().SUPPRESS_TYPES: | |
| 539 self._HandleError( | |
| 540 errors.INVALID_SUPPRESS_TYPE, | |
| 541 'Invalid suppression type: %s' % suppress_type, token) | |
| 542 | |
| 543 elif (error_check.ShouldCheck(Rule.WELL_FORMED_AUTHOR) and | |
| 544 flag.flag_type == 'author'): | |
| 545 # TODO(user): In non strict mode check the author tag for as much as | |
| 546 # it exists, though the full form checked below isn't required. | |
| 547 string = token.next.string | |
| 548 result = self.AUTHOR_SPEC.match(string) | |
| 549 if not result: | |
| 550 self._HandleError(errors.INVALID_AUTHOR_TAG_DESCRIPTION, | |
| 551 'Author tag line should be of the form: ' | |
| 552 '@author foo@somewhere.com (Your Name)', | |
| 553 token.next) | |
| 554 else: | |
| 555 # Check spacing between email address and name. Do this before | |
| 556 # checking earlier spacing so positions are easier to calculate for | |
| 557 # autofixing. | |
| 558 num_spaces = len(result.group(2)) | |
| 559 if num_spaces < 1: | |
| 560 self._HandleError(errors.MISSING_SPACE, | |
| 561 'Missing space after email address', | |
| 562 token.next, position=Position(result.start(2), 0)) | |
| 563 elif num_spaces > 1: | |
| 564 self._HandleError( | |
| 565 errors.EXTRA_SPACE, 'Extra space after email address', | |
| 566 token.next, | |
| 567 position=Position(result.start(2) + 1, num_spaces - 1)) | |
| 568 | |
| 569 # Check for extra spaces before email address. Can't be too few, if | |
| 570 # not at least one we wouldn't match @author tag. | |
| 571 num_spaces = len(result.group(1)) | |
| 572 if num_spaces > 1: | |
| 573 self._HandleError(errors.EXTRA_SPACE, | |
| 574 'Extra space before email address', | |
| 575 token.next, position=Position(1, num_spaces - 1)) | |
| 576 | |
| 577 elif (flag.flag_type in state.GetDocFlag().HAS_DESCRIPTION and | |
| 578 not self._limited_doc_checks): | |
| 579 if flag.flag_type == 'param': | |
| 580 if flag.name is None: | |
| 581 self._HandleError(errors.MISSING_JSDOC_PARAM_NAME, | |
| 582 'Missing name in @param tag', token) | |
| 583 | |
| 584 if not flag.description or flag.description is None: | |
| 585 flag_name = token.type | |
| 586 if 'name' in token.values: | |
| 587 flag_name = '@' + token.values['name'] | |
| 588 | |
| 589 if flag_name not in self.JSDOC_FLAGS_DESCRIPTION_NOT_REQUIRED: | |
| 590 self._HandleError( | |
| 591 errors.MISSING_JSDOC_TAG_DESCRIPTION, | |
| 592 'Missing description in %s tag' % flag_name, token) | |
| 593 else: | |
| 594 self._CheckForMissingSpaceBeforeToken(flag.description_start_token) | |
| 595 | |
| 596 if flag.HasType(): | |
| 597 if flag.type_start_token is not None: | |
| 598 self._CheckForMissingSpaceBeforeToken( | |
| 599 token.attached_object.type_start_token) | |
| 600 | |
| 601 if flag.jstype and not flag.jstype.IsEmpty(): | |
| 602 self._CheckJsDocType(token, flag.jstype) | |
| 603 | |
| 604 if error_check.ShouldCheck(Rule.BRACES_AROUND_TYPE) and ( | |
| 605 flag.type_start_token.type != Type.DOC_START_BRACE or | |
| 606 flag.type_end_token.type != Type.DOC_END_BRACE): | |
| 607 self._HandleError( | |
| 608 errors.MISSING_BRACES_AROUND_TYPE, | |
| 609 'Type must always be surrounded by curly braces.', token) | |
| 610 | |
| 611 if token_type in (Type.DOC_FLAG, Type.DOC_INLINE_FLAG): | |
| 612 if (token.values['name'] not in state.GetDocFlag().LEGAL_DOC and | |
| 613 token.values['name'] not in FLAGS.custom_jsdoc_tags): | |
| 614 self._HandleError( | |
| 615 errors.INVALID_JSDOC_TAG, | |
| 616 'Invalid JsDoc tag: %s' % token.values['name'], token) | |
| 617 | |
| 618 if (error_check.ShouldCheck(Rule.NO_BRACES_AROUND_INHERIT_DOC) and | |
| 619 token.values['name'] == 'inheritDoc' and | |
| 620 token_type == Type.DOC_INLINE_FLAG): | |
| 621 self._HandleError(errors.UNNECESSARY_BRACES_AROUND_INHERIT_DOC, | |
| 622 'Unnecessary braces around @inheritDoc', | |
| 623 token) | |
| 624 | |
| 625 elif token_type == Type.SIMPLE_LVALUE: | |
| 626 identifier = token.values['identifier'] | |
| 627 | |
| 628 if ((not state.InFunction() or state.InConstructor()) and | |
| 629 state.InTopLevel() and not state.InObjectLiteralDescendant()): | |
| 630 jsdoc = state.GetDocComment() | |
| 631 if not state.HasDocComment(identifier): | |
| 632 # Only test for documentation on identifiers with .s in them to | |
| 633 # avoid checking things like simple variables. We don't require | |
| 634 # documenting assignments to .prototype itself (bug 1880803). | |
| 635 if (not state.InConstructor() and | |
| 636 identifier.find('.') != -1 and not | |
| 637 identifier.endswith('.prototype') and not | |
| 638 self._limited_doc_checks): | |
| 639 comment = state.GetLastComment() | |
| 640 if not (comment and comment.lower().count('jsdoc inherited')): | |
| 641 self._HandleError( | |
| 642 errors.MISSING_MEMBER_DOCUMENTATION, | |
| 643 "No docs found for member '%s'" % identifier, | |
| 644 token) | |
| 645 elif jsdoc and (not state.InConstructor() or | |
| 646 identifier.startswith('this.')): | |
| 647 # We are at the top level and the function/member is documented. | |
| 648 if identifier.endswith('_') and not identifier.endswith('__'): | |
| 649 # Can have a private class which inherits documentation from a | |
| 650 # public superclass. | |
| 651 # | |
| 652 # @inheritDoc is deprecated in favor of using @override, and they | |
| 653 if (jsdoc.HasFlag('override') and not jsdoc.HasFlag('constructor') | |
| 654 and ('accessControls' not in jsdoc.suppressions)): | |
| 655 self._HandleError( | |
| 656 errors.INVALID_OVERRIDE_PRIVATE, | |
| 657 '%s should not override a private member.' % identifier, | |
| 658 jsdoc.GetFlag('override').flag_token) | |
| 659 if (jsdoc.HasFlag('inheritDoc') and not jsdoc.HasFlag('constructor') | |
| 660 and ('accessControls' not in jsdoc.suppressions)): | |
| 661 self._HandleError( | |
| 662 errors.INVALID_INHERIT_DOC_PRIVATE, | |
| 663 '%s should not inherit from a private member.' % identifier, | |
| 664 jsdoc.GetFlag('inheritDoc').flag_token) | |
| 665 if (not jsdoc.HasFlag('private') and | |
| 666 ('underscore' not in jsdoc.suppressions) and not | |
| 667 ((jsdoc.HasFlag('inheritDoc') or jsdoc.HasFlag('override')) and | |
| 668 ('accessControls' in jsdoc.suppressions))): | |
| 669 self._HandleError( | |
| 670 errors.MISSING_PRIVATE, | |
| 671 'Member "%s" must have @private JsDoc.' % | |
| 672 identifier, token) | |
| 673 if jsdoc.HasFlag('private') and 'underscore' in jsdoc.suppressions: | |
| 674 self._HandleError( | |
| 675 errors.UNNECESSARY_SUPPRESS, | |
| 676 '@suppress {underscore} is not necessary with @private', | |
| 677 jsdoc.suppressions['underscore']) | |
| 678 elif (jsdoc.HasFlag('private') and | |
| 679 not self.InExplicitlyTypedLanguage()): | |
| 680 # It is convention to hide public fields in some ECMA | |
| 681 # implementations from documentation using the @private tag. | |
| 682 self._HandleError( | |
| 683 errors.EXTRA_PRIVATE, | |
| 684 'Member "%s" must not have @private JsDoc' % | |
| 685 identifier, token) | |
| 686 | |
| 687 # These flags are only legal on localizable message definitions; | |
| 688 # such variables always begin with the prefix MSG_. | |
| 689 if not identifier.startswith('MSG_') and '.MSG_' not in identifier: | |
| 690 for f in ('desc', 'hidden', 'meaning'): | |
| 691 if jsdoc.HasFlag(f): | |
| 692 self._HandleError( | |
| 693 errors.INVALID_USE_OF_DESC_TAG, | |
| 694 'Member "%s" does not start with MSG_ and thus ' | |
| 695 'should not have @%s JsDoc' % (identifier, f), | |
| 696 token) | |
| 697 | |
| 698 # Check for illegaly assigning live objects as prototype property values. | |
| 699 index = identifier.find('.prototype.') | |
| 700 # Ignore anything with additional .s after the prototype. | |
| 701 if index != -1 and identifier.find('.', index + 11) == -1: | |
| 702 equal_operator = tokenutil.SearchExcept(token, Type.NON_CODE_TYPES) | |
| 703 next_code = tokenutil.SearchExcept(equal_operator, Type.NON_CODE_TYPES) | |
| 704 if next_code and ( | |
| 705 next_code.type in (Type.START_BRACKET, Type.START_BLOCK) or | |
| 706 next_code.IsOperator('new')): | |
| 707 self._HandleError( | |
| 708 errors.ILLEGAL_PROTOTYPE_MEMBER_VALUE, | |
| 709 'Member %s cannot have a non-primitive value' % identifier, | |
| 710 token) | |
| 711 | |
| 712 elif token_type == Type.END_PARAMETERS: | |
| 713 # Find extra space at the end of parameter lists. We check the token | |
| 714 # prior to the current one when it is a closing paren. | |
| 715 if (token.previous and token.previous.type == Type.PARAMETERS | |
| 716 and self.ENDS_WITH_SPACE.search(token.previous.string)): | |
| 717 self._HandleError(errors.EXTRA_SPACE, 'Extra space before ")"', | |
| 718 token.previous) | |
| 719 | |
| 720 jsdoc = state.GetDocComment() | |
| 721 if state.GetFunction().is_interface: | |
| 722 if token.previous and token.previous.type == Type.PARAMETERS: | |
| 723 self._HandleError( | |
| 724 errors.INTERFACE_CONSTRUCTOR_CANNOT_HAVE_PARAMS, | |
| 725 'Interface constructor cannot have parameters', | |
| 726 token.previous) | |
| 727 elif (state.InTopLevel() and jsdoc and not jsdoc.HasFlag('see') | |
| 728 and not jsdoc.InheritsDocumentation() | |
| 729 and not state.InObjectLiteralDescendant() and not | |
| 730 jsdoc.IsInvalidated()): | |
| 731 distance, edit = jsdoc.CompareParameters(state.GetParams()) | |
| 732 if distance: | |
| 733 params_iter = iter(state.GetParams()) | |
| 734 docs_iter = iter(jsdoc.ordered_params) | |
| 735 | |
| 736 for op in edit: | |
| 737 if op == 'I': | |
| 738 # Insertion. | |
| 739 # Parsing doc comments is the same for all languages | |
| 740 # but some languages care about parameters that don't have | |
| 741 # doc comments and some languages don't care. | |
| 742 # Languages that don't allow variables to by typed such as | |
| 743 # JavaScript care but languages such as ActionScript or Java | |
| 744 # that allow variables to be typed don't care. | |
| 745 if not self._limited_doc_checks: | |
| 746 self.HandleMissingParameterDoc(token, params_iter.next()) | |
| 747 | |
| 748 elif op == 'D': | |
| 749 # Deletion | |
| 750 self._HandleError(errors.EXTRA_PARAMETER_DOCUMENTATION, | |
| 751 'Found docs for non-existing parameter: "%s"' % | |
| 752 docs_iter.next(), token) | |
| 753 elif op == 'S': | |
| 754 # Substitution | |
| 755 if not self._limited_doc_checks: | |
| 756 self._HandleError( | |
| 757 errors.WRONG_PARAMETER_DOCUMENTATION, | |
| 758 'Parameter mismatch: got "%s", expected "%s"' % | |
| 759 (params_iter.next(), docs_iter.next()), token) | |
| 760 | |
| 761 else: | |
| 762 # Equality - just advance the iterators | |
| 763 params_iter.next() | |
| 764 docs_iter.next() | |
| 765 | |
| 766 elif token_type == Type.STRING_TEXT: | |
| 767 # If this is the first token after the start of the string, but it's at | |
| 768 # the end of a line, we know we have a multi-line string. | |
| 769 if token.previous.type in ( | |
| 770 Type.SINGLE_QUOTE_STRING_START, | |
| 771 Type.DOUBLE_QUOTE_STRING_START) and last_in_line: | |
| 772 self._HandleError(errors.MULTI_LINE_STRING, | |
| 773 'Multi-line strings are not allowed', token) | |
| 774 | |
| 775 # This check is orthogonal to the ones above, and repeats some types, so | |
| 776 # it is a plain if and not an elif. | |
| 777 if token.type in Type.COMMENT_TYPES: | |
| 778 if self.ILLEGAL_TAB.search(token.string): | |
| 779 self._HandleError(errors.ILLEGAL_TAB, | |
| 780 'Illegal tab in comment "%s"' % token.string, token) | |
| 781 | |
| 782 trimmed = token.string.rstrip() | |
| 783 if last_in_line and token.string != trimmed: | |
| 784 # Check for extra whitespace at the end of a line. | |
| 785 self._HandleError( | |
| 786 errors.EXTRA_SPACE, 'Extra space at end of line', token, | |
| 787 position=Position(len(trimmed), len(token.string) - len(trimmed))) | |
| 788 | |
| 789 # This check is also orthogonal since it is based on metadata. | |
| 790 if token.metadata.is_implied_semicolon: | |
| 791 self._HandleError(errors.MISSING_SEMICOLON, | |
| 792 'Missing semicolon at end of line', token) | |
| 793 | |
| 794 def _HandleStartBracket(self, token, last_non_space_token): | |
| 795 """Handles a token that is an open bracket. | |
| 796 | |
| 797 Args: | |
| 798 token: The token to handle. | |
| 799 last_non_space_token: The last token that was not a space. | |
| 800 """ | |
| 801 if (not token.IsFirstInLine() and token.previous.type == Type.WHITESPACE and | |
| 802 last_non_space_token and | |
| 803 last_non_space_token.type in Type.EXPRESSION_ENDER_TYPES): | |
| 804 self._HandleError( | |
| 805 errors.EXTRA_SPACE, 'Extra space before "["', | |
| 806 token.previous, position=Position.All(token.previous.string)) | |
| 807 # If the [ token is the first token in a line we shouldn't complain | |
| 808 # about a missing space before [. This is because some Ecma script | |
| 809 # languages allow syntax like: | |
| 810 # [Annotation] | |
| 811 # class MyClass {...} | |
| 812 # So we don't want to blindly warn about missing spaces before [. | |
| 813 # In the the future, when rules for computing exactly how many spaces | |
| 814 # lines should be indented are added, then we can return errors for | |
| 815 # [ tokens that are improperly indented. | |
| 816 # For example: | |
| 817 # var someVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongVariableName = | |
| 818 # [a,b,c]; | |
| 819 # should trigger a proper indentation warning message as [ is not indented | |
| 820 # by four spaces. | |
| 821 elif (not token.IsFirstInLine() and token.previous and | |
| 822 token.previous.type not in ( | |
| 823 [Type.WHITESPACE, Type.START_PAREN, Type.START_BRACKET] + | |
| 824 Type.EXPRESSION_ENDER_TYPES)): | |
| 825 self._HandleError(errors.MISSING_SPACE, 'Missing space before "["', | |
| 826 token, position=Position.AtBeginning()) | |
| 827 | |
| 828 def Finalize(self, state): | |
| 829 """Perform all checks that need to occur after all lines are processed. | |
| 830 | |
| 831 Args: | |
| 832 state: State of the parser after parsing all tokens | |
| 833 | |
| 834 Raises: | |
| 835 TypeError: If not overridden. | |
| 836 """ | |
| 837 last_non_space_token = state.GetLastNonSpaceToken() | |
| 838 # Check last line for ending with newline. | |
| 839 if state.GetLastLine() and not ( | |
| 840 state.GetLastLine().isspace() or | |
| 841 state.GetLastLine().rstrip('\n\r\f') != state.GetLastLine()): | |
| 842 self._HandleError( | |
| 843 errors.FILE_MISSING_NEWLINE, | |
| 844 'File does not end with new line. (%s)' % state.GetLastLine(), | |
| 845 last_non_space_token) | |
| 846 | |
| 847 try: | |
| 848 self._indentation.Finalize() | |
| 849 except Exception, e: | |
| 850 self._HandleError( | |
| 851 errors.FILE_DOES_NOT_PARSE, | |
| 852 str(e), | |
| 853 last_non_space_token) | |
| 854 | |
| 855 def GetLongLineExceptions(self): | |
| 856 """Gets a list of regexps for lines which can be longer than the limit. | |
| 857 | |
| 858 Returns: | |
| 859 A list of regexps, used as matches (rather than searches). | |
| 860 """ | |
| 861 return [] | |
| 862 | |
| 863 def InExplicitlyTypedLanguage(self): | |
| 864 """Returns whether this ecma implementation is explicitly typed.""" | |
| 865 return False | |
| OLD | NEW |