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
18Simple class to scan a JavaScript file and express its dependencies.
19"""
20
21__author__ = 'nnaze@google.com'
22
23
24import re
25
26_BASE_REGEX_STRING = '^\s*goog\.%s\(\s*[\'"](.+)[\'"]\s*\)'
27_PROVIDE_REGEX = re.compile(_BASE_REGEX_STRING % 'provide')
28_REQUIRES_REGEX = re.compile(_BASE_REGEX_STRING % 'require')
29
30
31class Source(object):
32  """Scans a JavaScript source for its provided and required namespaces."""
33
34  # Matches a "/* ... */" comment.
35  # Note: We can't definitively distinguish a "/*" in a string literal without a
36  # state machine tokenizer. We'll assume that a line starting with whitespace
37  # and "/*" is a comment.
38  _COMMENT_REGEX = re.compile(
39      r"""
40      ^\s*   # Start of a new line and whitespace
41      /\*    # Opening "/*"
42      .*?    # Non greedy match of any characters (including newlines)
43      \*/    # Closing "*/""",
44      re.MULTILINE | re.DOTALL | re.VERBOSE)
45
46  def __init__(self, source):
47    """Initialize a source.
48
49    Args:
50      source: str, The JavaScript source.
51    """
52
53    self.provides = set()
54    self.requires = set()
55
56    self._source = source
57    self._ScanSource()
58
59  def GetSource(self):
60    """Get the source as a string."""
61    return self._source
62
63  @classmethod
64  def _StripComments(cls, source):
65    return cls._COMMENT_REGEX.sub('', source)
66
67  @classmethod
68  def _HasProvideGoogFlag(cls, source):
69    """Determines whether the @provideGoog flag is in a comment."""
70    for comment_content in cls._COMMENT_REGEX.findall(source):
71      if '@provideGoog' in comment_content:
72        return True
73
74    return False
75
76  def _ScanSource(self):
77    """Fill in provides and requires by scanning the source."""
78
79    stripped_source = self._StripComments(self.GetSource())
80
81    source_lines = stripped_source.splitlines()
82    for line in source_lines:
83      match = _PROVIDE_REGEX.match(line)
84      if match:
85        self.provides.add(match.group(1))
86      match = _REQUIRES_REGEX.match(line)
87      if match:
88        self.requires.add(match.group(1))
89
90    # Closure's base file implicitly provides 'goog'.
91    # This is indicated with the @provideGoog flag.
92    if self._HasProvideGoogFlag(self.GetSource()):
93
94      if len(self.provides) or len(self.requires):
95        raise Exception(
96            'Base file should not provide or require namespaces.')
97
98      self.provides.add('goog')
99
100
101def GetFileContents(path):
102  """Get a file's contents as a string.
103
104  Args:
105    path: str, Path to file.
106
107  Returns:
108    str, Contents of file.
109
110  Raises:
111    IOError: An error occurred opening or reading the file.
112
113  """
114  fileobj = open(path)
115  try:
116    return fileobj.read()
117  finally:
118    fileobj.close()
119