Update to the right Java parser. (Charles Crain)
[scons.git] / src / engine / SCons / Tool / javac.py
1 """SCons.Tool.javac
2
3 Tool-specific initialization for javac.
4
5 There normally shouldn't be any need to import this module directly.
6 It will usually be imported through the generic SCons.Tool.Tool()
7 selection method.
8
9 """
10
11 #
12 # __COPYRIGHT__
13 #
14 # Permission is hereby granted, free of charge, to any person obtaining
15 # a copy of this software and associated documentation files (the
16 # "Software"), to deal in the Software without restriction, including
17 # without limitation the rights to use, copy, modify, merge, publish,
18 # distribute, sublicense, and/or sell copies of the Software, and to
19 # permit persons to whom the Software is furnished to do so, subject to
20 # the following conditions:
21 #
22 # The above copyright notice and this permission notice shall be included
23 # in all copies or substantial portions of the Software.
24 #
25 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
26 # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
27 # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
28 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
29 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
30 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
31 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
32 #
33
34 __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
35
36 import os.path
37 import re
38 import string
39
40 import SCons.Builder
41
42 java_parsing = 1
43
44 if java_parsing:
45     # Parse Java files for class names.
46     #
47     # This is a really cool parser from Charles Crain
48     # that finds appropriate class names in Java source.
49
50     # A regular expression that will find, in a java file,
51     # any alphanumeric token (keyword, class name, specifier); open or
52     # close brackets; a single-line comment "//"; the multi-line comment
53     # begin and end tokens /* and */; single or double quotes; and
54     # single or double quotes preceeded by a backslash.
55     _reToken = re.compile(r'(//[^\r\n]*|\\[\'"]|[\'"\{\}]|[A-Za-z_][\w\.]*|' +
56                           r'/\*|\*/)')
57
58     class OuterState:
59         """The initial state for parsing a Java file for classes,
60         interfaces, and anonymous inner classes."""
61         def __init__(self):
62             self.listClasses = []
63             self.listOutputs = []
64             self.stackBrackets = []
65             self.brackets = 0
66             self.nextAnon = 1
67             self.package = None
68
69         def __getClassState(self):
70             try:
71                 return self.classState
72             except AttributeError:
73                 ret = ClassState(self)
74                 self.classState = ret
75                 return ret
76
77         def __getPackageState(self):
78             try:
79                 return self.packageState
80             except AttributeError:
81                 ret = PackageState(self)
82                 self.packageState = ret
83                 return ret
84
85         def __getAnonClassState(self):
86             try:
87                 return self.anonState
88             except AttributeError:
89                 ret = SkipState(1, AnonClassState(self))
90                 self.anonState = ret
91                 return ret
92
93         def __getSkipState(self):
94             try:
95                 return self.skipState
96             except AttributeError:
97                 ret = SkipState(1, self)
98                 self.skipState = ret
99                 return ret
100
101         def parseToken(self, token):
102             if token[:2] == '//':
103                 pass # ignore comment
104             elif token == '/*':
105                 return IgnoreState('*/', self)
106             elif token == '{':
107                 self.brackets = self.brackets + 1
108             elif token == '}':
109                 self.brackets = self.brackets - 1
110                 if len(self.stackBrackets) and \
111                    self.brackets == self.stackBrackets[-1]:
112                     self.listOutputs.append(string.join(self.listClasses, '$'))
113                     self.listClasses.pop()
114                     self.stackBrackets.pop()
115             elif token == '"' or token == "'":
116                 return IgnoreState(token, self)
117             elif token == "new":
118                 # anonymous inner class
119                 if len(self.listClasses) > 0:
120                     return self.__getAnonClassState()
121                 return self.__getSkipState() # Skip the class name
122             elif token == 'class' or token == 'interface':
123                 if len(self.listClasses) == 0:
124                     self.nextAnon = 1
125                 self.stackBrackets.append(self.brackets)
126                 return self.__getClassState()
127             elif token == 'package':
128                 return self.__getPackageState()
129             return self
130
131         def addAnonClass(self):
132             """Add an anonymous inner class"""
133             clazz = self.listClasses[0]
134             self.listOutputs.append('%s$%d' % (clazz, self.nextAnon))
135             self.brackets = self.brackets + 1
136             self.nextAnon = self.nextAnon + 1
137
138         def setPackage(self, package):
139             self.package = package
140
141     class AnonClassState:
142         """A state that looks for anonymous inner classes."""
143         def __init__(self, outer_state):
144             # outer_state is always an instance of OuterState
145             self.outer_state = outer_state
146             self.tokens_to_find = 2
147         def parseToken(self, token):
148             # This is an anonymous class if and only if the next token is a bracket
149             if token == '{':
150                 self.outer_state.addAnonClass()
151             return self.outer_state
152
153     class SkipState:
154         """A state that will skip a specified number of tokens before
155         reverting to the previous state."""
156         def __init__(self, tokens_to_skip, old_state):
157             self.tokens_to_skip = tokens_to_skip
158             self.old_state = old_state
159         def parseToken(self, token):
160             self.tokens_to_skip = self.tokens_to_skip - 1
161             if self.tokens_to_skip < 1:
162                 return self.old_state
163             return self
164
165     class ClassState:
166         """A state we go into when we hit a class or interface keyword."""
167         def __init__(self, outer_state):
168             # outer_state is always an instance of OuterState
169             self.outer_state = outer_state
170         def parseToken(self, token):
171             # the only token we get should be the name of the class.
172             self.outer_state.listClasses.append(token)
173             return self.outer_state
174
175     class IgnoreState:
176         """A state that will ignore all tokens until it gets to a
177         specified token."""
178         def __init__(self, ignore_until, old_state):
179             self.ignore_until = ignore_until
180             self.old_state = old_state
181         def parseToken(self, token):
182             if self.ignore_until == token:
183                 return self.old_state
184             return self
185
186     class PackageState:
187         """The state we enter when we encounter the package keyword.
188         We assume the next token will be the package name."""
189         def __init__(self, outer_state):
190             # outer_state is always an instance of OuterState
191             self.outer_state = outer_state
192         def parseToken(self, token):
193             self.outer_state.setPackage(token)
194             return self.outer_state
195
196     def parse_java(fn):
197         """Parse a .java file and return a double of package directory,
198         plus a list of .class files that compiling that .java file will
199         produce"""
200         package = None
201         initial = OuterState()
202         currstate = initial
203         for token in _reToken.findall(open(fn, 'r').read()):
204             # The regex produces a bunch of groups, but only one will
205             # have anything in it.
206             currstate = currstate.parseToken(token)
207         if initial.package:
208             package = string.replace(initial.package, '.', os.sep)
209         return (package, initial.listOutputs)
210
211 else:
212     # Don't actually parse Java files for class names.
213     #
214     # We might make this a configurable option in the future if
215     # Java-file parsing takes too long (although it shouldn't relative
216     # to how long the Java compiler itself seems to take...).
217
218     def parse_java(file):
219         """ "Parse" a .java file.
220
221         This actually just splits the file name, so the assumption here
222         is that the file name matches the public class name, and that
223         the path to the file is the same as the package name.
224         """
225         return os.path.split(file)
226
227 def generate(env):
228     """Add Builders and construction variables for javac to an Environment."""
229
230     def emit_java_files(target, source, env):
231         """Create and return lists of source java files
232         and their corresponding target class files.
233         """
234         env['_JAVACLASSDIR'] = target[0]
235         env['_JAVASRCDIR'] = source[0]
236         java_suffix = env.get('JAVASUFFIX', '.java')
237         class_suffix = env.get('JAVACLASSSUFFIX', '.class')
238
239         slist = []
240         def visit(arg, dirname, names, js=java_suffix):
241             java_files = filter(lambda n, js=js: n[-len(js):] == js, names)
242             java_paths = map(lambda f, d=dirname:
243                                     os.path.join(d, f),
244                              java_files)
245             arg.extend(java_paths)
246         os.path.walk(source[0], visit, slist)
247
248         tlist = []
249         for file in slist:
250             pkg_dir, classes = parse_java(file)
251             if pkg_dir:
252                 for c in classes:
253                     tlist.append(os.path.join(target[0],
254                                               pkg_dir,
255                                               c + class_suffix))
256             elif classes:
257                 for c in classes:
258                     tlist.append(os.path.join(target[0], c + class_suffix))
259             else:
260                 # This is an odd end case:  no package and no classes.
261                 # Just do our best based on the source file name.
262                 tlist.append(os.path.join(target[0],
263                                           file[:-len(java_suffix)] + class_suffix))
264
265         return tlist, slist
266
267     JavaBuilder = SCons.Builder.Builder(action = '$JAVACCOM',
268                         emitter = emit_java_files,
269                         target_factory = SCons.Node.FS.default_fs.File,
270                         source_factory = SCons.Node.FS.default_fs.File)
271
272     env['BUILDERS']['Java'] = JavaBuilder
273
274     env['JAVAC']            = 'javac'
275     env['JAVACFLAGS']       = ''
276     env['JAVACCOM']         = '$JAVAC $JAVACFLAGS -d $_JAVACLASSDIR -sourcepath $_JAVASRCDIR $SOURCES'
277     env['JAVACLASSSUFFIX']  = '.class'
278     env['JAVASUFFIX']       = '.java'
279
280 def exists(env):
281     return env.Detect('javac')