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, 2010, 2011, 2012 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 issue-2856:2676:d23b7a2f45e8 2012/08/05 15:38:28 garyo"
31
32import os
33import os.path
34import re
35
36java_parsing = 1
37
38default_java_version = '1.4'
39
40if java_parsing:
41    # Parse Java files for class names.
42    #
43    # This is a really cool parser from Charles Crain
44    # that finds appropriate class names in Java source.
45
46    # A regular expression that will find, in a java file:
47    #     newlines;
48    #     double-backslashes;
49    #     a single-line comment "//";
50    #     single or double quotes preceeded by a backslash;
51    #     single quotes, double quotes, open or close braces, semi-colons,
52    #         periods, open or close parentheses;
53    #     floating-point numbers;
54    #     any alphanumeric token (keyword, class name, specifier);
55    #     any alphanumeric token surrounded by angle brackets (generics);
56    #     the multi-line comment begin and end tokens /* and */;
57    #     array declarations "[]".
58    _reToken = re.compile(r'(\n|\\\\|//|\\[\'"]|[\'"\{\}\;\.\(\)]|' +
59                          r'\d*\.\d*|[A-Za-z_][\w\$\.]*|<[A-Za-z_]\w+>|' +
60                          r'/\*|\*/|\[\])')
61
62    class OuterState(object):
63        """The initial state for parsing a Java file for classes,
64        interfaces, and anonymous inner classes."""
65        def __init__(self, version=default_java_version):
66
67            if not version in ('1.1', '1.2', '1.3','1.4', '1.5', '1.6',
68                               '5', '6'):
69                msg = "Java version %s not supported" % version
70                raise NotImplementedError(msg)
71
72            self.version = version
73            self.listClasses = []
74            self.listOutputs = []
75            self.stackBrackets = []
76            self.brackets = 0
77            self.nextAnon = 1
78            self.localClasses = []
79            self.stackAnonClassBrackets = []
80            self.anonStacksStack = [[0]]
81            self.package = None
82
83        def trace(self):
84            pass
85
86        def __getClassState(self):
87            try:
88                return self.classState
89            except AttributeError:
90                ret = ClassState(self)
91                self.classState = ret
92                return ret
93
94        def __getPackageState(self):
95            try:
96                return self.packageState
97            except AttributeError:
98                ret = PackageState(self)
99                self.packageState = ret
100                return ret
101
102        def __getAnonClassState(self):
103            try:
104                return self.anonState
105            except AttributeError:
106                self.outer_state = self
107                ret = SkipState(1, AnonClassState(self))
108                self.anonState = ret
109                return ret
110
111        def __getSkipState(self):
112            try:
113                return self.skipState
114            except AttributeError:
115                ret = SkipState(1, self)
116                self.skipState = ret
117                return ret
118
119        def __getAnonStack(self):
120            return self.anonStacksStack[-1]
121
122        def openBracket(self):
123            self.brackets = self.brackets + 1
124
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('$'.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()
138
139        def parseToken(self, token):
140            if token[:2] == '//':
141                return IgnoreState('\n', self)
142            elif token == '/*':
143                return IgnoreState('*/', self)
144            elif token == '{':
145                self.openBracket()
146            elif token == '}':
147                self.closeBracket()
148            elif token in [ '"', "'" ]:
149                return IgnoreState(token, self)
150            elif token == "new":
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:
157                    self.nextAnon = 1
158                self.stackBrackets.append(self.brackets)
159                return self.__getClassState()
160            elif token == 'package':
161                return self.__getPackageState()
162            elif token == '.':
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()
167            return self
168
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', '5', '6'):
175                self.stackAnonClassBrackets.append(self.brackets)
176                className = []
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('$'.join(className))
182
183            self.nextAnon = self.nextAnon + 1
184            self.__getAnonStack().append(0)
185
186        def setPackage(self, package):
187            self.package = package
188
189    class AnonClassState(object):
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
195            self.brace_level = 0
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)
202            elif token == '/*':
203                return IgnoreState('*/', self)
204            elif token == '\n':
205                return self
206            elif token[0] == '<' and token[-1] == '>':
207                return self
208            elif token == '(':
209                self.brace_level = self.brace_level + 1
210                return self
211            if self.brace_level > 0:
212                if token == 'new':
213                    # look further for anonymous inner class
214                    return SkipState(1, AnonClassState(self))
215                elif token in [ '"', "'" ]:
216                    return IgnoreState(token, self)
217                elif token == ')':
218                    self.brace_level = self.brace_level - 1
219                return self
220            if token == '{':
221                self.outer_state.addAnonClass()
222            return self.old_state.parseToken(token)
223
224    class SkipState(object):
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
234            return self
235
236    class ClassState(object):
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
243            if token == '\n':
244                return self
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]
252                try:
253                    idx = locals[token]
254                    locals[token] = locals[token]+1
255                except KeyError:
256                    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
262
263    class IgnoreState(object):
264        """A state that will ignore all tokens until it gets to a
265        specified token."""
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
272            return self
273
274    class PackageState(object):
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
283
284    def parse_java_file(fn, version=default_java_version):
285        return parse_java(open(fn, 'r').read(), version)
286
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
290        produce"""
291        package = None
292        initial = OuterState(version)
293        currstate = initial
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)
299        if initial.package:
300            package = initial.package.replace('.', os.sep)
301        return (package, initial.listOutputs)
302
303else:
304    # Don't actually parse Java files for class names.
305    #
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...).
309
310    def parse_java_file(fn):
311        """ "Parse" a .java file.
312
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.
316        """
317        return os.path.split(file)
318
319# Local Variables:
320# tab-width:4
321# indent-tabs-mode:nil
322# End:
323# vim: set expandtab tabstop=4 shiftwidth=4:
324