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