Really parse .java files for inner class names. (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 simple and cool parser from Charles Crain
48     # that finds appropriate class names in Java source.
49
50     _reToken = re.compile(r'[^\\]([\'"])|([\{\}])|' +
51                           r'(?:^|[\{\}\s;])((?:class|interface)'+
52                           r'\s+[A-Za-z_]\w*)|' +
53                           r'(new\s+[A-Za-z_]\w*\s*\([^\)]*\)\s*\{)|' +
54                           r'(//[^\r\n]*)|(/\*|\*/)')
55
56     class OuterState:
57         def __init__(self):
58             self.listClasses = []
59             self.listOutputs = []
60             self.stackBrackets = []
61             self.brackets = 0
62             self.nextAnon = 1
63
64         def parseToken(self, token):
65             #print token
66             if token[:2] == '//':
67                 pass # ignore comment
68             elif token == '/*':
69                 return IgnoreState('*/', self)
70             elif token == '{':
71                 self.brackets = self.brackets + 1
72             elif token == '}':
73                 self.brackets = self.brackets - 1
74                 if len(self.stackBrackets) and \
75                    self.brackets == self.stackBrackets[-1]:
76                     self.listOutputs.append(string.join(self.listClasses, '$'))
77                     self.listClasses.pop()
78                     self.stackBrackets.pop()
79             elif token == '"':
80                 return IgnoreState('"', self)
81             elif token == "'":
82                 return IgnoreState("'", self)
83             elif token[:3] == "new":
84                 # anonymous inner class
85                 if len(self.listClasses) > 0:
86                     clazz = self.listClasses[0]
87                     self.listOutputs.append('%s$%d' % (clazz, self.nextAnon))
88                     self.brackets = self.brackets + 1
89                     self.nextAnon = self.nextAnon + 1
90             elif token[:5] == 'class':
91                 if len(self.listClasses) == 0:
92                     self.nextAnon = 1
93                 self.listClasses.append(string.join(string.split(token[6:])))
94                 self.stackBrackets.append(self.brackets)
95             elif token[:9] == 'interface':
96                 if len(self.listClasses) == 0:
97                     self.nextAnon = 1
98                 self.listClasses.append(string.join(string.split(token[10:])))
99                 self.stackBrackets.append(self.brackets)
100             return self
101
102     class IgnoreState:
103         def __init__(self, ignore_until, old_state):
104             self.ignore_until = ignore_until
105             self.old_state = old_state
106         def parseToken(self, token):
107             if token == self.ignore_until:
108                 return self.old_state
109             return self
110
111     def parse_java(file):
112         contents = open(file, 'r').read()
113
114         # Is there a more efficient way to do this than to split
115         # the contents like this?
116         pkg_dir = None
117         for line in string.split(contents, "\n"):
118             if line[:7] == 'package':
119                 pkg = string.split(line)[1]
120                 if pkg[-1] == ';':
121                     pkg = pkg[:-1]
122                 pkg_dir = apply(os.path.join, string.split(pkg, '.'))
123                 break
124
125         initial = OuterState()
126         currstate = initial
127         for matches in _reToken.findall(contents):
128             # The regex produces a bunch of groups, but only one will
129             # have anything in it.
130             token = filter(lambda x: x, matches)[0]
131             currstate = currstate.parseToken(token)
132
133         return pkg_dir, initial.listOutputs
134
135 else:
136     # Don't actually parse Java files for class names.
137     #
138     # We might make this a configurable option in the future if
139     # Java-file parsing takes too long (although it shouldn't relative
140     # to how long the Java compiler itself seems to take...).
141
142     def parse_java(file):
143         """ "Parse" a .java file.
144
145         This actually just splits the file name, so the assumption here
146         is that the file name matches the public class name, and that
147         the path to the file is the same as the package name.
148         """
149         return os.path.split(file)
150
151 def generate(env):
152     """Add Builders and construction variables for javac to an Environment."""
153
154     def emit_java_files(target, source, env):
155         """Create and return lists of source java files
156         and their corresponding target class files.
157         """
158         env['_JAVACLASSDIR'] = target[0]
159         env['_JAVASRCDIR'] = source[0]
160         java_suffix = env.get('JAVASUFFIX', '.java')
161         class_suffix = env.get('JAVACLASSSUFFIX', '.class')
162
163         slist = []
164         def visit(arg, dirname, names, js=java_suffix):
165             java_files = filter(lambda n, js=js: n[-len(js):] == js, names)
166             java_paths = map(lambda f, d=dirname:
167                                     os.path.join(d, f),
168                              java_files)
169             arg.extend(java_paths)
170         os.path.walk(source[0], visit, slist)
171
172         tlist = []
173         for file in slist:
174             pkg_dir, classes = parse_java(file)
175             if pkg_dir:
176                 for c in classes:
177                     tlist.append(os.path.join(target[0],
178                                               pkg_dir,
179                                               c + class_suffix))
180             elif classes:
181                 for c in classes:
182                     tlist.append(os.path.join(target[0], c + class_suffix))
183             else:
184                 # This is an odd end case:  no package and no classes.
185                 # Just do our best based on the source file name.
186                 tlist.append(os.path.join(target[0],
187                                           file[:-len(java_suffix)] + class_suffix))
188
189         return tlist, slist
190
191     JavaBuilder = SCons.Builder.Builder(action = '$JAVACCOM',
192                         emitter = emit_java_files,
193                         target_factory = SCons.Node.FS.default_fs.File,
194                         source_factory = SCons.Node.FS.default_fs.File)
195
196     env['BUILDERS']['Java'] = JavaBuilder
197
198     env['JAVAC']            = 'javac'
199     env['JAVACFLAGS']       = ''
200     env['JAVACCOM']         = '$JAVAC $JAVACFLAGS -d $_JAVACLASSDIR -sourcepath $_JAVASRCDIR $SOURCES'
201     env['JAVACLASSSUFFIX']  = '.class'
202     env['JAVASUFFIX']       = '.java'
203
204 def exists(env):
205     return env.Detect('javac')