1#!/usr/bin/env python 2# 3# Copyright 2011 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"""Methods for checking JS files for common style guide violations. 18 19These style guide violations should only apply to JavaScript and not an Ecma 20scripting languages. 21""" 22 23__author__ = ('robbyw@google.com (Robert Walker)', 24 'ajp@google.com (Andy Perelson)', 25 'jacobr@google.com (Jacob Richman)') 26 27import re 28from sets import Set 29from closure_linter import ecmalintrules 30from closure_linter import error_check 31from closure_linter import errors 32from closure_linter import javascripttokenizer 33from closure_linter import javascripttokens 34from closure_linter import requireprovidesorter 35from closure_linter import tokenutil 36from closure_linter.common import error 37from closure_linter.common import position 38 39# Shorthand 40Error = error.Error 41Position = position.Position 42Rule = error_check.Rule 43Type = javascripttokens.JavaScriptTokenType 44 45 46class JavaScriptLintRules(ecmalintrules.EcmaScriptLintRules): 47 """JavaScript lint rules that catch JavaScript specific style errors.""" 48 49 def __init__(self, namespaces_info): 50 """Initializes a JavaScriptLintRules instance.""" 51 ecmalintrules.EcmaScriptLintRules.__init__(self) 52 self._namespaces_info = namespaces_info 53 self._declared_private_member_tokens = {} 54 self._declared_private_members = Set() 55 self._used_private_members = Set() 56 57 def HandleMissingParameterDoc(self, token, param_name): 58 """Handle errors associated with a parameter missing a param tag.""" 59 self._HandleError(errors.MISSING_PARAMETER_DOCUMENTATION, 60 'Missing docs for parameter: "%s"' % param_name, token) 61 62 def __ContainsRecordType(self, token): 63 """Check whether the given token contains a record type. 64 65 Args: 66 token: The token being checked 67 68 Returns: 69 True if the token contains a record type, False otherwise. 70 """ 71 # If we see more than one left-brace in the string of an annotation token, 72 # then there's a record type in there. 73 return ( 74 token and token.type == Type.DOC_FLAG and 75 token.attached_object.type is not None and 76 token.attached_object.type.find('{') != token.string.rfind('{')) 77 78 def CheckToken(self, token, state): 79 """Checks a token, given the current parser_state, for warnings and errors. 80 81 Args: 82 token: The current token under consideration 83 state: parser_state object that indicates the current state in the page 84 """ 85 if self.__ContainsRecordType(token): 86 # We should bail out and not emit any warnings for this annotation. 87 # TODO(nicksantos): Support record types for real. 88 state.GetDocComment().Invalidate() 89 return 90 91 # Call the base class's CheckToken function. 92 super(JavaScriptLintRules, self).CheckToken(token, state) 93 94 # Store some convenience variables 95 namespaces_info = self._namespaces_info 96 97 if error_check.ShouldCheck(Rule.UNUSED_PRIVATE_MEMBERS): 98 # Find all assignments to private members. 99 if token.type == Type.SIMPLE_LVALUE: 100 identifier = token.string 101 if identifier.endswith('_') and not identifier.endswith('__'): 102 doc_comment = state.GetDocComment() 103 suppressed = (doc_comment and doc_comment.HasFlag('suppress') and 104 doc_comment.GetFlag('suppress').type == 'underscore') 105 if not suppressed: 106 # Look for static members defined on a provided namespace. 107 namespace = namespaces_info.GetClosurizedNamespace(identifier) 108 provided_namespaces = namespaces_info.GetProvidedNamespaces() 109 110 # Skip cases of this.something_.somethingElse_. 111 regex = re.compile('^this\.[a-zA-Z_]+$') 112 if namespace in provided_namespaces or regex.match(identifier): 113 variable = identifier.split('.')[-1] 114 self._declared_private_member_tokens[variable] = token 115 self._declared_private_members.add(variable) 116 elif not identifier.endswith('__'): 117 # Consider setting public members of private members to be a usage. 118 for piece in identifier.split('.'): 119 if piece.endswith('_'): 120 self._used_private_members.add(piece) 121 122 # Find all usages of private members. 123 if token.type == Type.IDENTIFIER: 124 for piece in token.string.split('.'): 125 if piece.endswith('_'): 126 self._used_private_members.add(piece) 127 128 if token.type == Type.DOC_FLAG: 129 flag = token.attached_object 130 131 if flag.flag_type == 'param' and flag.name_token is not None: 132 self._CheckForMissingSpaceBeforeToken( 133 token.attached_object.name_token) 134 135 if (error_check.ShouldCheck(Rule.OPTIONAL_TYPE_MARKER) and 136 flag.type is not None and flag.name is not None): 137 # Check for optional marker in type. 138 if (flag.type.endswith('=') and 139 not flag.name.startswith('opt_')): 140 self._HandleError(errors.JSDOC_MISSING_OPTIONAL_PREFIX, 141 'Optional parameter name %s must be prefixed ' 142 'with opt_.' % flag.name, 143 token) 144 elif (not flag.type.endswith('=') and 145 flag.name.startswith('opt_')): 146 self._HandleError(errors.JSDOC_MISSING_OPTIONAL_TYPE, 147 'Optional parameter %s type must end with =.' % 148 flag.name, 149 token) 150 151 if flag.flag_type in state.GetDocFlag().HAS_TYPE: 152 # Check for both missing type token and empty type braces '{}' 153 # Missing suppress types are reported separately and we allow enums 154 # without types. 155 if (flag.flag_type not in ('suppress', 'enum') and 156 (not flag.type or flag.type.isspace())): 157 self._HandleError(errors.MISSING_JSDOC_TAG_TYPE, 158 'Missing type in %s tag' % token.string, token) 159 160 elif flag.name_token and flag.type_end_token and tokenutil.Compare( 161 flag.type_end_token, flag.name_token) > 0: 162 self._HandleError( 163 errors.OUT_OF_ORDER_JSDOC_TAG_TYPE, 164 'Type should be immediately after %s tag' % token.string, 165 token) 166 167 elif token.type == Type.DOUBLE_QUOTE_STRING_START: 168 next_token = token.next 169 while next_token.type == Type.STRING_TEXT: 170 if javascripttokenizer.JavaScriptTokenizer.SINGLE_QUOTE.search( 171 next_token.string): 172 break 173 next_token = next_token.next 174 else: 175 self._HandleError( 176 errors.UNNECESSARY_DOUBLE_QUOTED_STRING, 177 'Single-quoted string preferred over double-quoted string.', 178 token, 179 Position.All(token.string)) 180 181 elif token.type == Type.END_DOC_COMMENT: 182 doc_comment = state.GetDocComment() 183 184 # When @externs appears in a @fileoverview comment, it should trigger 185 # the same limited doc checks as a special filename like externs.js. 186 if doc_comment.HasFlag('fileoverview') and doc_comment.HasFlag('externs'): 187 self._SetLimitedDocChecks(True) 188 189 if (error_check.ShouldCheck(Rule.BLANK_LINES_AT_TOP_LEVEL) and 190 not self._is_html and state.InTopLevel() and not state.InBlock()): 191 192 # Check if we're in a fileoverview or constructor JsDoc. 193 is_constructor = ( 194 doc_comment.HasFlag('constructor') or 195 doc_comment.HasFlag('interface')) 196 is_file_overview = doc_comment.HasFlag('fileoverview') 197 198 # If the comment is not a file overview, and it does not immediately 199 # precede some code, skip it. 200 # NOTE: The tokenutil methods are not used here because of their 201 # behavior at the top of a file. 202 next_token = token.next 203 if (not next_token or 204 (not is_file_overview and next_token.type in Type.NON_CODE_TYPES)): 205 return 206 207 # Don't require extra blank lines around suppression of extra 208 # goog.require errors. 209 if (doc_comment.SuppressionOnly() and 210 next_token.type == Type.IDENTIFIER and 211 next_token.string in ['goog.provide', 'goog.require']): 212 return 213 214 # Find the start of this block (include comments above the block, unless 215 # this is a file overview). 216 block_start = doc_comment.start_token 217 if not is_file_overview: 218 token = block_start.previous 219 while token and token.type in Type.COMMENT_TYPES: 220 block_start = token 221 token = token.previous 222 223 # Count the number of blank lines before this block. 224 blank_lines = 0 225 token = block_start.previous 226 while token and token.type in [Type.WHITESPACE, Type.BLANK_LINE]: 227 if token.type == Type.BLANK_LINE: 228 # A blank line. 229 blank_lines += 1 230 elif token.type == Type.WHITESPACE and not token.line.strip(): 231 # A line with only whitespace on it. 232 blank_lines += 1 233 token = token.previous 234 235 # Log errors. 236 error_message = False 237 expected_blank_lines = 0 238 239 if is_file_overview and blank_lines == 0: 240 error_message = 'Should have a blank line before a file overview.' 241 expected_blank_lines = 1 242 elif is_constructor and blank_lines != 3: 243 error_message = ( 244 'Should have 3 blank lines before a constructor/interface.') 245 expected_blank_lines = 3 246 elif not is_file_overview and not is_constructor and blank_lines != 2: 247 error_message = 'Should have 2 blank lines between top-level blocks.' 248 expected_blank_lines = 2 249 250 if error_message: 251 self._HandleError( 252 errors.WRONG_BLANK_LINE_COUNT, error_message, 253 block_start, Position.AtBeginning(), 254 expected_blank_lines - blank_lines) 255 256 elif token.type == Type.END_BLOCK: 257 if state.InFunction() and state.IsFunctionClose(): 258 is_immediately_called = (token.next and 259 token.next.type == Type.START_PAREN) 260 261 function = state.GetFunction() 262 if not self._limited_doc_checks: 263 if (function.has_return and function.doc and 264 not is_immediately_called and 265 not function.doc.HasFlag('return') and 266 not function.doc.InheritsDocumentation() and 267 not function.doc.HasFlag('constructor')): 268 # Check for proper documentation of return value. 269 self._HandleError( 270 errors.MISSING_RETURN_DOCUMENTATION, 271 'Missing @return JsDoc in function with non-trivial return', 272 function.doc.end_token, Position.AtBeginning()) 273 elif (not function.has_return and 274 not function.has_throw and 275 function.doc and 276 function.doc.HasFlag('return') and 277 not state.InInterfaceMethod()): 278 return_flag = function.doc.GetFlag('return') 279 if (return_flag.type is None or ( 280 'undefined' not in return_flag.type and 281 'void' not in return_flag.type and 282 '*' not in return_flag.type)): 283 self._HandleError( 284 errors.UNNECESSARY_RETURN_DOCUMENTATION, 285 'Found @return JsDoc on function that returns nothing', 286 return_flag.flag_token, Position.AtBeginning()) 287 288 if state.InFunction() and state.IsFunctionClose(): 289 is_immediately_called = (token.next and 290 token.next.type == Type.START_PAREN) 291 if (function.has_this and function.doc and 292 not function.doc.HasFlag('this') and 293 not function.is_constructor and 294 not function.is_interface and 295 '.prototype.' not in function.name): 296 self._HandleError( 297 errors.MISSING_JSDOC_TAG_THIS, 298 'Missing @this JsDoc in function referencing "this". (' 299 'this usually means you are trying to reference "this" in ' 300 'a static function, or you have forgotten to mark a ' 301 'constructor with @constructor)', 302 function.doc.end_token, Position.AtBeginning()) 303 304 elif token.type == Type.IDENTIFIER: 305 if token.string == 'goog.inherits' and not state.InFunction(): 306 if state.GetLastNonSpaceToken().line_number == token.line_number: 307 self._HandleError( 308 errors.MISSING_LINE, 309 'Missing newline between constructor and goog.inherits', 310 token, 311 Position.AtBeginning()) 312 313 extra_space = state.GetLastNonSpaceToken().next 314 while extra_space != token: 315 if extra_space.type == Type.BLANK_LINE: 316 self._HandleError( 317 errors.EXTRA_LINE, 318 'Extra line between constructor and goog.inherits', 319 extra_space) 320 extra_space = extra_space.next 321 322 # TODO(robbyw): Test the last function was a constructor. 323 # TODO(robbyw): Test correct @extends and @implements documentation. 324 325 elif (token.string == 'goog.provide' and 326 not state.InFunction() and 327 namespaces_info is not None): 328 namespace = tokenutil.Search(token, Type.STRING_TEXT).string 329 330 # Report extra goog.provide statement. 331 if namespaces_info.IsExtraProvide(token): 332 self._HandleError( 333 errors.EXTRA_GOOG_PROVIDE, 334 'Unnecessary goog.provide: ' + namespace, 335 token, position=Position.AtBeginning()) 336 337 if namespaces_info.IsLastProvide(token): 338 # Report missing provide statements after the last existing provide. 339 missing_provides = namespaces_info.GetMissingProvides() 340 if missing_provides: 341 self._ReportMissingProvides( 342 missing_provides, 343 tokenutil.GetLastTokenInSameLine(token).next, 344 False) 345 346 # If there are no require statements, missing requires should be 347 # reported after the last provide. 348 if not namespaces_info.GetRequiredNamespaces(): 349 missing_requires = namespaces_info.GetMissingRequires() 350 if missing_requires: 351 self._ReportMissingRequires( 352 missing_requires, 353 tokenutil.GetLastTokenInSameLine(token).next, 354 True) 355 356 elif (token.string == 'goog.require' and 357 not state.InFunction() and 358 namespaces_info is not None): 359 namespace = tokenutil.Search(token, Type.STRING_TEXT).string 360 361 # If there are no provide statements, missing provides should be 362 # reported before the first require. 363 if (namespaces_info.IsFirstRequire(token) and 364 not namespaces_info.GetProvidedNamespaces()): 365 missing_provides = namespaces_info.GetMissingProvides() 366 if missing_provides: 367 self._ReportMissingProvides( 368 missing_provides, 369 tokenutil.GetFirstTokenInSameLine(token), 370 True) 371 372 # Report extra goog.require statement. 373 if namespaces_info.IsExtraRequire(token): 374 self._HandleError( 375 errors.EXTRA_GOOG_REQUIRE, 376 'Unnecessary goog.require: ' + namespace, 377 token, position=Position.AtBeginning()) 378 379 # Report missing goog.require statements. 380 if namespaces_info.IsLastRequire(token): 381 missing_requires = namespaces_info.GetMissingRequires() 382 if missing_requires: 383 self._ReportMissingRequires( 384 missing_requires, 385 tokenutil.GetLastTokenInSameLine(token).next, 386 False) 387 388 elif token.type == Type.OPERATOR: 389 last_in_line = token.IsLastInLine() 390 # If the token is unary and appears to be used in a unary context 391 # it's ok. Otherwise, if it's at the end of the line or immediately 392 # before a comment, it's ok. 393 # Don't report an error before a start bracket - it will be reported 394 # by that token's space checks. 395 if (not token.metadata.IsUnaryOperator() and not last_in_line 396 and not token.next.IsComment() 397 and not token.next.IsOperator(',') 398 and not token.next.type in (Type.WHITESPACE, Type.END_PAREN, 399 Type.END_BRACKET, Type.SEMICOLON, 400 Type.START_BRACKET)): 401 self._HandleError( 402 errors.MISSING_SPACE, 403 'Missing space after "%s"' % token.string, 404 token, 405 Position.AtEnd(token.string)) 406 elif token.type == Type.WHITESPACE: 407 first_in_line = token.IsFirstInLine() 408 last_in_line = token.IsLastInLine() 409 # Check whitespace length if it's not the first token of the line and 410 # if it's not immediately before a comment. 411 if not last_in_line and not first_in_line and not token.next.IsComment(): 412 # Ensure there is no space after opening parentheses. 413 if (token.previous.type in (Type.START_PAREN, Type.START_BRACKET, 414 Type.FUNCTION_NAME) 415 or token.next.type == Type.START_PARAMETERS): 416 self._HandleError( 417 errors.EXTRA_SPACE, 418 'Extra space after "%s"' % token.previous.string, 419 token, 420 Position.All(token.string)) 421 422 def _ReportMissingProvides(self, missing_provides, token, need_blank_line): 423 """Reports missing provide statements to the error handler. 424 425 Args: 426 missing_provides: A list of strings where each string is a namespace that 427 should be provided, but is not. 428 token: The token where the error was detected (also where the new provides 429 will be inserted. 430 need_blank_line: Whether a blank line needs to be inserted after the new 431 provides are inserted. May be True, False, or None, where None 432 indicates that the insert location is unknown. 433 """ 434 self._HandleError( 435 errors.MISSING_GOOG_PROVIDE, 436 'Missing the following goog.provide statements:\n' + 437 '\n'.join(map(lambda x: 'goog.provide(\'%s\');' % x, 438 sorted(missing_provides))), 439 token, position=Position.AtBeginning(), 440 fix_data=(missing_provides, need_blank_line)) 441 442 def _ReportMissingRequires(self, missing_requires, token, need_blank_line): 443 """Reports missing require statements to the error handler. 444 445 Args: 446 missing_requires: A list of strings where each string is a namespace that 447 should be required, but is not. 448 token: The token where the error was detected (also where the new requires 449 will be inserted. 450 need_blank_line: Whether a blank line needs to be inserted before the new 451 requires are inserted. May be True, False, or None, where None 452 indicates that the insert location is unknown. 453 """ 454 self._HandleError( 455 errors.MISSING_GOOG_REQUIRE, 456 'Missing the following goog.require statements:\n' + 457 '\n'.join(map(lambda x: 'goog.require(\'%s\');' % x, 458 sorted(missing_requires))), 459 token, position=Position.AtBeginning(), 460 fix_data=(missing_requires, need_blank_line)) 461 462 def Finalize(self, state, tokenizer_mode): 463 """Perform all checks that need to occur after all lines are processed.""" 464 # Call the base class's Finalize function. 465 super(JavaScriptLintRules, self).Finalize(state, tokenizer_mode) 466 467 if error_check.ShouldCheck(Rule.UNUSED_PRIVATE_MEMBERS): 468 # Report an error for any declared private member that was never used. 469 unused_private_members = (self._declared_private_members - 470 self._used_private_members) 471 472 for variable in unused_private_members: 473 token = self._declared_private_member_tokens[variable] 474 self._HandleError(errors.UNUSED_PRIVATE_MEMBER, 475 'Unused private member: %s.' % token.string, 476 token) 477 478 # Clear state to prepare for the next file. 479 self._declared_private_member_tokens = {} 480 self._declared_private_members = Set() 481 self._used_private_members = Set() 482 483 namespaces_info = self._namespaces_info 484 if namespaces_info is not None: 485 # If there are no provide or require statements, missing provides and 486 # requires should be reported on line 1. 487 if (not namespaces_info.GetProvidedNamespaces() and 488 not namespaces_info.GetRequiredNamespaces()): 489 missing_provides = namespaces_info.GetMissingProvides() 490 if missing_provides: 491 self._ReportMissingProvides( 492 missing_provides, state.GetFirstToken(), None) 493 494 missing_requires = namespaces_info.GetMissingRequires() 495 if missing_requires: 496 self._ReportMissingRequires( 497 missing_requires, state.GetFirstToken(), None) 498 499 self._CheckSortedRequiresProvides(state.GetFirstToken()) 500 501 def _CheckSortedRequiresProvides(self, token): 502 """Checks that all goog.require and goog.provide statements are sorted. 503 504 Note that this method needs to be run after missing statements are added to 505 preserve alphabetical order. 506 507 Args: 508 token: The first token in the token stream. 509 """ 510 sorter = requireprovidesorter.RequireProvideSorter() 511 provides_result = sorter.CheckProvides(token) 512 if provides_result: 513 self._HandleError( 514 errors.GOOG_PROVIDES_NOT_ALPHABETIZED, 515 'goog.provide classes must be alphabetized. The correct code is:\n' + 516 '\n'.join( 517 map(lambda x: 'goog.provide(\'%s\');' % x, provides_result[1])), 518 provides_result[0], 519 position=Position.AtBeginning(), 520 fix_data=provides_result[0]) 521 522 requires_result = sorter.CheckRequires(token) 523 if requires_result: 524 self._HandleError( 525 errors.GOOG_REQUIRES_NOT_ALPHABETIZED, 526 'goog.require classes must be alphabetized. The correct code is:\n' + 527 '\n'.join( 528 map(lambda x: 'goog.require(\'%s\');' % x, requires_result[1])), 529 requires_result[0], 530 position=Position.AtBeginning(), 531 fix_data=requires_result[0]) 532 533 def GetLongLineExceptions(self): 534 """Gets a list of regexps for lines which can be longer than the limit.""" 535 return [ 536 re.compile('.*// @suppress longLineCheck$'), 537 re.compile('goog\.require\(.+\);?\s*$'), 538 re.compile('goog\.provide\(.+\);?\s*$') 539 ] 540