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