1"""SCons.Tool.JavaCommon
2
3Stuff for processing Java.
4
5"""
6
7#
8# Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009 The SCons Foundation
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__ = "src/engine/SCons/Tool/JavaCommon.py 4369 2009/09/19 15:58:29 scons"
31
32import os
33import os.path
34import re
35import string
36
37java_parsing = 1
38
39default_java_version = '1.4'
40
41if java_parsing:
42    # Parse Java files for class names.
43    #
44    # This is a really cool parser from Charles Crain
45    # that finds appropriate class names in Java source.
46
47    # A regular expression that will find, in a java file:
48    #     newlines;
49    #     double-backslashes;
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+>|' +
61                          r'/\*|\*/|\[\])')
62
63    class OuterState:
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):
67
68            if not version in ('1.1', '1.2', '1.3','1.4', '1.5', '1.6',
69                               '5', '6'):
70                msg = "Java version %s not supported" % version
71                raise NotImplementedError(msg)
72
73            self.version = version
74            self.listClasses = []
75            self.listOutputs = []
76            self.stackBrackets = []
77            self.brackets = 0
78            self.nextAnon = 1
79            self.localClasses = []
80            self.stackAnonClassBrackets = []
81            self.anonStacksStack = [[0]]
82            self.package = None
83
84        def trace(self):
85            pass
86
87        def __getClassState(self):
88            try:
89                return self.classState
90            except AttributeError:
91                ret = ClassState(self)
92                self.classState = ret
93                return ret
94
95        def __getPackageState(self):
96            try:
97                return self.packageState
98            except AttributeError:
99                ret = PackageState(self)
100                self.packageState = ret
101                return ret
102
103        def __getAnonClassState(self):
104            try:
105                return self.anonState
106            except AttributeError:
107                self.outer_state = self
108                ret = SkipState(1, AnonClassState(self))
109                self.anonState = ret
110                return ret
111
112        def __getSkipState(self):
113            try:
114                return self.skipState
115            except AttributeError:
116                ret = SkipState(1, self)
117                self.skipState = ret
118                return ret
119
120        def __getAnonStack(self):
121            return self.anonStacksStack[-1]
122
123        def openBracket(self):
124            self.brackets = self.brackets + 1
125
126        def closeBracket(self):
127            self.brackets = self.brackets - 1
128            if len(self.stackBrackets) and \
129               self.brackets == self.stackBrackets[-1]:
130                self.listOutputs.append(string.join(self.listClasses, '$'))
131                self.localClasses.pop()
132                self.listClasses.pop()
133                self.anonStacksStack.pop()
134                self.stackBrackets.pop()
135            if len(self.stackAnonClassBrackets) and \
136               self.brackets == self.stackAnonClassBrackets[-1]:
137                self.__getAnonStack().pop()
138                self.stackAnonClassBrackets.pop()
139
140        def parseToken(self, token):
141            if token[:2] == '//':
142                return IgnoreState('\n', self)
143            elif token == '/*':
144                return IgnoreState('*/', self)
145            elif token == '{':
146                self.openBracket()
147            elif token == '}':
148                self.closeBracket()
149            elif token in [ '"', "'" ]:
150                return IgnoreState(token, self)
151            elif token == "new":
152                # anonymous inner class
153                if len(self.listClasses) > 0:
154                    return self.__getAnonClassState()
155                return self.__getSkipState() # Skip the class name
156            elif token in ['class', 'interface', 'enum']:
157                if len(self.listClasses) == 0:
158                    self.nextAnon = 1
159                self.stackBrackets.append(self.brackets)
160                return self.__getClassState()
161            elif token == 'package':
162                return self.__getPackageState()
163            elif token == '.':
164                # Skip the attribute, it might be named "class", in which
165                # case we don't want to treat the following token as
166                # an inner class name...
167                return self.__getSkipState()
168            return self
169
170        def addAnonClass(self):
171            """Add an anonymous inner class"""
172            if self.version in ('1.1', '1.2', '1.3', '1.4'):
173                clazz = self.listClasses[0]
174                self.listOutputs.append('%s$%d' % (clazz, self.nextAnon))
175            elif self.version in ('1.5', '1.6', '5', '6'):
176                self.stackAnonClassBrackets.append(self.brackets)
177                className = []
178                className.extend(self.listClasses)
179                self.__getAnonStack()[-1] = self.__getAnonStack()[-1] + 1
180                for anon in self.__getAnonStack():
181                    className.append(str(anon))
182                self.listOutputs.append(string.join(className, '$'))
183
184            self.nextAnon = self.nextAnon + 1
185            self.__getAnonStack().append(0)
186
187        def setPackage(self, package):
188            self.package = package
189
190    class AnonClassState:
191        """A state that looks for anonymous inner classes."""
192        def __init__(self, old_state):
193            # outer_state is always an instance of OuterState
194            self.outer_state = old_state.outer_state
195            self.old_state = old_state
196            self.brace_level = 0
197        def parseToken(self, token):
198            # This is an anonymous class if and only if the next
199            # non-whitespace token is a bracket. Everything between
200            # braces should be parsed as normal java code.
201            if token[:2] == '//':
202                return IgnoreState('\n', self)
203            elif token == '/*':
204                return IgnoreState('*/', self)
205            elif token == '\n':
206                return self
207            elif token[0] == '<' and token[-1] == '>':
208                return self
209            elif token == '(':
210                self.brace_level = self.brace_level + 1
211                return self
212            if self.brace_level > 0:
213                if token == 'new':
214                    # look further for anonymous inner class
215                    return SkipState(1, AnonClassState(self))
216                elif token in [ '"', "'" ]:
217                    return IgnoreState(token, self)
218                elif token == ')':
219                    self.brace_level = self.brace_level - 1
220                return self
221            if token == '{':
222                self.outer_state.addAnonClass()
223            return self.old_state.parseToken(token)
224
225    class SkipState:
226        """A state that will skip a specified number of tokens before
227        reverting to the previous state."""
228        def __init__(self, tokens_to_skip, old_state):
229            self.tokens_to_skip = tokens_to_skip
230            self.old_state = old_state
231        def parseToken(self, token):
232            self.tokens_to_skip = self.tokens_to_skip - 1
233            if self.tokens_to_skip < 1:
234                return self.old_state
235            return self
236
237    class ClassState:
238        """A state we go into when we hit a class or interface keyword."""
239        def __init__(self, outer_state):
240            # outer_state is always an instance of OuterState
241            self.outer_state = outer_state
242        def parseToken(self, token):
243            # the next non-whitespace token should be the name of the class
244            if token == '\n':
245                return self
246            # If that's an inner class which is declared in a method, it
247            # requires an index prepended to the class-name, e.g.
248            # 'Foo$1Inner' (Tigris Issue 2087)
249            if self.outer_state.localClasses and \
250                self.outer_state.stackBrackets[-1] > \
251                self.outer_state.stackBrackets[-2]+1:
252                locals = self.outer_state.localClasses[-1]
253                try:
254                    idx = locals[token]
255                    locals[token] = locals[token]+1
256                except KeyError:
257                    locals[token] = 1
258                token = str(locals[token]) + token
259            self.outer_state.localClasses.append({})
260            self.outer_state.listClasses.append(token)
261            self.outer_state.anonStacksStack.append([0])
262            return self.outer_state
263
264    class IgnoreState:
265        """A state that will ignore all tokens until it gets to a
266        specified token."""
267        def __init__(self, ignore_until, old_state):
268            self.ignore_until = ignore_until
269            self.old_state = old_state
270        def parseToken(self, token):
271            if self.ignore_until == token:
272                return self.old_state
273            return self
274
275    class PackageState:
276        """The state we enter when we encounter the package keyword.
277        We assume the next token will be the package name."""
278        def __init__(self, outer_state):
279            # outer_state is always an instance of OuterState
280            self.outer_state = outer_state
281        def parseToken(self, token):
282            self.outer_state.setPackage(token)
283            return self.outer_state
284
285    def parse_java_file(fn, version=default_java_version):
286        return parse_java(open(fn, 'r').read(), version)
287
288    def parse_java(contents, version=default_java_version, trace=None):
289        """Parse a .java file and return a double of package directory,
290        plus a list of .class files that compiling that .java file will
291        produce"""
292        package = None
293        initial = OuterState(version)
294        currstate = initial
295        for token in _reToken.findall(contents):
296            # The regex produces a bunch of groups, but only one will
297            # have anything in it.
298            currstate = currstate.parseToken(token)
299            if trace: trace(token, currstate)
300        if initial.package:
301            package = string.replace(initial.package, '.', os.sep)
302        return (package, initial.listOutputs)
303
304else:
305    # Don't actually parse Java files for class names.
306    #
307    # We might make this a configurable option in the future if
308    # Java-file parsing takes too long (although it shouldn't relative
309    # to how long the Java compiler itself seems to take...).
310
311    def parse_java_file(fn):
312        """ "Parse" a .java file.
313
314        This actually just splits the file name, so the assumption here
315        is that the file name matches the public class name, and that
316        the path to the file is the same as the package name.
317        """
318        return os.path.split(file)
319
320# Local Variables:
321# tab-width:4
322# indent-tabs-mode:nil
323# End:
324# vim: set expandtab tabstop=4 shiftwidth=4:
325