1 """SCons.Tool.JavaCommon
3 Stuff for processing Java.
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__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
40 # Parse Java files for class names.
42 # This is a really cool parser from Charles Crain
43 # that finds appropriate class names in Java source.
45 # A regular expression that will find, in a java file:
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 "[]";
56 _reToken = re.compile(r'(\n|\\\\|//|\\[\'"]|[\'"\{\}\;\.\(\)]|' +
57 r'[A-Za-z_][\w\.]*|/\*|\*/|\[\])')
60 """The initial state for parsing a Java file for classes,
61 interfaces, and anonymous inner classes."""
65 self.stackBrackets = []
73 def __getClassState(self):
75 return self.classState
76 except AttributeError:
77 ret = ClassState(self)
81 def __getPackageState(self):
83 return self.packageState
84 except AttributeError:
85 ret = PackageState(self)
86 self.packageState = ret
89 def __getAnonClassState(self):
92 except AttributeError:
93 self.outer_state = self
94 ret = SkipState(1, AnonClassState(self))
98 def __getSkipState(self):
100 return self.skipState
101 except AttributeError:
102 ret = SkipState(1, self)
106 def openBracket(self):
107 self.brackets = self.brackets + 1
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()
117 def parseToken(self, token):
118 if token[:2] == '//':
119 return IgnoreState('\n', self)
121 return IgnoreState('*/', self)
126 elif token in [ '"', "'" ]:
127 return IgnoreState(token, self)
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:
136 self.stackBrackets.append(self.brackets)
137 return self.__getClassState()
138 elif token == 'package':
139 return self.__getPackageState()
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()
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
153 def setPackage(self, package):
154 self.package = package
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
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)
170 return IgnoreState('*/', self)
174 self.brace_level = self.brace_level + 1
176 if self.brace_level > 0:
178 # look further for anonymous inner class
179 return SkipState(1, AnonClassState(self))
180 elif token in [ '"', "'" ]:
181 return IgnoreState(token, self)
183 self.brace_level = self.brace_level - 1
186 self.outer_state.addAnonClass()
187 return self.old_state.parseToken(token)
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
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
210 self.outer_state.listClasses.append(token)
211 return self.outer_state
214 """A state that will ignore all tokens until it gets to a
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
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
234 def parse_java_file(fn):
235 return parse_java(open(fn, 'r').read())
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
242 initial = OuterState()
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)
250 package = string.replace(initial.package, '.', os.sep)
251 return (package, initial.listOutputs)
254 # Don't actually parse Java files for class names.
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...).
260 def parse_java_file(fn):
261 """ "Parse" a .java file.
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.
267 return os.path.split(file)