def should_ignore_line(self, current_line, content): ''' Wil ignore the line if it is: - an import (using) - defines a namespace. In this case, it will update the current_namespace value. :see: `AbstractStaticTypeFileParser.should_ignore_line` ''' if has_any_keyword(current_line, ['using']): self.index += 1 return True elif has_any_keyword(current_line, ['namespace']): if not '{' in current_line: # if the "{" isn't in the next line, then # it will be in the following.. self.index += 1 self.index += 1 return True elif '}' in current_line and not has_any_keyword( current_line, CLASS_KEYWORDS): # it is closing the current namespace self.index += 1 return True else: return False
def is_method(self, current_line, current_class): ''' Overrides the is method to take into account that the line could be an operator definition, or the properties of an attribute. :parameters: content: list(str) the content of the file current_class: `pywebuml.models.Class` the current class that is being parsed. :returns: True if the current line is a method definition else False. ''' if has_any_keyword(current_line, ['operator']): # Take into account if the line is an operator definition. # public static bool operator !=(Foo lhs, Foo rhs) return True # check if it is a method defined in one line # or the properties (rare which has a method that uses ( # of the attribute. For example: # public OnDemand() { b = 3; } # public int attr { get { Debug.Log("Foo"); value; } } elif has_any_keyword(current_line, ['get', 'set']): return False else: return super(CSharpClassParser, self).is_method(current_line, current_class)
def test_has_keyword(self): ''' Some basic tests for has keyword. ''' self.assertTrue(has_any_keyword('this is KEYWORD', ['KEYWORD'])) self.assertFalse(has_any_keyword('this is keyword', ['KEYWORD'])) self.assertFalse(has_any_keyword('this is KEYWORDs', ['KEYWORD'])) self.assertFalse(has_any_keyword('this isKEYWORD', ['KEYWORD']))
def test_has_keyword_more_than_one(self): ''' Test what happens to has_keyword when using a list that has more than one element. ''' self.assertTrue(has_any_keyword('KEY this is WORD', ['KEY', 'WORD'])) self.assertTrue(has_any_keyword('KEY this is WORD', ['KEY', 'FOO'])) self.assertTrue(has_any_keyword('KEY this is WORD', ['FOO', 'WORD'])) self.assertFalse(has_any_keyword('KEY this is WORD', ['FOO', 'BAR']))
def _get_data(self, content): ''' Parse the file to identify the package for each class. ''' for line in content: if has_any_keyword(line, ['package']): package = remove_keywords(line, ['package']) package = package.replace(';', '') self.current_package = package elif has_any_keyword(line, ['import']): import_data = remove_keywords(line, ['import']) import_data = import_data.replace(';', '') if not import_data.endswith('*'): class_name = import_data.rsplit('.', 1)[1] self.classes_packages[class_name] = import_data
def parse_namespaces(self, content): ''' Creates a list for each index, which namespace should be used. For example, if the list is [(0, 'global'), (3, 'unity'), (10, 'global')] for the class definitions between 0 and 2 the namespace that should be used is 'global', for the ones between 3 and 9 is **unity** and finally for all the ones between 10 and the end of the file is **global**. :parameters: content: list(str) the cleaned content of the file. ''' if not has_any_keyword(content[0], ['namespace']): self.indexes_and_namespaces.append((0, 'global')) index = 0 while True: if index >= len(content): break line = content[index] if has_any_keyword(line, ['namespace']): namespace = remove_keywords(line, ['namespace']) opened_blocks = 0 if '{' in namespace: opened_blocks = 1 # TODO missing test for the strip, and this case namespace = namespace[: namespace.index('{')].strip() # go to the end of the namespace self.indexes_and_namespaces.append((index, namespace)) while True: index += 1 line = content[index] opened_blocks += line.count('{') - line.count('}') if opened_blocks == 0: break if (index +1) < len(content) and not has_any_keyword(content[index+1], ['namespace']): # if there are more lines in the file, and it doesn't # defines a new namespace, then it has to # restore the previous namespace self.indexes_and_namespaces.append((index+1, 'global')) else: # this is the case of a previous namespace or # the global namespace. index += 1
def should_ignore_line(self, current_line, content): ''' Ignore the lines that are the package or import classes. ''' if has_any_keyword(current_line, ['package', 'import']): self.index += 1 return True return False
def should_skip_language_methods(self, signature): ''' Skip all the methods that belong to the Java Object methods. ''' if '(' in signature: # remove the params of the signature just to be sure, an take # into account that the keywords doesn't have (. Because # of this it won't find: # public String toString() signature = signature[:signature.index('(')] return has_any_keyword(signature, JAVA_LANGUAGE_METHODS)
def get_class_definition_data(self, class_definition, content): ''' Returns the class_name, interfaces, and base classes. In this case, it will use the content to search for the base and interfaces classes packages. :see: `AbstractStaticTypedClassParser`.get_class_definition_data ''' class_name = [] base_classes = [] implemented_interfaces = [] # TODO check which keyword is first: the extends or implements # TODO check if in Java is required that base clasess should # go before implemented classes. if has_any_keyword(class_definition, ['implements']): # the +12 is because len(' implements ') == 12 implementations_data = class_definition[class_definition. index(' implements ') + 12:] # remove the implements part of the class_definitions class_definition = class_definition[:class_definition. index(' implements ')] for implemented_class in implementations_data.split(','): implemented_interfaces.append( self.get_class_package(implemented_class.strip(), content)) if has_any_keyword(class_definition, ['extends']): # the +9 is becuase len(' extends ') == 9 # TODO there could be a problem with this and template values.... base_data = class_definition[class_definition.index(' extends ') + 9:] class_definition = class_definition[:class_definition. index(' extends ')] for base_class in base_data.split(','): base_classes.append( self.get_class_package(base_class.strip(), content)) class_name = class_definition.strip() return class_definition, base_classes, implemented_interfaces
def has_preprocessor_directive(self, content): ''' Identifies if the content of the file has a preprocesor directive. :parameters: content: list(str) the lines of the file. :returns: True if the file has a preprocesor directive. ''' for line in content: if has_any_keyword(line, PREPROCESSOR_DIRECTIVES): return True return False
def get_class_definition_data(self, class_definition, content): ''' Returns the interfaces, base class and class name of the class_definition. All the packages of the interfaces and of the base class will always be **global**. :see: `AbstractStaticTypedClassParser`.get_class_definition_data ''' implemented_interfaces = [] base_classes = [] class_name = '' if ':' in class_definition: class_name = class_definition[:class_definition.index(':')] are_template_constrains = False # check if the class is a singlton, and # the template might have some constrains. if '<' in class_name: # in this case is a template that has some constrains # TODO falta testear... if has_any_keyword(class_name, ['where']): #TODO keyword bug.... class_name = class_name[:class_name.index('where')].strip() are_template_constrains = True if not are_template_constrains: class_extensions = class_definition[class_definition. index(':') + 1:].strip() for extension in class_extensions.split(','): extension = extension.strip() if extension.startswith('I'): # TODO falta checkear que la segunda palabra tambien sea # una mayuscula implemented_interfaces.append('global.%s' % extension) else: base_classes.append('global.%s' % extension) else: class_name = class_definition class_name = class_name.strip() return class_name, base_classes, implemented_interfaces
def _has_preprocessor_value(self, line, preprocessors_to_search): ''' Identifies if the line has any of the preprocesosr values, even if they have a space. :parameters: line: str the current line of the file preprocessors_to_search: list(str) the list of preprocessors directives to search in the line it will take into account that they can have an space. They must have the #. For example: # region public :returns: Ture if any preprocesor is found. ''' keywords = line.split(' ') if not '#' in line: return False elif has_any_keyword(line, preprocessors_to_search): return True else: keywords = line.split(' ') if len(keywords) == 1: # in this case the line is a preprocessor directive but # isnt the one I am looking for. return False if not '#' in keywords: # in this case the # is used as a value. For example: # public string foo = '#'; return False symbol_position = keywords.index('#') directives = map(lambda directive: directive.replace('#', ''), preprocessors_to_search) return keywords[symbol_position + 1] in directives
def parse(self, class_model, content, index, package_manager): ''' Parse the method of the class of the content. :parameters: class_model: ``pywebuml.model.Class`` the current class that is being parsed. content: list(str) the content of the file index: int from where start parsing package_manager: `pywebuml.parsers.static_typed.package_manager` the package manager to use to get the referenced class complete path. :return: a tuple (pywembuml.models.Method, int) that has the parsed method and the index value from where it should continue reading. :raises: - `pywebuml.initialize.parser.exceptions.ClossingArrayException` if it can't find the closing } of the array definition - `pywebuml.initialize.parser.exceptions.EmptyAttributeNameException` if it cant get the name of the attribute. ''' current_position = index current_line = content[index] visibility = get_visibility(current_line) is_static = has_any_keyword(current_line, ['static']) is_final = has_any_keyword(current_line, CONST_KEYWORDS) current_line = remove_keywords(current_line, MODIFIERS) # iterate the property methods of the attribute. if self.should_update_index(current_line, content, current_position, class_model): opened_keys = 0 opened_parenthesis = 0 found_equals = False # before starting checking unitl which position, the index should # move, it check from where it should start. if (current_line.endswith('=') or '=' not in current_line) \ and not '{' in current_line: # in this case, the line should be changed because the value # is in the current line. For example: # private i = # 3; current_position += 1 elif '{' not in current_line and content[current_position + 1].startswith('{'): # This is the case of an array definition. For example # private int[] k = new int[] # { 1, 2, 3 } current_position += 1 while True: if current_position == len(content): # TODO change the value of this because this isn't only for arrays... raise ClossingArrayException(index) line = content[current_position] opened_keys = opened_keys + line.count('{') - line.count('}') opened_parenthesis = opened_parenthesis + line.count( '(') - line.count(')') current_position += 1 if opened_keys == 0 and opened_parenthesis == 0 and ( line.endswith(';') or line.endswith('}')): # this is for cases like: # private String text = "This is a long text" + # "this continues here" + # "and finally ends here"; # the endswith } is for taking into account C# properties. # TODO remove this from here it take it into c# parser. break current_line = self.clean_line(current_line) if self.should_skip_language_attributes(current_line): if current_position == index: current_position += 1 return (current_position, None) # can't use current_line.split(' '), becuase there can be more than # one space between the type and variable definition variable_type, name = get_type_and_name(current_line, index, class_model) referenced_class_package = self.get_referenced_type( variable_type, package_manager) if index == current_position: # in this case the variable is of 1 line... # private int j; current_position += 1 LOGGER.debug("Found attribute: %s", name) return (current_position, Attribute(name, variable_type, class_model, visibility, is_static, is_final, referenced_class_package))
def parse_content(self, filepath, content): ''' It will parse the content of the file. If the file has a compiler directive, then it will be ignore it by returning an empty list, because it is very difficult to take into account the changes that affect the compiler. :parameters: filepath: str the path and name of the file. The path will be realive to where the command was executed. content: list(str) the content of the file already modified by the base parser. :returns: a list of pywebuml.models.Class with it's methods and attributes. ''' res = [] if not content: # the file is empty return [] content = remove_comments(content) if not content: # the whole file was a comment. return [] content = self.clean_content(content) current_line = None self.class_parser = self.get_class_parser(content) while True: current_line = content[self.index] if self.should_ignore_line(current_line, content): pass elif has_any_keyword(current_line, CLASS_KEYWORDS): # To take into account that some methods or enums, could # be defined before the class. self.index = self.parse_class_definition(filepath, content, self.index, res) else: # iterate the method or enum that is outside the class opened = current_line.count('{') - current_line.count('}') current_position = self.index opened_keys = 0 while True: line = content[current_position] opened_keys = opened_keys + line.count('{') - line.count('}') current_position += 1 if opened_keys == 0: break self.index = current_position if self.index >= len(content): break return res
def is_other_type(self, current_line): ''' Returns true if the class is partial. ''' return has_any_keyword(current_line, ['partial'])
def parse(self, class_model, content, index): ''' Parse the method of the class of the content. :parameters: class_model: ``pywebuml.model.Class`` the current class that is being parsed. content: list(str) the content of the file index: int from where start parsing :return: a tuple (pywembuml.models.Method, int) that has the parsed method and the index value from where it should continue reading. :raises: - MissingClosingParenthesis: if it cant find the closing ) of the method signature ''' current_position = index current_line = content[index] visibility = get_visibility(current_line) is_static = has_any_keyword(current_line, ['static']) is_abstract = has_any_keyword(current_line, ABSTRACT_KEYWORDS) \ or isinstance(class_model, Interface) current_line = remove_keywords(current_line, [visibility]) current_line = remove_keywords(current_line, MODIFIERS) signature = current_line.strip() if not ')' in current_line: # in this case the signature of the method is in only one # line. For example: # public void Foo(int a, int b, int c, # int d) current_position += 1 while True: if current_position >= len(content): raise MissingClosingParenthesis(index) signature += content[current_position].strip() if ')' in content[current_position]: break current_position += 1 if not is_abstract: # now move the index to the end of the method code # the abstract method only have thier definitions, so this # isn't necessary # take into account that the definition of the method can # only be of one line. if '{' in content[current_position] and \ (content[current_position].count('{') == content[current_position].count('}')): current_position += 1 else: opened_keys = 0 if '{' in content[current_position]: opened_keys = 1 started_method = False current_position += 1 while True: if current_position == len(content): raise MissingClosingMethod(index) line = content[current_position] started_method = started_method or opened_keys > 0 opened_keys = opened_keys + line.count('{') - line.count( '}') current_position += 1 # take into account that the next line of the method definition # could be another thing that isn't the openin. For example: # public Foo() # : base(1) # { # // do something # } if opened_keys == 0 and started_method: break else: current_position += 1 # remove all the code of the signature if '{' in signature: signature = signature[:signature.index('{')].strip() signature = self.clean_language_things(signature) if self.should_skip_language_methods(signature): return (current_position, None) # remove the parameters of the signature tmp = signature[:signature.index('(')].strip() if len(tmp.split(' ')) == 1: # the the method is a constructor, and the name # is the value name = tmp return_type = 'void' else: return_type, name = get_type_and_name(tmp, index, class_model) LOGGER.debug("Found method: %s", name) return ( current_position, Method(name, class_model, signature, visibility, is_static, is_abstract), )
def should_skip_language_methods(self, signature): ''' Skip the method if it is an operator. ''' return has_any_keyword(signature, ['operator'])
def parse(self, filepath, content, index, owner=None): ''' Parse the content of the class. The index must be setted in the first line of the class definition. The result will be a list with more than one element if there is an inner class. If there is more than one class definition in the file, then this method should be used more than once. :parameters: filepath: str the folder and name of the file. For example: ./examples/mycode.cs content: list(str) the content of the file. owner: pywebuml.model.Class the owner of this class. This will only be used if the class as inner. :return: a tuple with the index and the list of class representation. I ''' res = [] started_definition = index if not owner: base_class_package = self.package_manager.get_class_package(index) else: base_class_package = owner.package index, current_class = self.parse_class_signature( filepath, content, index, base_class_package, owner) res.append(current_class) position = index - 1 current_line = content[position] if '{' in current_line and '}' in current_line and \ has_any_keyword(current_line, CLASS_KEYWORDS): # then the class definition is of 1 line # For example: # public class Foo { } return (index, res) while True: if index == len(content): raise MissingClosingClassDefinition(started_definition) current_line = content[index] if current_line in ('}', '};'): # the closing key of the class definition # must comprare to equals because the current line could be a # one line method, attr or inner class definition index += 1 break if has_any_keyword(current_line, CLASS_KEYWORDS): index, inner_res = self.parse(filepath, content, index, current_class) res.extend(inner_res) elif self.is_method(current_line, current_class): index = self.parse_method(content, index, current_class) else: index = self.parse_attribute(content, index, current_class) return (index, res)
def parse_class_signature(self, filepath, content, index, base_class_package, owner=None): ''' Returns the `pywebuml.models.Class` object without any attribute or method, but with its implementations and extensions. :parameters: content: list(str) the content of the file index: int from which position of the content start getting the info. base_class_package: str the base class package that will be used with the class name to get the complete package of the class. owner: `pywebuml.models.Class` if the class is an inner class, then this is the parent class. :returns: a ``pywebuml.models.Class`` taking into account the content of the file. ''' current_line = content[index] self.is_interface = has_any_keyword(current_line, ['interface']) self.is_abstract = has_any_keyword(current_line, ['abstract']) self.is_enum = has_any_keyword(current_line, ['enum']) is_other_type = self.is_other_type(current_line) class_data = remove_keywords(current_line, CLASS_KEYWORDS) class_data = remove_keywords(class_data, MODIFIERS) visibility = get_visibility(class_data) class_data = remove_keywords(class_data, [visibility]) # if the class definition doesn't end in that line then # it might have some other base_classes in the following lines # for example: # public class Foo : A, B, C # D, E, F # G { finish = False current_line = class_data class_definition = '' current_position = index while True: current_position += 1 class_definition += current_line # found the end of the class definition if '{' in current_line: break if current_position == len(content): raise MissingOpeningClassDefinition() current_line = content[current_position].strip() # removes that it at the right of the { becuase # that belongs to the class implementation class_definition = class_definition[:class_definition.index('{')] class_name, base_classes, implemented_interfaces = \ self.get_class_definition_data(class_definition, content) index = current_position LOGGER.debug("Found class: %s", class_name) package = '%s.%s' % (base_class_package, class_name) # TODO ver como manejar el tema de partial if is_other_type: klass = self.get_other_type_class_model(package, class_name, filepath, self.language, owner) else: klass = self.get_class_model(package, class_name, filepath, self.language, owner) for base_package in base_classes: ClassInheritence(klass, base_package) for interface_package in implemented_interfaces: ClassImplements(klass, interface_package) return (index, klass)