# Copyright 2009 The Closure Library 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. """Scans a source JS file for its provided and required namespaces. Simple class to scan a JavaScript file and express its dependencies. """ __author__ = 'nnaze@google.com' import codecs import re _BASE_REGEX_STRING = r'^\s*goog\.%s\(\s*[\'"](.+)[\'"]\s*\)' _MODULE_REGEX = re.compile(_BASE_REGEX_STRING % 'module') _PROVIDE_REGEX = re.compile(_BASE_REGEX_STRING % 'provide') _REQUIRE_REGEX_STRING = (r'^\s*(?:(?:var|let|const)\s+[a-zA-Z0-9$_,:{}\s]*' r'\s*=\s*)?goog\.require\(\s*[\'"](.+)[\'"]\s*\)') _REQUIRES_REGEX = re.compile(_REQUIRE_REGEX_STRING) class Source(object): """Scans a JavaScript source for its provided and required namespaces.""" # Matches a "/* ... */" comment. # Note: We can't definitively distinguish a "/*" in a string literal without a # state machine tokenizer. We'll assume that a line starting with whitespace # and "/*" is a comment. _COMMENT_REGEX = re.compile( r""" ^\s* # Start of a new line and whitespace /\* # Opening "/*" .*? # Non greedy match of any characters (including newlines) \*/ # Closing "*/""", re.MULTILINE | re.DOTALL | re.VERBOSE) def __init__(self, source): """Initialize a source. Args: source: str, The JavaScript source. """ self.provides = set() self.requires = set() self.is_goog_module = False self._source = source self._ScanSource() def GetSource(self): """Get the source as a string.""" return self._source @classmethod def _StripComments(cls, source): return cls._COMMENT_REGEX.sub('', source) @classmethod def _HasProvideGoogFlag(cls, source): """Determines whether the @provideGoog flag is in a comment.""" for comment_content in cls._COMMENT_REGEX.findall(source): if '@provideGoog' in comment_content: return True return False def _ScanSource(self): """Fill in provides and requires by scanning the source.""" stripped_source = self._StripComments(self.GetSource()) source_lines = stripped_source.splitlines() for line in source_lines: match = _PROVIDE_REGEX.match(line) if match: self.provides.add(match.group(1)) match = _MODULE_REGEX.match(line) if match: self.provides.add(match.group(1)) self.is_goog_module = True match = _REQUIRES_REGEX.match(line) if match: self.requires.add(match.group(1)) # Closure's base file implicitly provides 'goog'. # This is indicated with the @provideGoog flag. if self._HasProvideGoogFlag(self.GetSource()): if len(self.provides) or len(self.requires): raise Exception( 'Base file should not provide or require namespaces.') self.provides.add('goog') def GetFileContents(path): """Get a file's contents as a string. Args: path: str, Path to file. Returns: str, Contents of file. Raises: IOError: An error occurred opening or reading the file. """ fileobj = None try: fileobj = codecs.open(path, encoding='utf-8-sig') return fileobj.read() except IOError as error: raise IOError('An error occurred opening or reading the file: %s. %s' % (path, error)) finally: if fileobj is not None: fileobj.close()