1#!/usr/bin/env python 2# 3# Copyright 2007 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"""Regular expression based JavaScript parsing classes.""" 18 19__author__ = ('robbyw@google.com (Robert Walker)', 20 'ajp@google.com (Andy Perelson)') 21 22import copy 23import re 24 25from closure_linter import javascripttokens 26from closure_linter.common import matcher 27from closure_linter.common import tokenizer 28 29# Shorthand 30Type = javascripttokens.JavaScriptTokenType 31Matcher = matcher.Matcher 32 33 34class JavaScriptModes(object): 35 """Enumeration of the different matcher modes used for JavaScript.""" 36 TEXT_MODE = 'text' 37 SINGLE_QUOTE_STRING_MODE = 'single_quote_string' 38 DOUBLE_QUOTE_STRING_MODE = 'double_quote_string' 39 BLOCK_COMMENT_MODE = 'block_comment' 40 DOC_COMMENT_MODE = 'doc_comment' 41 DOC_COMMENT_LEX_SPACES_MODE = 'doc_comment_spaces' 42 LINE_COMMENT_MODE = 'line_comment' 43 PARAMETER_MODE = 'parameter' 44 FUNCTION_MODE = 'function' 45 46 47class JavaScriptTokenizer(tokenizer.Tokenizer): 48 """JavaScript tokenizer. 49 50 Convert JavaScript code in to an array of tokens. 51 """ 52 53 # Useful patterns for JavaScript parsing. 54 IDENTIFIER_CHAR = r'A-Za-z0-9_$.' 55 56 # Number patterns based on: 57 # http://www.mozilla.org/js/language/js20-2000-07/formal/lexer-grammar.html 58 MANTISSA = r""" 59 (\d+(?!\.)) | # Matches '10' 60 (\d+\.(?!\d)) | # Matches '10.' 61 (\d*\.\d+) # Matches '.5' or '10.5' 62 """ 63 DECIMAL_LITERAL = r'(%s)([eE][-+]?\d+)?' % MANTISSA 64 HEX_LITERAL = r'0[xX][0-9a-fA-F]+' 65 NUMBER = re.compile(r""" 66 ((%s)|(%s)) 67 """ % (HEX_LITERAL, DECIMAL_LITERAL), re.VERBOSE) 68 69 # Strings come in three parts - first we match the start of the string, then 70 # the contents, then the end. The contents consist of any character except a 71 # backslash or end of string, or a backslash followed by any character, or a 72 # backslash followed by end of line to support correct parsing of multi-line 73 # strings. 74 SINGLE_QUOTE = re.compile(r"'") 75 SINGLE_QUOTE_TEXT = re.compile(r"([^'\\]|\\(.|$))+") 76 DOUBLE_QUOTE = re.compile(r'"') 77 DOUBLE_QUOTE_TEXT = re.compile(r'([^"\\]|\\(.|$))+') 78 79 START_SINGLE_LINE_COMMENT = re.compile(r'//') 80 END_OF_LINE_SINGLE_LINE_COMMENT = re.compile(r'//$') 81 82 START_DOC_COMMENT = re.compile(r'/\*\*') 83 START_BLOCK_COMMENT = re.compile(r'/\*') 84 END_BLOCK_COMMENT = re.compile(r'\*/') 85 BLOCK_COMMENT_TEXT = re.compile(r'([^*]|\*(?!/))+') 86 87 # Comment text is anything that we are not going to parse into another special 88 # token like (inline) flags or end comments. Complicated regex to match 89 # most normal characters, and '*', '{', '}', and '@' when we are sure that 90 # it is safe. Expression [^*{\s]@ must come first, or the other options will 91 # match everything before @, and we won't match @'s that aren't part of flags 92 # like in email addresses in the @author tag. 93 DOC_COMMENT_TEXT = re.compile(r'([^*{}\s]@|[^*{}@]|\*(?!/))+') 94 DOC_COMMENT_NO_SPACES_TEXT = re.compile(r'([^*{}\s]@|[^*{}@\s]|\*(?!/))+') 95 96 # Match the prefix ' * ' that starts every line of jsdoc. Want to include 97 # spaces after the '*', but nothing else that occurs after a '*', and don't 98 # want to match the '*' in '*/'. 99 DOC_PREFIX = re.compile(r'\s*\*(\s+|(?!/))') 100 101 START_BLOCK = re.compile('{') 102 END_BLOCK = re.compile('}') 103 104 REGEX_CHARACTER_CLASS = r""" 105 \[ # Opening bracket 106 ([^\]\\]|\\.)* # Anything but a ] or \, 107 # or a backslash followed by anything 108 \] # Closing bracket 109 """ 110 # We ensure the regex is followed by one of the above tokens to avoid 111 # incorrectly parsing something like x / y / z as x REGEX(/ y /) z 112 POST_REGEX_LIST = [ 113 ';', ',', r'\.', r'\)', r'\]', '$', r'\/\/', r'\/\*', ':', '}'] 114 115 REGEX = re.compile(r""" 116 / # opening slash 117 (?!\*) # not the start of a comment 118 (\\.|[^\[\/\\]|(%s))* # a backslash followed by anything, 119 # or anything but a / or [ or \, 120 # or a character class 121 / # closing slash 122 [gimsx]* # optional modifiers 123 (?=\s*(%s)) 124 """ % (REGEX_CHARACTER_CLASS, '|'.join(POST_REGEX_LIST)), 125 re.VERBOSE) 126 127 ANYTHING = re.compile(r'.*') 128 PARAMETERS = re.compile(r'[^\)]+') 129 CLOSING_PAREN_WITH_SPACE = re.compile(r'\)\s*') 130 131 FUNCTION_DECLARATION = re.compile(r'\bfunction\b') 132 133 OPENING_PAREN = re.compile(r'\(') 134 CLOSING_PAREN = re.compile(r'\)') 135 136 OPENING_BRACKET = re.compile(r'\[') 137 CLOSING_BRACKET = re.compile(r'\]') 138 139 # We omit these JS keywords from the list: 140 # function - covered by FUNCTION_DECLARATION. 141 # delete, in, instanceof, new, typeof - included as operators. 142 # this - included in identifiers. 143 # null, undefined - not included, should go in some "special constant" list. 144 KEYWORD_LIST = ['break', 'case', 'catch', 'continue', 'default', 'do', 'else', 145 'finally', 'for', 'if', 'return', 'switch', 'throw', 'try', 'var', 146 'while', 'with'] 147 # Match a keyword string followed by a non-identifier character in order to 148 # not match something like doSomething as do + Something. 149 KEYWORD = re.compile('(%s)((?=[^%s])|$)' % ( 150 '|'.join(KEYWORD_LIST), IDENTIFIER_CHAR)) 151 152 # List of regular expressions to match as operators. Some notes: for our 153 # purposes, the comma behaves similarly enough to a normal operator that we 154 # include it here. r'\bin\b' actually matches 'in' surrounded by boundary 155 # characters - this may not match some very esoteric uses of the in operator. 156 # Operators that are subsets of larger operators must come later in this list 157 # for proper matching, e.g., '>>' must come AFTER '>>>'. 158 OPERATOR_LIST = [',', r'\+\+', '===', '!==', '>>>=', '>>>', '==', '>=', '<=', 159 '!=', '<<=', '>>=', '<<', '>>', '>', '<', r'\+=', r'\+', 160 '--', '\^=', '-=', '-', '/=', '/', r'\*=', r'\*', '%=', '%', 161 '&&', r'\|\|', '&=', '&', r'\|=', r'\|', '=', '!', ':', '\?', 162 r'\bdelete\b', r'\bin\b', r'\binstanceof\b', r'\bnew\b', 163 r'\btypeof\b', r'\bvoid\b'] 164 OPERATOR = re.compile('|'.join(OPERATOR_LIST)) 165 166 WHITESPACE = re.compile(r'\s+') 167 SEMICOLON = re.compile(r';') 168 # Technically JavaScript identifiers can't contain '.', but we treat a set of 169 # nested identifiers as a single identifier. 170 NESTED_IDENTIFIER = r'[a-zA-Z_$][%s.]*' % IDENTIFIER_CHAR 171 IDENTIFIER = re.compile(NESTED_IDENTIFIER) 172 173 SIMPLE_LVALUE = re.compile(r""" 174 (?P<identifier>%s) # a valid identifier 175 (?=\s* # optional whitespace 176 \= # look ahead to equal sign 177 (?!=)) # not follwed by equal 178 """ % NESTED_IDENTIFIER, re.VERBOSE) 179 180 # A doc flag is a @ sign followed by non-space characters that appears at the 181 # beginning of the line, after whitespace, or after a '{'. The look-behind 182 # check is necessary to not match someone@google.com as a flag. 183 DOC_FLAG = re.compile(r'(^|(?<=\s))@(?P<name>[a-zA-Z]+)') 184 # To properly parse parameter names, we need to tokenize whitespace into a 185 # token. 186 DOC_FLAG_LEX_SPACES = re.compile(r'(^|(?<=\s))@(?P<name>%s)\b' % 187 '|'.join(['param'])) 188 189 DOC_INLINE_FLAG = re.compile(r'(?<={)@(?P<name>[a-zA-Z]+)') 190 191 # Star followed by non-slash, i.e a star that does not end a comment. 192 # This is used for TYPE_GROUP below. 193 SAFE_STAR = r'(\*(?!/))' 194 195 COMMON_DOC_MATCHERS = [ 196 # Find the end of the comment. 197 Matcher(END_BLOCK_COMMENT, Type.END_DOC_COMMENT, 198 JavaScriptModes.TEXT_MODE), 199 200 # Tokenize documented flags like @private. 201 Matcher(DOC_INLINE_FLAG, Type.DOC_INLINE_FLAG), 202 Matcher(DOC_FLAG_LEX_SPACES, Type.DOC_FLAG, 203 JavaScriptModes.DOC_COMMENT_LEX_SPACES_MODE), 204 205 # Encountering a doc flag should leave lex spaces mode. 206 Matcher(DOC_FLAG, Type.DOC_FLAG, JavaScriptModes.DOC_COMMENT_MODE), 207 208 # Tokenize braces so we can find types. 209 Matcher(START_BLOCK, Type.DOC_START_BRACE), 210 Matcher(END_BLOCK, Type.DOC_END_BRACE), 211 Matcher(DOC_PREFIX, Type.DOC_PREFIX, None, True)] 212 213 214 # The token matcher groups work as follows: it is an list of Matcher objects. 215 # The matchers will be tried in this order, and the first to match will be 216 # returned. Hence the order is important because the matchers that come first 217 # overrule the matchers that come later. 218 JAVASCRIPT_MATCHERS = { 219 # Matchers for basic text mode. 220 JavaScriptModes.TEXT_MODE: [ 221 # Check a big group - strings, starting comments, and regexes - all 222 # of which could be intertwined. 'string with /regex/', 223 # /regex with 'string'/, /* comment with /regex/ and string */ (and so 224 # on) 225 Matcher(START_DOC_COMMENT, Type.START_DOC_COMMENT, 226 JavaScriptModes.DOC_COMMENT_MODE), 227 Matcher(START_BLOCK_COMMENT, Type.START_BLOCK_COMMENT, 228 JavaScriptModes.BLOCK_COMMENT_MODE), 229 Matcher(END_OF_LINE_SINGLE_LINE_COMMENT, 230 Type.START_SINGLE_LINE_COMMENT), 231 Matcher(START_SINGLE_LINE_COMMENT, Type.START_SINGLE_LINE_COMMENT, 232 JavaScriptModes.LINE_COMMENT_MODE), 233 Matcher(SINGLE_QUOTE, Type.SINGLE_QUOTE_STRING_START, 234 JavaScriptModes.SINGLE_QUOTE_STRING_MODE), 235 Matcher(DOUBLE_QUOTE, Type.DOUBLE_QUOTE_STRING_START, 236 JavaScriptModes.DOUBLE_QUOTE_STRING_MODE), 237 Matcher(REGEX, Type.REGEX), 238 239 # Next we check for start blocks appearing outside any of the items 240 # above. 241 Matcher(START_BLOCK, Type.START_BLOCK), 242 Matcher(END_BLOCK, Type.END_BLOCK), 243 244 # Then we search for function declarations. 245 Matcher(FUNCTION_DECLARATION, Type.FUNCTION_DECLARATION, 246 JavaScriptModes.FUNCTION_MODE), 247 248 # Next, we convert non-function related parens to tokens. 249 Matcher(OPENING_PAREN, Type.START_PAREN), 250 Matcher(CLOSING_PAREN, Type.END_PAREN), 251 252 # Next, we convert brackets to tokens. 253 Matcher(OPENING_BRACKET, Type.START_BRACKET), 254 Matcher(CLOSING_BRACKET, Type.END_BRACKET), 255 256 # Find numbers. This has to happen before operators because scientific 257 # notation numbers can have + and - in them. 258 Matcher(NUMBER, Type.NUMBER), 259 260 # Find operators and simple assignments 261 Matcher(SIMPLE_LVALUE, Type.SIMPLE_LVALUE), 262 Matcher(OPERATOR, Type.OPERATOR), 263 264 # Find key words and whitespace. 265 Matcher(KEYWORD, Type.KEYWORD), 266 Matcher(WHITESPACE, Type.WHITESPACE), 267 268 # Find identifiers. 269 Matcher(IDENTIFIER, Type.IDENTIFIER), 270 271 # Finally, we convert semicolons to tokens. 272 Matcher(SEMICOLON, Type.SEMICOLON)], 273 274 # Matchers for single quote strings. 275 JavaScriptModes.SINGLE_QUOTE_STRING_MODE: [ 276 Matcher(SINGLE_QUOTE_TEXT, Type.STRING_TEXT), 277 Matcher(SINGLE_QUOTE, Type.SINGLE_QUOTE_STRING_END, 278 JavaScriptModes.TEXT_MODE)], 279 280 # Matchers for double quote strings. 281 JavaScriptModes.DOUBLE_QUOTE_STRING_MODE: [ 282 Matcher(DOUBLE_QUOTE_TEXT, Type.STRING_TEXT), 283 Matcher(DOUBLE_QUOTE, Type.DOUBLE_QUOTE_STRING_END, 284 JavaScriptModes.TEXT_MODE)], 285 286 # Matchers for block comments. 287 JavaScriptModes.BLOCK_COMMENT_MODE: [ 288 # First we check for exiting a block comment. 289 Matcher(END_BLOCK_COMMENT, Type.END_BLOCK_COMMENT, 290 JavaScriptModes.TEXT_MODE), 291 292 # Match non-comment-ending text.. 293 Matcher(BLOCK_COMMENT_TEXT, Type.COMMENT)], 294 295 # Matchers for doc comments. 296 JavaScriptModes.DOC_COMMENT_MODE: COMMON_DOC_MATCHERS + [ 297 Matcher(DOC_COMMENT_TEXT, Type.COMMENT)], 298 299 JavaScriptModes.DOC_COMMENT_LEX_SPACES_MODE: COMMON_DOC_MATCHERS + [ 300 Matcher(WHITESPACE, Type.COMMENT), 301 Matcher(DOC_COMMENT_NO_SPACES_TEXT, Type.COMMENT)], 302 303 # Matchers for single line comments. 304 JavaScriptModes.LINE_COMMENT_MODE: [ 305 # We greedy match until the end of the line in line comment mode. 306 Matcher(ANYTHING, Type.COMMENT, JavaScriptModes.TEXT_MODE)], 307 308 # Matchers for code after the function keyword. 309 JavaScriptModes.FUNCTION_MODE: [ 310 # Must match open paren before anything else and move into parameter 311 # mode, otherwise everything inside the parameter list is parsed 312 # incorrectly. 313 Matcher(OPENING_PAREN, Type.START_PARAMETERS, 314 JavaScriptModes.PARAMETER_MODE), 315 Matcher(WHITESPACE, Type.WHITESPACE), 316 Matcher(IDENTIFIER, Type.FUNCTION_NAME)], 317 318 # Matchers for function parameters 319 JavaScriptModes.PARAMETER_MODE: [ 320 # When in function parameter mode, a closing paren is treated specially. 321 # Everything else is treated as lines of parameters. 322 Matcher(CLOSING_PAREN_WITH_SPACE, Type.END_PARAMETERS, 323 JavaScriptModes.TEXT_MODE), 324 Matcher(PARAMETERS, Type.PARAMETERS, JavaScriptModes.PARAMETER_MODE)]} 325 326 # When text is not matched, it is given this default type based on mode. 327 # If unspecified in this map, the default default is Type.NORMAL. 328 JAVASCRIPT_DEFAULT_TYPES = { 329 JavaScriptModes.DOC_COMMENT_MODE: Type.COMMENT, 330 JavaScriptModes.DOC_COMMENT_LEX_SPACES_MODE: Type.COMMENT 331 } 332 333 def __init__(self, parse_js_doc = True): 334 """Create a tokenizer object. 335 336 Args: 337 parse_js_doc: Whether to do detailed parsing of javascript doc comments, 338 or simply treat them as normal comments. Defaults to parsing JsDoc. 339 """ 340 matchers = self.JAVASCRIPT_MATCHERS 341 if not parse_js_doc: 342 # Make a copy so the original doesn't get modified. 343 matchers = copy.deepcopy(matchers) 344 matchers[JavaScriptModes.DOC_COMMENT_MODE] = matchers[ 345 JavaScriptModes.BLOCK_COMMENT_MODE] 346 347 tokenizer.Tokenizer.__init__(self, JavaScriptModes.TEXT_MODE, matchers, 348 self.JAVASCRIPT_DEFAULT_TYPES) 349 350 def _CreateToken(self, string, token_type, line, line_number, values=None): 351 """Creates a new JavaScriptToken object. 352 353 Args: 354 string: The string of input the token contains. 355 token_type: The type of token. 356 line: The text of the line this token is in. 357 line_number: The line number of the token. 358 values: A dict of named values within the token. For instance, a 359 function declaration may have a value called 'name' which captures the 360 name of the function. 361 """ 362 return javascripttokens.JavaScriptToken(string, token_type, line, 363 line_number, values) 364