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