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