| #!/usr/bin/env python |
| # |
| # Copyright 2008 The Closure Linter Authors. All Rights Reserved. |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS-IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| """Core methods for checking EcmaScript files for common style guide violations. |
| """ |
| |
| __author__ = ('robbyw@google.com (Robert Walker)', |
| 'ajp@google.com (Andy Perelson)', |
| 'jacobr@google.com (Jacob Richman)') |
| |
| import re |
| |
| import gflags as flags |
| |
| from closure_linter import checkerbase |
| from closure_linter import ecmametadatapass |
| from closure_linter import error_check |
| from closure_linter import errorrules |
| from closure_linter import errors |
| from closure_linter import indentation |
| from closure_linter import javascripttokenizer |
| from closure_linter import javascripttokens |
| from closure_linter import statetracker |
| from closure_linter import tokenutil |
| from closure_linter.common import error |
| from closure_linter.common import position |
| |
| |
| FLAGS = flags.FLAGS |
| flags.DEFINE_list('custom_jsdoc_tags', '', 'Extra jsdoc tags to allow') |
| # TODO(user): When flipping this to True, remove logic from unit tests |
| # that overrides this flag. |
| flags.DEFINE_boolean('dot_on_next_line', False, 'Require dots to be' |
| 'placed on the next line for wrapped expressions') |
| |
| flags.DEFINE_boolean('check_trailing_comma', False, 'Check trailing commas' |
| ' (ES3, not needed from ES5 onwards)') |
| |
| # TODO(robbyw): Check for extra parens on return statements |
| # TODO(robbyw): Check for 0px in strings |
| # TODO(robbyw): Ensure inline jsDoc is in {} |
| # TODO(robbyw): Check for valid JS types in parameter docs |
| |
| # Shorthand |
| Context = ecmametadatapass.EcmaContext |
| Error = error.Error |
| Modes = javascripttokenizer.JavaScriptModes |
| Position = position.Position |
| Rule = error_check.Rule |
| Type = javascripttokens.JavaScriptTokenType |
| |
| |
| class EcmaScriptLintRules(checkerbase.LintRulesBase): |
| """EmcaScript lint style checking rules. |
| |
| Can be used to find common style errors in JavaScript, ActionScript and other |
| Ecma like scripting languages. Style checkers for Ecma scripting languages |
| should inherit from this style checker. |
| Please do not add any state to EcmaScriptLintRules or to any subclasses. |
| |
| All state should be added to the StateTracker subclass used for a particular |
| language. |
| """ |
| |
| # It will be initialized in constructor so the flags are initialized. |
| max_line_length = -1 |
| |
| # Static constants. |
| MISSING_PARAMETER_SPACE = re.compile(r',\S') |
| |
| EXTRA_SPACE = re.compile(r'(\(\s|\s\))') |
| |
| ENDS_WITH_SPACE = re.compile(r'\s$') |
| |
| ILLEGAL_TAB = re.compile(r'\t') |
| |
| # Regex used to split up complex types to check for invalid use of ? and |. |
| TYPE_SPLIT = re.compile(r'[,<>()]') |
| |
| # Regex for form of author lines after the @author tag. |
| AUTHOR_SPEC = re.compile(r'(\s*)[^\s]+@[^(\s]+(\s*)\(.+\)') |
| |
| # Acceptable tokens to remove for line too long testing. |
| LONG_LINE_IGNORE = frozenset( |
| ['*', '//', '@see'] + |
| ['@%s' % tag for tag in statetracker.DocFlag.HAS_TYPE]) |
| |
| JSDOC_FLAGS_DESCRIPTION_NOT_REQUIRED = frozenset([ |
| '@fileoverview', '@param', '@return', '@returns']) |
| |
| def __init__(self): |
| """Initialize this lint rule object.""" |
| checkerbase.LintRulesBase.__init__(self) |
| if EcmaScriptLintRules.max_line_length == -1: |
| EcmaScriptLintRules.max_line_length = errorrules.GetMaxLineLength() |
| |
| def Initialize(self, checker, limited_doc_checks, is_html): |
| """Initialize this lint rule object before parsing a new file.""" |
| checkerbase.LintRulesBase.Initialize(self, checker, limited_doc_checks, |
| is_html) |
| self._indentation = indentation.IndentationRules() |
| |
| def HandleMissingParameterDoc(self, token, param_name): |
| """Handle errors associated with a parameter missing a @param tag.""" |
| raise TypeError('Abstract method HandleMissingParameterDoc not implemented') |
| |
| def _CheckLineLength(self, last_token, state): |
| """Checks whether the line is too long. |
| |
| Args: |
| last_token: The last token in the line. |
| state: parser_state object that indicates the current state in the page |
| """ |
| # Start from the last token so that we have the flag object attached to |
| # and DOC_FLAG tokens. |
| line_number = last_token.line_number |
| token = last_token |
| |
| # Build a representation of the string where spaces indicate potential |
| # line-break locations. |
| line = [] |
| while token and token.line_number == line_number: |
| if state.IsTypeToken(token): |
| line.insert(0, 'x' * len(token.string)) |
| elif token.type in (Type.IDENTIFIER, Type.OPERATOR): |
| # Dots are acceptable places to wrap (may be tokenized as identifiers). |
| line.insert(0, token.string.replace('.', ' ')) |
| else: |
| line.insert(0, token.string) |
| token = token.previous |
| |
| line = ''.join(line) |
| line = line.rstrip('\n\r\f') |
| try: |
| length = len(unicode(line, 'utf-8')) |
| except (LookupError, UnicodeDecodeError): |
| # Unknown encoding. The line length may be wrong, as was originally the |
| # case for utf-8 (see bug 1735846). For now just accept the default |
| # length, but as we find problems we can either add test for other |
| # possible encodings or return without an error to protect against |
| # false positives at the cost of more false negatives. |
| length = len(line) |
| |
| if length > EcmaScriptLintRules.max_line_length: |
| |
| # If the line matches one of the exceptions, then it's ok. |
| for long_line_regexp in self.GetLongLineExceptions(): |
| if long_line_regexp.match(last_token.line): |
| return |
| |
| # If the line consists of only one "word", or multiple words but all |
| # except one are ignoreable, then it's ok. |
| parts = set(line.split()) |
| |
| # We allow two "words" (type and name) when the line contains @param |
| max_parts = 1 |
| if '@param' in parts: |
| max_parts = 2 |
| |
| # Custom tags like @requires may have url like descriptions, so ignore |
| # the tag, similar to how we handle @see. |
| custom_tags = set(['@%s' % f for f in FLAGS.custom_jsdoc_tags]) |
| if (len(parts.difference(self.LONG_LINE_IGNORE | custom_tags)) |
| > max_parts): |
| self._HandleError( |
| errors.LINE_TOO_LONG, |
| 'Line too long (%d characters).' % len(line), last_token) |
| |
| def _CheckJsDocType(self, token, js_type): |
| """Checks the given type for style errors. |
| |
| Args: |
| token: The DOC_FLAG token for the flag whose type to check. |
| js_type: The flag's typeannotation.TypeAnnotation instance. |
| """ |
| if not js_type: return |
| |
| if js_type.type_group and len(js_type.sub_types) == 2: |
| identifiers = [t.identifier for t in js_type.sub_types] |
| if 'null' in identifiers: |
| # Don't warn if the identifier is a template type (e.g. {TYPE|null}. |
| if not identifiers[0].isupper() and not identifiers[1].isupper(): |
| self._HandleError( |
| errors.JSDOC_PREFER_QUESTION_TO_PIPE_NULL, |
| 'Prefer "?Type" to "Type|null": "%s"' % js_type, token) |
| |
| # TODO(user): We should report an error for wrong usage of '?' and '|' |
| # e.g. {?number|string|null} etc. |
| |
| for sub_type in js_type.IterTypes(): |
| self._CheckJsDocType(token, sub_type) |
| |
| def _CheckForMissingSpaceBeforeToken(self, token): |
| """Checks for a missing space at the beginning of a token. |
| |
| Reports a MISSING_SPACE error if the token does not begin with a space or |
| the previous token doesn't end with a space and the previous token is on the |
| same line as the token. |
| |
| Args: |
| token: The token being checked |
| """ |
| # TODO(user): Check if too many spaces? |
| if (len(token.string) == len(token.string.lstrip()) and |
| token.previous and token.line_number == token.previous.line_number and |
| len(token.previous.string) - len(token.previous.string.rstrip()) == 0): |
| self._HandleError( |
| errors.MISSING_SPACE, |
| 'Missing space before "%s"' % token.string, |
| token, |
| position=Position.AtBeginning()) |
| |
| def _CheckOperator(self, token): |
| """Checks an operator for spacing and line style. |
| |
| Args: |
| token: The operator token. |
| """ |
| last_code = token.metadata.last_code |
| |
| if not self._ExpectSpaceBeforeOperator(token): |
| if (token.previous and token.previous.type == Type.WHITESPACE and |
| last_code and last_code.type in (Type.NORMAL, Type.IDENTIFIER) and |
| last_code.line_number == token.line_number): |
| self._HandleError( |
| errors.EXTRA_SPACE, 'Extra space before "%s"' % token.string, |
| token.previous, position=Position.All(token.previous.string)) |
| |
| elif (token.previous and |
| not token.previous.IsComment() and |
| not tokenutil.IsDot(token) and |
| token.previous.type in Type.EXPRESSION_ENDER_TYPES): |
| self._HandleError(errors.MISSING_SPACE, |
| 'Missing space before "%s"' % token.string, token, |
| position=Position.AtBeginning()) |
| |
| # Check wrapping of operators. |
| next_code = tokenutil.GetNextCodeToken(token) |
| |
| is_dot = tokenutil.IsDot(token) |
| wrapped_before = last_code and last_code.line_number != token.line_number |
| wrapped_after = next_code and next_code.line_number != token.line_number |
| |
| if FLAGS.dot_on_next_line and is_dot and wrapped_after: |
| self._HandleError( |
| errors.LINE_ENDS_WITH_DOT, |
| '"." must go on the following line', |
| token) |
| if (not is_dot and wrapped_before and |
| not token.metadata.IsUnaryOperator()): |
| self._HandleError( |
| errors.LINE_STARTS_WITH_OPERATOR, |
| 'Binary operator must go on previous line "%s"' % token.string, |
| token) |
| |
| def _IsLabel(self, token): |
| # A ':' token is considered part of a label if it occurs in a case |
| # statement, a plain label, or an object literal, i.e. is not part of a |
| # ternary. |
| |
| return (token.string == ':' and |
| token.metadata.context.type in (Context.LITERAL_ELEMENT, |
| Context.CASE_BLOCK, |
| Context.STATEMENT)) |
| |
| def _ExpectSpaceBeforeOperator(self, token): |
| """Returns whether a space should appear before the given operator token. |
| |
| Args: |
| token: The operator token. |
| |
| Returns: |
| Whether there should be a space before the token. |
| """ |
| if token.string == ',' or token.metadata.IsUnaryPostOperator(): |
| return False |
| |
| if tokenutil.IsDot(token): |
| return False |
| |
| # Colons should appear in labels, object literals, the case of a switch |
| # statement, and ternary operator. Only want a space in the case of the |
| # ternary operator. |
| if self._IsLabel(token): |
| return False |
| |
| if token.metadata.IsUnaryOperator() and token.IsFirstInLine(): |
| return False |
| |
| return True |
| |
| def CheckToken(self, token, state): |
| """Checks a token, given the current parser_state, for warnings and errors. |
| |
| Args: |
| token: The current token under consideration |
| state: parser_state object that indicates the current state in the page |
| """ |
| # Store some convenience variables |
| first_in_line = token.IsFirstInLine() |
| last_in_line = token.IsLastInLine() |
| last_non_space_token = state.GetLastNonSpaceToken() |
| |
| token_type = token.type |
| |
| # Process the line change. |
| if not self._is_html and error_check.ShouldCheck(Rule.INDENTATION): |
| # TODO(robbyw): Support checking indentation in HTML files. |
| indentation_errors = self._indentation.CheckToken(token, state) |
| for indentation_error in indentation_errors: |
| self._HandleError(*indentation_error) |
| |
| if last_in_line: |
| self._CheckLineLength(token, state) |
| |
| if token_type == Type.PARAMETERS: |
| # Find missing spaces in parameter lists. |
| if self.MISSING_PARAMETER_SPACE.search(token.string): |
| fix_data = ', '.join([s.strip() for s in token.string.split(',')]) |
| self._HandleError(errors.MISSING_SPACE, 'Missing space after ","', |
| token, position=None, fix_data=fix_data.strip()) |
| |
| # Find extra spaces at the beginning of parameter lists. Make sure |
| # we aren't at the beginning of a continuing multi-line list. |
| if not first_in_line: |
| space_count = len(token.string) - len(token.string.lstrip()) |
| if space_count: |
| self._HandleError(errors.EXTRA_SPACE, 'Extra space after "("', |
| token, position=Position(0, space_count)) |
| |
| elif (token_type == Type.START_BLOCK and |
| token.metadata.context.type == Context.BLOCK): |
| self._CheckForMissingSpaceBeforeToken(token) |
| |
| elif token_type == Type.END_BLOCK: |
| last_code = token.metadata.last_code |
| |
| if FLAGS.check_trailing_comma: |
| if last_code.IsOperator(','): |
| self._HandleError( |
| errors.COMMA_AT_END_OF_LITERAL, |
| 'Illegal comma at end of object literal', last_code, |
| position=Position.All(last_code.string)) |
| |
| if state.InFunction() and state.IsFunctionClose(): |
| if state.InTopLevelFunction(): |
| # A semicolons should not be included at the end of a function |
| # declaration. |
| if not state.InAssignedFunction(): |
| if not last_in_line and token.next.type == Type.SEMICOLON: |
| self._HandleError( |
| errors.ILLEGAL_SEMICOLON_AFTER_FUNCTION, |
| 'Illegal semicolon after function declaration', |
| token.next, position=Position.All(token.next.string)) |
| |
| # A semicolon should be included at the end of a function expression |
| # that is not immediately called or used by a dot operator. |
| if (state.InAssignedFunction() and token.next |
| and token.next.type != Type.SEMICOLON): |
| next_token = tokenutil.GetNextCodeToken(token) |
| is_immediately_used = next_token and ( |
| next_token.type == Type.START_PAREN or |
| tokenutil.IsDot(next_token)) |
| if not is_immediately_used: |
| self._HandleError( |
| errors.MISSING_SEMICOLON_AFTER_FUNCTION, |
| 'Missing semicolon after function assigned to a variable', |
| token, position=Position.AtEnd(token.string)) |
| |
| if state.InInterfaceMethod() and last_code.type != Type.START_BLOCK: |
| self._HandleError(errors.INTERFACE_METHOD_CANNOT_HAVE_CODE, |
| 'Interface methods cannot contain code', last_code) |
| |
| elif (state.IsBlockClose() and |
| token.next and token.next.type == Type.SEMICOLON): |
| if (last_code.metadata.context.parent.type != Context.OBJECT_LITERAL |
| and last_code.metadata.context.type != Context.OBJECT_LITERAL): |
| self._HandleError( |
| errors.REDUNDANT_SEMICOLON, |
| 'No semicolon is required to end a code block', |
| token.next, position=Position.All(token.next.string)) |
| |
| elif token_type == Type.SEMICOLON: |
| if token.previous and token.previous.type == Type.WHITESPACE: |
| self._HandleError( |
| errors.EXTRA_SPACE, 'Extra space before ";"', |
| token.previous, position=Position.All(token.previous.string)) |
| |
| if token.next and token.next.line_number == token.line_number: |
| if token.metadata.context.type != Context.FOR_GROUP_BLOCK: |
| # TODO(robbyw): Error about no multi-statement lines. |
| pass |
| |
| elif token.next.type not in ( |
| Type.WHITESPACE, Type.SEMICOLON, Type.END_PAREN): |
| self._HandleError( |
| errors.MISSING_SPACE, |
| 'Missing space after ";" in for statement', |
| token.next, |
| position=Position.AtBeginning()) |
| |
| last_code = token.metadata.last_code |
| if last_code and last_code.type == Type.SEMICOLON: |
| # Allow a single double semi colon in for loops for cases like: |
| # for (;;) { }. |
| # NOTE(user): This is not a perfect check, and will not throw an error |
| # for cases like: for (var i = 0;; i < n; i++) {}, but then your code |
| # probably won't work either. |
| for_token = tokenutil.CustomSearch( |
| last_code, |
| lambda token: token.type == Type.KEYWORD and token.string == 'for', |
| end_func=lambda token: token.type == Type.SEMICOLON, |
| distance=None, |
| reverse=True) |
| |
| if not for_token: |
| self._HandleError(errors.REDUNDANT_SEMICOLON, 'Redundant semicolon', |
| token, position=Position.All(token.string)) |
| |
| elif token_type == Type.START_PAREN: |
| # Ensure that opening parentheses have a space before any keyword |
| # that is not being invoked like a member function. |
| if (token.previous and token.previous.type == Type.KEYWORD and |
| (not token.previous.metadata or |
| not token.previous.metadata.last_code or |
| not token.previous.metadata.last_code.string or |
| token.previous.metadata.last_code.string[-1:] != '.')): |
| self._HandleError(errors.MISSING_SPACE, 'Missing space before "("', |
| token, position=Position.AtBeginning()) |
| elif token.previous and token.previous.type == Type.WHITESPACE: |
| before_space = token.previous.previous |
| # Ensure that there is no extra space before a function invocation, |
| # even if the function being invoked happens to be a keyword. |
| if (before_space and before_space.line_number == token.line_number and |
| before_space.type == Type.IDENTIFIER or |
| (before_space.type == Type.KEYWORD and before_space.metadata and |
| before_space.metadata.last_code and |
| before_space.metadata.last_code.string and |
| before_space.metadata.last_code.string[-1:] == '.')): |
| self._HandleError( |
| errors.EXTRA_SPACE, 'Extra space before "("', |
| token.previous, position=Position.All(token.previous.string)) |
| |
| elif token_type == Type.START_BRACKET: |
| self._HandleStartBracket(token, last_non_space_token) |
| elif token_type in (Type.END_PAREN, Type.END_BRACKET): |
| # Ensure there is no space before closing parentheses, except when |
| # it's in a for statement with an omitted section, or when it's at the |
| # beginning of a line. |
| |
| last_code = token.metadata.last_code |
| if FLAGS.check_trailing_comma and token_type == Type.END_BRACKET: |
| if last_code.IsOperator(','): |
| self._HandleError( |
| errors.COMMA_AT_END_OF_LITERAL, |
| 'Illegal comma at end of array literal', last_code, |
| position=Position.All(last_code.string)) |
| |
| if (token.previous and token.previous.type == Type.WHITESPACE and |
| not token.previous.IsFirstInLine() and |
| not (last_non_space_token and last_non_space_token.line_number == |
| token.line_number and |
| last_non_space_token.type == Type.SEMICOLON)): |
| self._HandleError( |
| errors.EXTRA_SPACE, 'Extra space before "%s"' % |
| token.string, token.previous, |
| position=Position.All(token.previous.string)) |
| |
| elif token_type == Type.WHITESPACE: |
| if self.ILLEGAL_TAB.search(token.string): |
| if token.IsFirstInLine(): |
| if token.next: |
| self._HandleError( |
| errors.ILLEGAL_TAB, |
| 'Illegal tab in whitespace before "%s"' % token.next.string, |
| token, position=Position.All(token.string)) |
| else: |
| self._HandleError( |
| errors.ILLEGAL_TAB, |
| 'Illegal tab in whitespace', |
| token, position=Position.All(token.string)) |
| else: |
| self._HandleError( |
| errors.ILLEGAL_TAB, |
| 'Illegal tab in whitespace after "%s"' % token.previous.string, |
| token, position=Position.All(token.string)) |
| |
| # Check whitespace length if it's not the first token of the line and |
| # if it's not immediately before a comment. |
| if last_in_line: |
| # Check for extra whitespace at the end of a line. |
| self._HandleError(errors.EXTRA_SPACE, 'Extra space at end of line', |
| token, position=Position.All(token.string)) |
| elif not first_in_line and not token.next.IsComment(): |
| if token.length > 1: |
| self._HandleError( |
| errors.EXTRA_SPACE, 'Extra space after "%s"' % |
| token.previous.string, token, |
| position=Position(1, len(token.string) - 1)) |
| |
| elif token_type == Type.OPERATOR: |
| self._CheckOperator(token) |
| elif token_type == Type.DOC_FLAG: |
| flag = token.attached_object |
| |
| if flag.flag_type == 'bug': |
| # TODO(robbyw): Check for exactly 1 space on the left. |
| string = token.next.string.lstrip() |
| string = string.split(' ', 1)[0] |
| |
| if not string.isdigit(): |
| self._HandleError(errors.NO_BUG_NUMBER_AFTER_BUG_TAG, |
| '@bug should be followed by a bug number', token) |
| |
| elif flag.flag_type == 'suppress': |
| if flag.type is None: |
| # A syntactically invalid suppress tag will get tokenized as a normal |
| # flag, indicating an error. |
| self._HandleError( |
| errors.INCORRECT_SUPPRESS_SYNTAX, |
| 'Invalid suppress syntax: should be @suppress {errortype}. ' |
| 'Spaces matter.', token) |
| else: |
| for suppress_type in flag.jstype.IterIdentifiers(): |
| if suppress_type not in state.GetDocFlag().SUPPRESS_TYPES: |
| self._HandleError( |
| errors.INVALID_SUPPRESS_TYPE, |
| 'Invalid suppression type: %s' % suppress_type, token) |
| |
| elif (error_check.ShouldCheck(Rule.WELL_FORMED_AUTHOR) and |
| flag.flag_type == 'author'): |
| # TODO(user): In non strict mode check the author tag for as much as |
| # it exists, though the full form checked below isn't required. |
| string = token.next.string |
| result = self.AUTHOR_SPEC.match(string) |
| if not result: |
| self._HandleError(errors.INVALID_AUTHOR_TAG_DESCRIPTION, |
| 'Author tag line should be of the form: ' |
| '@author foo@somewhere.com (Your Name)', |
| token.next) |
| else: |
| # Check spacing between email address and name. Do this before |
| # checking earlier spacing so positions are easier to calculate for |
| # autofixing. |
| num_spaces = len(result.group(2)) |
| if num_spaces < 1: |
| self._HandleError(errors.MISSING_SPACE, |
| 'Missing space after email address', |
| token.next, position=Position(result.start(2), 0)) |
| elif num_spaces > 1: |
| self._HandleError( |
| errors.EXTRA_SPACE, 'Extra space after email address', |
| token.next, |
| position=Position(result.start(2) + 1, num_spaces - 1)) |
| |
| # Check for extra spaces before email address. Can't be too few, if |
| # not at least one we wouldn't match @author tag. |
| num_spaces = len(result.group(1)) |
| if num_spaces > 1: |
| self._HandleError(errors.EXTRA_SPACE, |
| 'Extra space before email address', |
| token.next, position=Position(1, num_spaces - 1)) |
| |
| elif (flag.flag_type in state.GetDocFlag().HAS_DESCRIPTION and |
| not self._limited_doc_checks): |
| if flag.flag_type == 'param': |
| if flag.name is None: |
| self._HandleError(errors.MISSING_JSDOC_PARAM_NAME, |
| 'Missing name in @param tag', token) |
| |
| if not flag.description or flag.description is None: |
| flag_name = token.type |
| if 'name' in token.values: |
| flag_name = '@' + token.values['name'] |
| |
| if flag_name not in self.JSDOC_FLAGS_DESCRIPTION_NOT_REQUIRED: |
| self._HandleError( |
| errors.MISSING_JSDOC_TAG_DESCRIPTION, |
| 'Missing description in %s tag' % flag_name, token) |
| else: |
| self._CheckForMissingSpaceBeforeToken(flag.description_start_token) |
| |
| if flag.HasType(): |
| if flag.type_start_token is not None: |
| self._CheckForMissingSpaceBeforeToken( |
| token.attached_object.type_start_token) |
| |
| if flag.jstype and not flag.jstype.IsEmpty(): |
| self._CheckJsDocType(token, flag.jstype) |
| |
| if error_check.ShouldCheck(Rule.BRACES_AROUND_TYPE) and ( |
| flag.type_start_token.type != Type.DOC_START_BRACE or |
| flag.type_end_token.type != Type.DOC_END_BRACE): |
| self._HandleError( |
| errors.MISSING_BRACES_AROUND_TYPE, |
| 'Type must always be surrounded by curly braces.', token) |
| |
| if token_type in (Type.DOC_FLAG, Type.DOC_INLINE_FLAG): |
| if (token.values['name'] not in state.GetDocFlag().LEGAL_DOC and |
| token.values['name'] not in FLAGS.custom_jsdoc_tags): |
| self._HandleError( |
| errors.INVALID_JSDOC_TAG, |
| 'Invalid JsDoc tag: %s' % token.values['name'], token) |
| |
| if (error_check.ShouldCheck(Rule.NO_BRACES_AROUND_INHERIT_DOC) and |
| token.values['name'] == 'inheritDoc' and |
| token_type == Type.DOC_INLINE_FLAG): |
| self._HandleError(errors.UNNECESSARY_BRACES_AROUND_INHERIT_DOC, |
| 'Unnecessary braces around @inheritDoc', |
| token) |
| |
| elif token_type == Type.SIMPLE_LVALUE: |
| identifier = token.values['identifier'] |
| |
| if ((not state.InFunction() or state.InConstructor()) and |
| state.InTopLevel() and not state.InObjectLiteralDescendant()): |
| jsdoc = state.GetDocComment() |
| if not state.HasDocComment(identifier): |
| # Only test for documentation on identifiers with .s in them to |
| # avoid checking things like simple variables. We don't require |
| # documenting assignments to .prototype itself (bug 1880803). |
| if (not state.InConstructor() and |
| identifier.find('.') != -1 and not |
| identifier.endswith('.prototype') and not |
| self._limited_doc_checks): |
| comment = state.GetLastComment() |
| if not (comment and comment.lower().count('jsdoc inherited')): |
| self._HandleError( |
| errors.MISSING_MEMBER_DOCUMENTATION, |
| "No docs found for member '%s'" % identifier, |
| token) |
| elif jsdoc and (not state.InConstructor() or |
| identifier.startswith('this.')): |
| # We are at the top level and the function/member is documented. |
| if identifier.endswith('_') and not identifier.endswith('__'): |
| # Can have a private class which inherits documentation from a |
| # public superclass. |
| # |
| # @inheritDoc is deprecated in favor of using @override, and they |
| if (jsdoc.HasFlag('override') and not jsdoc.HasFlag('constructor') |
| and ('accessControls' not in jsdoc.suppressions)): |
| self._HandleError( |
| errors.INVALID_OVERRIDE_PRIVATE, |
| '%s should not override a private member.' % identifier, |
| jsdoc.GetFlag('override').flag_token) |
| if (jsdoc.HasFlag('inheritDoc') and not jsdoc.HasFlag('constructor') |
| and ('accessControls' not in jsdoc.suppressions)): |
| self._HandleError( |
| errors.INVALID_INHERIT_DOC_PRIVATE, |
| '%s should not inherit from a private member.' % identifier, |
| jsdoc.GetFlag('inheritDoc').flag_token) |
| if (not jsdoc.HasFlag('private') and |
| ('underscore' not in jsdoc.suppressions) and not |
| ((jsdoc.HasFlag('inheritDoc') or jsdoc.HasFlag('override')) and |
| ('accessControls' in jsdoc.suppressions))): |
| self._HandleError( |
| errors.MISSING_PRIVATE, |
| 'Member "%s" must have @private JsDoc.' % |
| identifier, token) |
| if jsdoc.HasFlag('private') and 'underscore' in jsdoc.suppressions: |
| self._HandleError( |
| errors.UNNECESSARY_SUPPRESS, |
| '@suppress {underscore} is not necessary with @private', |
| jsdoc.suppressions['underscore']) |
| elif (jsdoc.HasFlag('private') and |
| not self.InExplicitlyTypedLanguage()): |
| # It is convention to hide public fields in some ECMA |
| # implementations from documentation using the @private tag. |
| self._HandleError( |
| errors.EXTRA_PRIVATE, |
| 'Member "%s" must not have @private JsDoc' % |
| identifier, token) |
| |
| # These flags are only legal on localizable message definitions; |
| # such variables always begin with the prefix MSG_. |
| if not identifier.startswith('MSG_') and '.MSG_' not in identifier: |
| for f in ('desc', 'hidden', 'meaning'): |
| if jsdoc.HasFlag(f): |
| self._HandleError( |
| errors.INVALID_USE_OF_DESC_TAG, |
| 'Member "%s" does not start with MSG_ and thus ' |
| 'should not have @%s JsDoc' % (identifier, f), |
| token) |
| |
| # Check for illegaly assigning live objects as prototype property values. |
| index = identifier.find('.prototype.') |
| # Ignore anything with additional .s after the prototype. |
| if index != -1 and identifier.find('.', index + 11) == -1: |
| equal_operator = tokenutil.SearchExcept(token, Type.NON_CODE_TYPES) |
| next_code = tokenutil.SearchExcept(equal_operator, Type.NON_CODE_TYPES) |
| if next_code and ( |
| next_code.type in (Type.START_BRACKET, Type.START_BLOCK) or |
| next_code.IsOperator('new')): |
| self._HandleError( |
| errors.ILLEGAL_PROTOTYPE_MEMBER_VALUE, |
| 'Member %s cannot have a non-primitive value' % identifier, |
| token) |
| |
| elif token_type == Type.END_PARAMETERS: |
| # Find extra space at the end of parameter lists. We check the token |
| # prior to the current one when it is a closing paren. |
| if (token.previous and token.previous.type == Type.PARAMETERS |
| and self.ENDS_WITH_SPACE.search(token.previous.string)): |
| self._HandleError(errors.EXTRA_SPACE, 'Extra space before ")"', |
| token.previous) |
| |
| jsdoc = state.GetDocComment() |
| if state.GetFunction().is_interface: |
| if token.previous and token.previous.type == Type.PARAMETERS: |
| self._HandleError( |
| errors.INTERFACE_CONSTRUCTOR_CANNOT_HAVE_PARAMS, |
| 'Interface constructor cannot have parameters', |
| token.previous) |
| elif (state.InTopLevel() and jsdoc and not jsdoc.HasFlag('see') |
| and not jsdoc.InheritsDocumentation() |
| and not state.InObjectLiteralDescendant() and not |
| jsdoc.IsInvalidated()): |
| distance, edit = jsdoc.CompareParameters(state.GetParams()) |
| if distance: |
| params_iter = iter(state.GetParams()) |
| docs_iter = iter(jsdoc.ordered_params) |
| |
| for op in edit: |
| if op == 'I': |
| # Insertion. |
| # Parsing doc comments is the same for all languages |
| # but some languages care about parameters that don't have |
| # doc comments and some languages don't care. |
| # Languages that don't allow variables to by typed such as |
| # JavaScript care but languages such as ActionScript or Java |
| # that allow variables to be typed don't care. |
| if not self._limited_doc_checks: |
| self.HandleMissingParameterDoc(token, params_iter.next()) |
| |
| elif op == 'D': |
| # Deletion |
| self._HandleError(errors.EXTRA_PARAMETER_DOCUMENTATION, |
| 'Found docs for non-existing parameter: "%s"' % |
| docs_iter.next(), token) |
| elif op == 'S': |
| # Substitution |
| if not self._limited_doc_checks: |
| self._HandleError( |
| errors.WRONG_PARAMETER_DOCUMENTATION, |
| 'Parameter mismatch: got "%s", expected "%s"' % |
| (params_iter.next(), docs_iter.next()), token) |
| |
| else: |
| # Equality - just advance the iterators |
| params_iter.next() |
| docs_iter.next() |
| |
| elif token_type == Type.STRING_TEXT: |
| # If this is the first token after the start of the string, but it's at |
| # the end of a line, we know we have a multi-line string. |
| if token.previous.type in ( |
| Type.SINGLE_QUOTE_STRING_START, |
| Type.DOUBLE_QUOTE_STRING_START) and last_in_line: |
| self._HandleError(errors.MULTI_LINE_STRING, |
| 'Multi-line strings are not allowed', token) |
| |
| # This check is orthogonal to the ones above, and repeats some types, so |
| # it is a plain if and not an elif. |
| if token.type in Type.COMMENT_TYPES: |
| if self.ILLEGAL_TAB.search(token.string): |
| self._HandleError(errors.ILLEGAL_TAB, |
| 'Illegal tab in comment "%s"' % token.string, token) |
| |
| trimmed = token.string.rstrip() |
| if last_in_line and token.string != trimmed: |
| # Check for extra whitespace at the end of a line. |
| self._HandleError( |
| errors.EXTRA_SPACE, 'Extra space at end of line', token, |
| position=Position(len(trimmed), len(token.string) - len(trimmed))) |
| |
| # This check is also orthogonal since it is based on metadata. |
| if token.metadata.is_implied_semicolon: |
| self._HandleError(errors.MISSING_SEMICOLON, |
| 'Missing semicolon at end of line', token) |
| |
| def _HandleStartBracket(self, token, last_non_space_token): |
| """Handles a token that is an open bracket. |
| |
| Args: |
| token: The token to handle. |
| last_non_space_token: The last token that was not a space. |
| """ |
| if (not token.IsFirstInLine() and token.previous.type == Type.WHITESPACE and |
| last_non_space_token and |
| last_non_space_token.type in Type.EXPRESSION_ENDER_TYPES): |
| self._HandleError( |
| errors.EXTRA_SPACE, 'Extra space before "["', |
| token.previous, position=Position.All(token.previous.string)) |
| # If the [ token is the first token in a line we shouldn't complain |
| # about a missing space before [. This is because some Ecma script |
| # languages allow syntax like: |
| # [Annotation] |
| # class MyClass {...} |
| # So we don't want to blindly warn about missing spaces before [. |
| # In the the future, when rules for computing exactly how many spaces |
| # lines should be indented are added, then we can return errors for |
| # [ tokens that are improperly indented. |
| # For example: |
| # var someVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongVariableName = |
| # [a,b,c]; |
| # should trigger a proper indentation warning message as [ is not indented |
| # by four spaces. |
| elif (not token.IsFirstInLine() and token.previous and |
| token.previous.type not in ( |
| [Type.WHITESPACE, Type.START_PAREN, Type.START_BRACKET] + |
| Type.EXPRESSION_ENDER_TYPES)): |
| self._HandleError(errors.MISSING_SPACE, 'Missing space before "["', |
| token, position=Position.AtBeginning()) |
| |
| def Finalize(self, state): |
| """Perform all checks that need to occur after all lines are processed. |
| |
| Args: |
| state: State of the parser after parsing all tokens |
| |
| Raises: |
| TypeError: If not overridden. |
| """ |
| last_non_space_token = state.GetLastNonSpaceToken() |
| # Check last line for ending with newline. |
| if state.GetLastLine() and not ( |
| state.GetLastLine().isspace() or |
| state.GetLastLine().rstrip('\n\r\f') != state.GetLastLine()): |
| self._HandleError( |
| errors.FILE_MISSING_NEWLINE, |
| 'File does not end with new line. (%s)' % state.GetLastLine(), |
| last_non_space_token) |
| |
| try: |
| self._indentation.Finalize() |
| except Exception, e: |
| self._HandleError( |
| errors.FILE_DOES_NOT_PARSE, |
| str(e), |
| last_non_space_token) |
| |
| def GetLongLineExceptions(self): |
| """Gets a list of regexps for lines which can be longer than the limit. |
| |
| Returns: |
| A list of regexps, used as matches (rather than searches). |
| """ |
| return [] |
| |
| def InExplicitlyTypedLanguage(self): |
| """Returns whether this ecma implementation is explicitly typed.""" |
| return False |