1 """SCons.Tool.JavaCommon
3 Stuff for processing Java.
8 # Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008 The SCons Foundation
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:
18 # The above copyright notice and this permission notice shall be included
19 # in all copies or substantial portions of the Software.
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.
30 __revision__ = "src/engine/SCons/Tool/JavaCommon.py 3842 2008/12/20 22:59:52 scons"
39 default_java_version = '1.4'
42 # Parse Java files for class names.
44 # This is a really cool parser from Charles Crain
45 # that finds appropriate class names in Java source.
47 # A regular expression that will find, in a java file:
50 # a single-line comment "//";
51 # single or double quotes preceeded by a backslash;
52 # single quotes, double quotes, open or close braces, semi-colons,
53 # periods, open or close parentheses;
54 # floating-point numbers;
55 # any alphanumeric token (keyword, class name, specifier);
56 # any alphanumeric token surrounded by angle brackets (generics);
57 # the multi-line comment begin and end tokens /* and */;
58 # array declarations "[]".
59 _reToken = re.compile(r'(\n|\\\\|//|\\[\'"]|[\'"\{\}\;\.\(\)]|' +
60 r'\d*\.\d*|[A-Za-z_][\w\$\.]*|<[A-Za-z_]\w+>|' +
64 """The initial state for parsing a Java file for classes,
65 interfaces, and anonymous inner classes."""
66 def __init__(self, version=default_java_version):
68 if not version in ('1.1', '1.2', '1.3','1.4', '1.5', '1.6'):
69 msg = "Java version %s not supported" % version
70 raise NotImplementedError, msg
72 self.version = version
75 self.stackBrackets = []
78 self.localClasses = []
79 self.stackAnonClassBrackets = []
80 self.anonStacksStack = [[0]]
86 def __getClassState(self):
88 return self.classState
89 except AttributeError:
90 ret = ClassState(self)
94 def __getPackageState(self):
96 return self.packageState
97 except AttributeError:
98 ret = PackageState(self)
99 self.packageState = ret
102 def __getAnonClassState(self):
104 return self.anonState
105 except AttributeError:
106 self.outer_state = self
107 ret = SkipState(1, AnonClassState(self))
111 def __getSkipState(self):
113 return self.skipState
114 except AttributeError:
115 ret = SkipState(1, self)
119 def __getAnonStack(self):
120 return self.anonStacksStack[-1]
122 def openBracket(self):
123 self.brackets = self.brackets + 1
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(string.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()
139 def parseToken(self, token):
140 if token[:2] == '//':
141 return IgnoreState('\n', self)
143 return IgnoreState('*/', self)
148 elif token in [ '"', "'" ]:
149 return IgnoreState(token, self)
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:
158 self.stackBrackets.append(self.brackets)
159 return self.__getClassState()
160 elif token == 'package':
161 return self.__getPackageState()
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()
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'):
175 self.stackAnonClassBrackets.append(self.brackets)
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(string.join(className, '$'))
183 self.nextAnon = self.nextAnon + 1
184 self.__getAnonStack().append(0)
186 def setPackage(self, package):
187 self.package = package
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
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)
203 return IgnoreState('*/', self)
206 elif token[0] == '<' and token[-1] == '>':
209 self.brace_level = self.brace_level + 1
211 if self.brace_level > 0:
213 # look further for anonymous inner class
214 return SkipState(1, AnonClassState(self))
215 elif token in [ '"', "'" ]:
216 return IgnoreState(token, self)
218 self.brace_level = self.brace_level - 1
221 self.outer_state.addAnonClass()
222 return self.old_state.parseToken(token)
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
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
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]
254 locals[token] = 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
264 """A state that will ignore all tokens until it gets to a
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
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
284 def parse_java_file(fn, version=default_java_version):
285 return parse_java(open(fn, 'r').read(), version)
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
292 initial = OuterState(version)
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)
300 package = string.replace(initial.package, '.', os.sep)
301 return (package, initial.listOutputs)
304 # Don't actually parse Java files for class names.
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...).
310 def parse_java_file(fn):
311 """ "Parse" a .java file.
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.
317 return os.path.split(file)