06a4199a3376c0b05546b00a73937f3cec9cd5c4
[scons.git] / src / engine / SCons / Tool / JavaCommon.py
1 """SCons.Tool.JavaCommon
2
3 Stuff for processing Java.
4
5 """
6
7 #
8 # __COPYRIGHT__
9 #
10 # Permission is hereby granted, free of charge, to any person obtaining
11 # a copy of this software and associated documentation files (the
12 # "Software"), to deal in the Software without restriction, including
13 # without limitation the rights to use, copy, modify, merge, publish,
14 # distribute, sublicense, and/or sell copies of the Software, and to
15 # permit persons to whom the Software is furnished to do so, subject to
16 # the following conditions:
17 #
18 # The above copyright notice and this permission notice shall be included
19 # in all copies or substantial portions of the Software.
20 #
21 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
22 # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
23 # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
24 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
25 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
26 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
27 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
28 #
29
30 __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
31
32 import os
33 import os.path
34 import re
35 import string
36
37 java_parsing = 1
38
39 if java_parsing:
40     # Parse Java files for class names.
41     #
42     # This is a really cool parser from Charles Crain
43     # that finds appropriate class names in Java source.
44
45     # A regular expression that will find, in a java file:  newlines;
46     # any alphanumeric token (keyword, class name, specifier); open or
47     # close brackets; a single-line comment "//"; the multi-line comment
48     # begin and end tokens /* and */; single or double quotes;
49     # single or double quotes preceeded by a backslash; array
50     # declarations "[]".
51     _reToken = re.compile(r'(\n|\\\\|//|\\[\'"]|[\'"\{\}]|[A-Za-z_][\w\.]*|' +
52                           r'/\*|\*/|\[\])')
53
54     class OuterState:
55         """The initial state for parsing a Java file for classes,
56         interfaces, and anonymous inner classes."""
57         def __init__(self):
58             self.listClasses = []
59             self.listOutputs = []
60             self.stackBrackets = []
61             self.brackets = 0
62             self.nextAnon = 1
63             self.package = None
64
65         def __getClassState(self):
66             try:
67                 return self.classState
68             except AttributeError:
69                 ret = ClassState(self)
70                 self.classState = ret
71                 return ret
72
73         def __getPackageState(self):
74             try:
75                 return self.packageState
76             except AttributeError:
77                 ret = PackageState(self)
78                 self.packageState = ret
79                 return ret
80
81         def __getAnonClassState(self):
82             try:
83                 return self.anonState
84             except AttributeError:
85                 ret = SkipState(1, AnonClassState(self))
86                 self.anonState = ret
87                 return ret
88
89         def __getSkipState(self):
90             try:
91                 return self.skipState
92             except AttributeError:
93                 ret = SkipState(1, self)
94                 self.skipState = ret
95                 return ret
96
97         def parseToken(self, token):
98             if token[:2] == '//':
99                 return IgnoreState('\n', self)
100             elif token == '/*':
101                 return IgnoreState('*/', self)
102             elif token == '{':
103                 self.brackets = self.brackets + 1
104             elif token == '}':
105                 self.brackets = self.brackets - 1
106                 if len(self.stackBrackets) and \
107                    self.brackets == self.stackBrackets[-1]:
108                     self.listOutputs.append(string.join(self.listClasses, '$'))
109                     self.listClasses.pop()
110                     self.stackBrackets.pop()
111             elif token == '"' or token == "'":
112                 return IgnoreState(token, self)
113             elif token == "new":
114                 # anonymous inner class
115                 if len(self.listClasses) > 0:
116                     return self.__getAnonClassState()
117                 return self.__getSkipState() # Skip the class name
118             elif token in ['class', 'interface', 'enum']:
119                 if len(self.listClasses) == 0:
120                     self.nextAnon = 1
121                 self.stackBrackets.append(self.brackets)
122                 return self.__getClassState()
123             elif token == 'package':
124                 return self.__getPackageState()
125             return self
126
127         def addAnonClass(self):
128             """Add an anonymous inner class"""
129             clazz = self.listClasses[0]
130             self.listOutputs.append('%s$%d' % (clazz, self.nextAnon))
131             self.brackets = self.brackets + 1
132             self.nextAnon = self.nextAnon + 1
133
134         def setPackage(self, package):
135             self.package = package
136
137     class AnonClassState:
138         """A state that looks for anonymous inner classes."""
139         def __init__(self, outer_state):
140             # outer_state is always an instance of OuterState
141             self.outer_state = outer_state
142             self.tokens_to_find = 2
143         def parseToken(self, token):
144             # This is an anonymous class if and only if the next token
145             # is a bracket
146             if token == '{':
147                 self.outer_state.addAnonClass()
148             elif token in ['"', "'"]:
149                 return IgnoreState(token, self)
150             return self.outer_state
151
152     class SkipState:
153         """A state that will skip a specified number of tokens before
154         reverting to the previous state."""
155         def __init__(self, tokens_to_skip, old_state):
156             self.tokens_to_skip = tokens_to_skip
157             self.old_state = old_state
158         def parseToken(self, token):
159             self.tokens_to_skip = self.tokens_to_skip - 1
160             if self.tokens_to_skip < 1:
161                 return self.old_state
162             return self
163
164     class ClassState:
165         """A state we go into when we hit a class or interface keyword."""
166         def __init__(self, outer_state):
167             # outer_state is always an instance of OuterState
168             self.outer_state = outer_state
169         def parseToken(self, token):
170             # the next non-whitespace token should be the name of the class
171             if token == '\n':
172                 return self
173             self.outer_state.listClasses.append(token)
174             return self.outer_state
175
176     class IgnoreState:
177         """A state that will ignore all tokens until it gets to a
178         specified token."""
179         def __init__(self, ignore_until, old_state):
180             self.ignore_until = ignore_until
181             self.old_state = old_state
182         def parseToken(self, token):
183             if self.ignore_until == token:
184                 return self.old_state
185             return self
186
187     class PackageState:
188         """The state we enter when we encounter the package keyword.
189         We assume the next token will be the package name."""
190         def __init__(self, outer_state):
191             # outer_state is always an instance of OuterState
192             self.outer_state = outer_state
193         def parseToken(self, token):
194             self.outer_state.setPackage(token)
195             return self.outer_state
196
197     def parse_java_file(fn):
198         return parse_java(open(fn, 'r').read())
199
200     def parse_java(contents):
201         """Parse a .java file and return a double of package directory,
202         plus a list of .class files that compiling that .java file will
203         produce"""
204         package = None
205         initial = OuterState()
206         currstate = initial
207         for token in _reToken.findall(contents):
208             # The regex produces a bunch of groups, but only one will
209             # have anything in it.
210             currstate = currstate.parseToken(token)
211         if initial.package:
212             package = string.replace(initial.package, '.', os.sep)
213         return (package, initial.listOutputs)
214
215 else:
216     # Don't actually parse Java files for class names.
217     #
218     # We might make this a configurable option in the future if
219     # Java-file parsing takes too long (although it shouldn't relative
220     # to how long the Java compiler itself seems to take...).
221
222     def parse_java_file(fn):
223         """ "Parse" a .java file.
224
225         This actually just splits the file name, so the assumption here
226         is that the file name matches the public class name, and that
227         the path to the file is the same as the package name.
228         """
229         return os.path.split(file)