Merged revisions 1907-1940,1942-1967 via svnmerge from
[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:
46     #     newlines;
47     #     double-backslashes;
48     #     a single-line comment "//";
49     #     single or double quotes preceeded by a backslash;
50     #     single quotes, double quotes, open or close braces, semi-colons;
51     #     any alphanumeric token (keyword, class name, specifier);
52     #     the multi-line comment begin and end tokens /* and */;
53     #     array declarations "[]";
54     #     semi-colons;
55     #     periods.
56     _reToken = re.compile(r'(\n|\\\\|//|\\[\'"]|[\'"\{\}\;\.\(\)]|' +
57                           r'[A-Za-z_][\w\.]*|/\*|\*/|\[\])')
58
59     class OuterState:
60         """The initial state for parsing a Java file for classes,
61         interfaces, and anonymous inner classes."""
62         def __init__(self):
63             self.listClasses = []
64             self.listOutputs = []
65             self.stackBrackets = []
66             self.brackets = 0
67             self.nextAnon = 1
68             self.package = None
69
70         def trace(self):
71             pass
72
73         def __getClassState(self):
74             try:
75                 return self.classState
76             except AttributeError:
77                 ret = ClassState(self)
78                 self.classState = ret
79                 return ret
80
81         def __getPackageState(self):
82             try:
83                 return self.packageState
84             except AttributeError:
85                 ret = PackageState(self)
86                 self.packageState = ret
87                 return ret
88
89         def __getAnonClassState(self):
90             try:
91                 return self.anonState
92             except AttributeError:
93                 self.outer_state = self
94                 ret = SkipState(1, AnonClassState(self))
95                 self.anonState = ret
96                 return ret
97
98         def __getSkipState(self):
99             try:
100                 return self.skipState
101             except AttributeError:
102                 ret = SkipState(1, self)
103                 self.skipState = ret
104                 return ret
105
106         def openBracket(self):
107             self.brackets = self.brackets + 1
108
109         def closeBracket(self):
110             self.brackets = self.brackets - 1
111             if len(self.stackBrackets) and \
112                self.brackets == self.stackBrackets[-1]:
113                 self.listOutputs.append(string.join(self.listClasses, '$'))
114                 self.listClasses.pop()
115                 self.stackBrackets.pop()
116
117         def parseToken(self, token):
118             if token[:2] == '//':
119                 return IgnoreState('\n', self)
120             elif token == '/*':
121                 return IgnoreState('*/', self)
122             elif token == '{':
123                 self.openBracket()
124             elif token == '}':
125                 self.closeBracket()
126             elif token in [ '"', "'" ]:
127                 return IgnoreState(token, self)
128             elif token == "new":
129                 # anonymous inner class
130                 if len(self.listClasses) > 0:
131                     return self.__getAnonClassState()
132                 return self.__getSkipState() # Skip the class name
133             elif token in ['class', 'interface', 'enum']:
134                 if len(self.listClasses) == 0:
135                     self.nextAnon = 1
136                 self.stackBrackets.append(self.brackets)
137                 return self.__getClassState()
138             elif token == 'package':
139                 return self.__getPackageState()
140             elif token == '.':
141                 # Skip the attribute, it might be named "class", in which
142                 # case we don't want to treat the following token as
143                 # an inner class name...
144                 return self.__getSkipState()
145             return self
146
147         def addAnonClass(self):
148             """Add an anonymous inner class"""
149             clazz = self.listClasses[0]
150             self.listOutputs.append('%s$%d' % (clazz, self.nextAnon))
151             self.nextAnon = self.nextAnon + 1
152
153         def setPackage(self, package):
154             self.package = package
155
156     class AnonClassState:
157         """A state that looks for anonymous inner classes."""
158         def __init__(self, old_state):
159             # outer_state is always an instance of OuterState
160             self.outer_state = old_state.outer_state
161             self.old_state = old_state
162             self.brace_level = 0
163         def parseToken(self, token):
164             # This is an anonymous class if and only if the next
165             # non-whitespace token is a bracket. Everything between
166             # braces should be parsed as normal java code.
167             if token[:2] == '//':
168                 return IgnoreState('\n', self)
169             elif token == '/*':
170                 return IgnoreState('*/', self)
171             elif token == '\n':
172                 return self
173             elif token == '(':
174                 self.brace_level = self.brace_level + 1
175                 return self
176             if self.brace_level > 0:
177                 if token == 'new':
178                     # look further for anonymous inner class
179                     return SkipState(1, AnonClassState(self))
180                 elif token in [ '"', "'" ]:
181                     return IgnoreState(token, self)
182                 elif token == ')':
183                     self.brace_level = self.brace_level - 1
184                 return self
185             if token == '{':
186                 self.outer_state.addAnonClass()
187             return self.old_state.parseToken(token)
188
189     class SkipState:
190         """A state that will skip a specified number of tokens before
191         reverting to the previous state."""
192         def __init__(self, tokens_to_skip, old_state):
193             self.tokens_to_skip = tokens_to_skip
194             self.old_state = old_state
195         def parseToken(self, token):
196             self.tokens_to_skip = self.tokens_to_skip - 1
197             if self.tokens_to_skip < 1:
198                 return self.old_state
199             return self
200
201     class ClassState:
202         """A state we go into when we hit a class or interface keyword."""
203         def __init__(self, outer_state):
204             # outer_state is always an instance of OuterState
205             self.outer_state = outer_state
206         def parseToken(self, token):
207             # the next non-whitespace token should be the name of the class
208             if token == '\n':
209                 return self
210             self.outer_state.listClasses.append(token)
211             return self.outer_state
212
213     class IgnoreState:
214         """A state that will ignore all tokens until it gets to a
215         specified token."""
216         def __init__(self, ignore_until, old_state):
217             self.ignore_until = ignore_until
218             self.old_state = old_state
219         def parseToken(self, token):
220             if self.ignore_until == token:
221                 return self.old_state
222             return self
223
224     class PackageState:
225         """The state we enter when we encounter the package keyword.
226         We assume the next token will be the package name."""
227         def __init__(self, outer_state):
228             # outer_state is always an instance of OuterState
229             self.outer_state = outer_state
230         def parseToken(self, token):
231             self.outer_state.setPackage(token)
232             return self.outer_state
233
234     def parse_java_file(fn):
235         return parse_java(open(fn, 'r').read())
236
237     def parse_java(contents, trace=None):
238         """Parse a .java file and return a double of package directory,
239         plus a list of .class files that compiling that .java file will
240         produce"""
241         package = None
242         initial = OuterState()
243         currstate = initial
244         for token in _reToken.findall(contents):
245             # The regex produces a bunch of groups, but only one will
246             # have anything in it.
247             currstate = currstate.parseToken(token)
248             if trace: trace(token, currstate)
249         if initial.package:
250             package = string.replace(initial.package, '.', os.sep)
251         return (package, initial.listOutputs)
252
253 else:
254     # Don't actually parse Java files for class names.
255     #
256     # We might make this a configurable option in the future if
257     # Java-file parsing takes too long (although it shouldn't relative
258     # to how long the Java compiler itself seems to take...).
259
260     def parse_java_file(fn):
261         """ "Parse" a .java file.
262
263         This actually just splits the file name, so the assumption here
264         is that the file name matches the public class name, and that
265         the path to the file is the same as the package name.
266         """
267         return os.path.split(file)