source.py (3697B)
1 # Copyright 2009 The Closure Library Authors. All Rights Reserved. 2 # 3 # Licensed under the Apache License, Version 2.0 (the "License"); 4 # you may not use this file except in compliance with the License. 5 # You may obtain a copy of the License at 6 # 7 # http://www.apache.org/licenses/LICENSE-2.0 8 # 9 # Unless required by applicable law or agreed to in writing, software 10 # distributed under the License is distributed on an "AS-IS" BASIS, 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 # See the License for the specific language governing permissions and 13 # limitations under the License. 14 15 16 """Scans a source JS file for its provided and required namespaces. 17 18 Simple class to scan a JavaScript file and express its dependencies. 19 """ 20 21 __author__ = 'nnaze@google.com' 22 23 24 import re 25 26 _BASE_REGEX_STRING = r'^\s*goog\.%s\(\s*[\'"](.+)[\'"]\s*\)' 27 _MODULE_REGEX = re.compile(_BASE_REGEX_STRING % 'module') 28 _PROVIDE_REGEX = re.compile(_BASE_REGEX_STRING % 'provide') 29 30 _REQUIRE_REGEX_STRING = (r'^\s*(?:(?:var|let|const)\s+[a-zA-Z_$][a-zA-Z0-9$_]*' 31 r'\s*=\s*)?goog\.require\(\s*[\'"](.+)[\'"]\s*\)') 32 _REQUIRES_REGEX = re.compile(_REQUIRE_REGEX_STRING) 33 34 35 class Source(object): 36 """Scans a JavaScript source for its provided and required namespaces.""" 37 38 # Matches a "/* ... */" comment. 39 # Note: We can't definitively distinguish a "/*" in a string literal without a 40 # state machine tokenizer. We'll assume that a line starting with whitespace 41 # and "/*" is a comment. 42 _COMMENT_REGEX = re.compile( 43 r""" 44 ^\s* # Start of a new line and whitespace 45 /\* # Opening "/*" 46 .*? # Non greedy match of any characters (including newlines) 47 \*/ # Closing "*/""", 48 re.MULTILINE | re.DOTALL | re.VERBOSE) 49 50 def __init__(self, source): 51 """Initialize a source. 52 53 Args: 54 source: str, The JavaScript source. 55 """ 56 57 self.provides = set() 58 self.requires = set() 59 self.is_goog_module = False 60 61 self._source = source 62 self._ScanSource() 63 64 def GetSource(self): 65 """Get the source as a string.""" 66 return self._source 67 68 @classmethod 69 def _StripComments(cls, source): 70 return cls._COMMENT_REGEX.sub('', source) 71 72 @classmethod 73 def _HasProvideGoogFlag(cls, source): 74 """Determines whether the @provideGoog flag is in a comment.""" 75 for comment_content in cls._COMMENT_REGEX.findall(source): 76 if '@provideGoog' in comment_content: 77 return True 78 79 return False 80 81 def _ScanSource(self): 82 """Fill in provides and requires by scanning the source.""" 83 84 stripped_source = self._StripComments(self.GetSource()) 85 86 source_lines = stripped_source.splitlines() 87 for line in source_lines: 88 match = _PROVIDE_REGEX.match(line) 89 if match: 90 self.provides.add(match.group(1)) 91 match = _MODULE_REGEX.match(line) 92 if match: 93 self.provides.add(match.group(1)) 94 self.is_goog_module = True 95 match = _REQUIRES_REGEX.match(line) 96 if match: 97 self.requires.add(match.group(1)) 98 99 # Closure's base file implicitly provides 'goog'. 100 # This is indicated with the @provideGoog flag. 101 if self._HasProvideGoogFlag(self.GetSource()): 102 103 if len(self.provides) or len(self.requires): 104 raise Exception( 105 'Base file should not provide or require namespaces.') 106 107 self.provides.add('goog') 108 109 110 def GetFileContents(path): 111 """Get a file's contents as a string. 112 113 Args: 114 path: str, Path to file. 115 116 Returns: 117 str, Contents of file. 118 119 Raises: 120 IOError: An error occurred opening or reading the file. 121 122 """ 123 fileobj = open(path) 124 try: 125 return fileobj.read() 126 finally: 127 fileobj.close()