http://scons.tigris.org/issues/show_bug.cgi?id=2345
[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
36 java_parsing = 1
37
38 default_java_version = '1.4'
39
40 if java_parsing:
41     # Parse Java files for class names.
42     #
43     # This is a really cool parser from Charles Crain
44     # that finds appropriate class names in Java source.
45
46     # A regular expression that will find, in a java file:
47     #     newlines;
48     #     double-backslashes;
49     #     a single-line comment "//";
50     #     single or double quotes preceeded by a backslash;
51     #     single quotes, double quotes, open or close braces, semi-colons,
52     #         periods, open or close parentheses;
53     #     floating-point numbers;
54     #     any alphanumeric token (keyword, class name, specifier);
55     #     any alphanumeric token surrounded by angle brackets (generics);
56     #     the multi-line comment begin and end tokens /* and */;
57     #     array declarations "[]".
58     _reToken = re.compile(r'(\n|\\\\|//|\\[\'"]|[\'"\{\}\;\.\(\)]|' +
59                           r'\d*\.\d*|[A-Za-z_][\w\$\.]*|<[A-Za-z_]\w+>|' +
60                           r'/\*|\*/|\[\])')
61
62     class OuterState:
63         """The initial state for parsing a Java file for classes,
64         interfaces, and anonymous inner classes."""
65         def __init__(self, version=default_java_version):
66
67             if not version in ('1.1', '1.2', '1.3','1.4', '1.5', '1.6',
68                                '5', '6'):
69                 msg = "Java version %s not supported" % version
70                 raise NotImplementedError(msg)
71
72             self.version = version
73             self.listClasses = []
74             self.listOutputs = []
75             self.stackBrackets = []
76             self.brackets = 0
77             self.nextAnon = 1
78             self.localClasses = []
79             self.stackAnonClassBrackets = []
80             self.anonStacksStack = [[0]]
81             self.package = None
82
83         def trace(self):
84             pass
85
86         def __getClassState(self):
87             try:
88                 return self.classState
89             except AttributeError:
90                 ret = ClassState(self)
91                 self.classState = ret
92                 return ret
93
94         def __getPackageState(self):
95             try:
96                 return self.packageState
97             except AttributeError:
98                 ret = PackageState(self)
99                 self.packageState = ret
100                 return ret
101
102         def __getAnonClassState(self):
103             try:
104                 return self.anonState
105             except AttributeError:
106                 self.outer_state = self
107                 ret = SkipState(1, AnonClassState(self))
108                 self.anonState = ret
109                 return ret
110
111         def __getSkipState(self):
112             try:
113                 return self.skipState
114             except AttributeError:
115                 ret = SkipState(1, self)
116                 self.skipState = ret
117                 return ret
118         
119         def __getAnonStack(self):
120             return self.anonStacksStack[-1]
121
122         def openBracket(self):
123             self.brackets = self.brackets + 1
124
125         def closeBracket(self):
126             self.brackets = self.brackets - 1
127             if len(self.stackBrackets) and \
128                self.brackets == self.stackBrackets[-1]:
129                 self.listOutputs.append('$'.join(self.listClasses))
130                 self.localClasses.pop()
131                 self.listClasses.pop()
132                 self.anonStacksStack.pop()
133                 self.stackBrackets.pop()
134             if len(self.stackAnonClassBrackets) and \
135                self.brackets == self.stackAnonClassBrackets[-1]:
136                 self.__getAnonStack().pop()
137                 self.stackAnonClassBrackets.pop()
138
139         def parseToken(self, token):
140             if token[:2] == '//':
141                 return IgnoreState('\n', self)
142             elif token == '/*':
143                 return IgnoreState('*/', self)
144             elif token == '{':
145                 self.openBracket()
146             elif token == '}':
147                 self.closeBracket()
148             elif token in [ '"', "'" ]:
149                 return IgnoreState(token, self)
150             elif token == "new":
151                 # anonymous inner class
152                 if len(self.listClasses) > 0:
153                     return self.__getAnonClassState()
154                 return self.__getSkipState() # Skip the class name
155             elif token in ['class', 'interface', 'enum']:
156                 if len(self.listClasses) == 0:
157                     self.nextAnon = 1
158                 self.stackBrackets.append(self.brackets)
159                 return self.__getClassState()
160             elif token == 'package':
161                 return self.__getPackageState()
162             elif token == '.':
163                 # Skip the attribute, it might be named "class", in which
164                 # case we don't want to treat the following token as
165                 # an inner class name...
166                 return self.__getSkipState()
167             return self
168
169         def addAnonClass(self):
170             """Add an anonymous inner class"""
171             if self.version in ('1.1', '1.2', '1.3', '1.4'):
172                 clazz = self.listClasses[0]
173                 self.listOutputs.append('%s$%d' % (clazz, self.nextAnon))
174             elif self.version in ('1.5', '1.6', '5', '6'):
175                 self.stackAnonClassBrackets.append(self.brackets)
176                 className = []
177                 className.extend(self.listClasses)
178                 self.__getAnonStack()[-1] = self.__getAnonStack()[-1] + 1
179                 for anon in self.__getAnonStack():
180                     className.append(str(anon))
181                 self.listOutputs.append('$'.join(className))
182
183             self.nextAnon = self.nextAnon + 1
184             self.__getAnonStack().append(0)
185
186         def setPackage(self, package):
187             self.package = package
188
189     class AnonClassState:
190         """A state that looks for anonymous inner classes."""
191         def __init__(self, old_state):
192             # outer_state is always an instance of OuterState
193             self.outer_state = old_state.outer_state
194             self.old_state = old_state
195             self.brace_level = 0
196         def parseToken(self, token):
197             # This is an anonymous class if and only if the next
198             # non-whitespace token is a bracket. Everything between
199             # braces should be parsed as normal java code.
200             if token[:2] == '//':
201                 return IgnoreState('\n', self)
202             elif token == '/*':
203                 return IgnoreState('*/', self)
204             elif token == '\n':
205                 return self
206             elif token[0] == '<' and token[-1] == '>':
207                 return self
208             elif token == '(':
209                 self.brace_level = self.brace_level + 1
210                 return self
211             if self.brace_level > 0:
212                 if token == 'new':
213                     # look further for anonymous inner class
214                     return SkipState(1, AnonClassState(self))
215                 elif token in [ '"', "'" ]:
216                     return IgnoreState(token, self)
217                 elif token == ')':
218                     self.brace_level = self.brace_level - 1
219                 return self
220             if token == '{':
221                 self.outer_state.addAnonClass()
222             return self.old_state.parseToken(token)
223
224     class SkipState:
225         """A state that will skip a specified number of tokens before
226         reverting to the previous state."""
227         def __init__(self, tokens_to_skip, old_state):
228             self.tokens_to_skip = tokens_to_skip
229             self.old_state = old_state
230         def parseToken(self, token):
231             self.tokens_to_skip = self.tokens_to_skip - 1
232             if self.tokens_to_skip < 1:
233                 return self.old_state
234             return self
235
236     class ClassState:
237         """A state we go into when we hit a class or interface keyword."""
238         def __init__(self, outer_state):
239             # outer_state is always an instance of OuterState
240             self.outer_state = outer_state
241         def parseToken(self, token):
242             # the next non-whitespace token should be the name of the class
243             if token == '\n':
244                 return self
245             # If that's an inner class which is declared in a method, it
246             # requires an index prepended to the class-name, e.g.
247             # 'Foo$1Inner' (Tigris Issue 2087)
248             if self.outer_state.localClasses and \
249                 self.outer_state.stackBrackets[-1] > \
250                 self.outer_state.stackBrackets[-2]+1:
251                 locals = self.outer_state.localClasses[-1]
252                 try:
253                     idx = locals[token]
254                     locals[token] = locals[token]+1
255                 except KeyError:
256                     locals[token] = 1
257                 token = str(locals[token]) + token
258             self.outer_state.localClasses.append({})
259             self.outer_state.listClasses.append(token)
260             self.outer_state.anonStacksStack.append([0])
261             return self.outer_state
262
263     class IgnoreState:
264         """A state that will ignore all tokens until it gets to a
265         specified token."""
266         def __init__(self, ignore_until, old_state):
267             self.ignore_until = ignore_until
268             self.old_state = old_state
269         def parseToken(self, token):
270             if self.ignore_until == token:
271                 return self.old_state
272             return self
273
274     class PackageState:
275         """The state we enter when we encounter the package keyword.
276         We assume the next token will be the package name."""
277         def __init__(self, outer_state):
278             # outer_state is always an instance of OuterState
279             self.outer_state = outer_state
280         def parseToken(self, token):
281             self.outer_state.setPackage(token)
282             return self.outer_state
283
284     def parse_java_file(fn, version=default_java_version):
285         return parse_java(open(fn, 'r').read(), version)
286
287     def parse_java(contents, version=default_java_version, trace=None):
288         """Parse a .java file and return a double of package directory,
289         plus a list of .class files that compiling that .java file will
290         produce"""
291         package = None
292         initial = OuterState(version)
293         currstate = initial
294         for token in _reToken.findall(contents):
295             # The regex produces a bunch of groups, but only one will
296             # have anything in it.
297             currstate = currstate.parseToken(token)
298             if trace: trace(token, currstate)
299         if initial.package:
300             package = initial.package.replace('.', os.sep)
301         return (package, initial.listOutputs)
302
303 else:
304     # Don't actually parse Java files for class names.
305     #
306     # We might make this a configurable option in the future if
307     # Java-file parsing takes too long (although it shouldn't relative
308     # to how long the Java compiler itself seems to take...).
309
310     def parse_java_file(fn):
311         """ "Parse" a .java file.
312
313         This actually just splits the file name, so the assumption here
314         is that the file name matches the public class name, and that
315         the path to the file is the same as the package name.
316         """
317         return os.path.split(file)
318
319 # Local Variables:
320 # tab-width:4
321 # indent-tabs-mode:nil
322 # End:
323 # vim: set expandtab tabstop=4 shiftwidth=4: