Index: third_party/closure_linter/closure_linter/ecmalintrules.py |
diff --git a/third_party/closure_linter/closure_linter/ecmalintrules.py b/third_party/closure_linter/closure_linter/ecmalintrules.py |
index 6ed2e97f5715e006441b7ad0360ca2e095ae895d..80331c25cc4273d5f12a35cbf6e2eeeab3a04de6 100755 |
--- a/third_party/closure_linter/closure_linter/ecmalintrules.py |
+++ b/third_party/closure_linter/closure_linter/ecmalintrules.py |
@@ -41,6 +41,13 @@ 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 |
@@ -92,7 +99,7 @@ class EcmaScriptLintRules(checkerbase.LintRulesBase): |
['@%s' % tag for tag in statetracker.DocFlag.HAS_TYPE]) |
JSDOC_FLAGS_DESCRIPTION_NOT_REQUIRED = frozenset([ |
- '@param', '@return', '@returns']) |
+ '@fileoverview', '@param', '@return', '@returns']) |
def __init__(self): |
"""Initialize this lint rule object.""" |
@@ -128,8 +135,8 @@ class EcmaScriptLintRules(checkerbase.LintRulesBase): |
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.NORMAL): |
- # Dots are acceptable places to wrap. |
+ 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) |
@@ -172,31 +179,29 @@ class EcmaScriptLintRules(checkerbase.LintRulesBase): |
errors.LINE_TOO_LONG, |
'Line too long (%d characters).' % len(line), last_token) |
- def _CheckJsDocType(self, 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. |
""" |
- flag = token.attached_object |
- flag_type = flag.type |
- if flag_type and flag_type is not None and not flag_type.isspace(): |
- pieces = self.TYPE_SPLIT.split(flag_type) |
- if len(pieces) == 1 and flag_type.count('|') == 1 and ( |
- flag_type.endswith('|null') or flag_type.startswith('null|')): |
- self._HandleError( |
- errors.JSDOC_PREFER_QUESTION_TO_PIPE_NULL, |
- 'Prefer "?Type" to "Type|null": "%s"' % flag_type, token) |
+ if not js_type: return |
- # TODO(user): We should do actual parsing of JsDoc types to report an |
- # error for wrong usage of '?' and '|' e.g. {?number|string|null} etc. |
+ 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) |
- 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) |
+ # 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. |
@@ -228,26 +233,49 @@ class EcmaScriptLintRules(checkerbase.LintRulesBase): |
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)): |
+ 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 that binary operators are not used to start lines. |
- if ((not last_code or last_code.line_number != token.line_number) and |
+ # 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 should go on previous line "%s"' % token.string, |
+ '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. |
@@ -260,13 +288,13 @@ class EcmaScriptLintRules(checkerbase.LintRulesBase): |
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 (token.string == ':' and |
- token.metadata.context.type in (Context.LITERAL_ELEMENT, |
- Context.CASE_BLOCK, |
- Context.STATEMENT)): |
+ if self._IsLabel(token): |
return False |
if token.metadata.IsUnaryOperator() and token.IsFirstInLine(): |
@@ -318,19 +346,16 @@ class EcmaScriptLintRules(checkerbase.LintRulesBase): |
self._CheckForMissingSpaceBeforeToken(token) |
elif token_type == Type.END_BLOCK: |
- # This check is for object literal end block tokens, but there is no need |
- # to test that condition since a comma at the end of any other kind of |
- # block is undoubtedly a parse error. |
last_code = token.metadata.last_code |
- 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 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(): |
- is_immediately_called = (token.next and |
- token.next.type == Type.START_PAREN) |
if state.InTopLevelFunction(): |
# A semicolons should not be included at the end of a function |
# declaration. |
@@ -342,10 +367,14 @@ class EcmaScriptLintRules(checkerbase.LintRulesBase): |
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. |
- if state.InAssignedFunction(): |
- if not is_immediately_called and ( |
- last_in_line or token.next.type != Type.SEMICOLON): |
+ # 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', |
@@ -402,13 +431,25 @@ class EcmaScriptLintRules(checkerbase.LintRulesBase): |
token, position=Position.All(token.string)) |
elif token_type == Type.START_PAREN: |
- if token.previous and token.previous.type == Type.KEYWORD: |
+ # 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): |
+ 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)) |
@@ -419,6 +460,15 @@ class EcmaScriptLintRules(checkerbase.LintRulesBase): |
# 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 == |
@@ -429,14 +479,6 @@ class EcmaScriptLintRules(checkerbase.LintRulesBase): |
token.string, token.previous, |
position=Position.All(token.previous.string)) |
- if token.type == Type.END_BRACKET: |
- last_code = token.metadata.last_code |
- 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)) |
- |
elif token_type == Type.WHITESPACE: |
if self.ILLEGAL_TAB.search(token.string): |
if token.IsFirstInLine(): |
@@ -492,7 +534,7 @@ class EcmaScriptLintRules(checkerbase.LintRulesBase): |
'Invalid suppress syntax: should be @suppress {errortype}. ' |
'Spaces matter.', token) |
else: |
- for suppress_type in re.split(r'\||,', flag.type): |
+ for suppress_type in flag.jstype.IterIdentifiers(): |
if suppress_type not in state.GetDocFlag().SUPPRESS_TYPES: |
self._HandleError( |
errors.INVALID_SUPPRESS_TYPE, |
@@ -551,13 +593,20 @@ class EcmaScriptLintRules(checkerbase.LintRulesBase): |
else: |
self._CheckForMissingSpaceBeforeToken(flag.description_start_token) |
- if flag.flag_type in state.GetDocFlag().HAS_TYPE: |
+ if flag.HasType(): |
if flag.type_start_token is not None: |
self._CheckForMissingSpaceBeforeToken( |
token.attached_object.type_start_token) |
- if flag.type and not flag.type.isspace(): |
- self._CheckJsDocType(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 |
@@ -637,14 +686,14 @@ class EcmaScriptLintRules(checkerbase.LintRulesBase): |
# These flags are only legal on localizable message definitions; |
# such variables always begin with the prefix MSG_. |
- for f in ('desc', 'hidden', 'meaning'): |
- if (jsdoc.HasFlag(f) |
- and not identifier.startswith('MSG_') |
- and identifier.find('.MSG_') == -1): |
- self._HandleError( |
- errors.INVALID_USE_OF_DESC_TAG, |
- 'Member "%s" should not have @%s JsDoc' % (identifier, f), |
- token) |
+ 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.') |