''' @author: olivier.massot ''' import re from _regex_core import MULTILINE from path import Path # TODO: gérer les cas où des objets de types différents portent le même nom. Ex: une table et un formulaire... Note: une requete et une table ne peuvent pas porter le même nom. # TODO: ignorer les commentaires dans les modules # TODO: ignorer les labels dans les formulaires et états # TODO: Gérer les références circulaires # TODO: Stocker un aperçu du contexte de la ou des références dans le code source, pour contrôle ultérieur # TODO: Permettre de supprimer / ajouter des réferences # TODO: Verifier que la recherche puisse être Case sensitive? def recurse(acc_obj): deptree = [] for dep in acc_obj.deps: deptree.append(dep) if dep.deps: deptree += recurse(dep) return deptree class InvalidFileExt(IOError): pass class Mention(): def __init__(self, line, objname, quote, obj): self.line = line self.objname = objname self.quote = quote self.obj = obj class AccessObject(): type_ = "" _valid_file_exts = (".bas") def __init__(self, name_): self.name_ = name_ self.functions = [] self.sourcefile = "" self._sourcecode = "" self.mentions = [] self.deps = [] self.refs = [] def __repr__(self): return "<{}: {}>".format(self.type_, self.name_) @classmethod def from_file(cls, file): file = Path(file) if file.ext not in cls._valid_file_exts: raise InvalidFileExt("Format de fichier d'entrée non valide ({})".format(file.name)) obj = cls(AccessObject.path_to_name(file)) obj.sourcefile = file obj._sourcecode = file.text() obj._sourcecode.replace("\r\n", "\n") return obj @property def sourcecode(self): if not self._sourcecode: self._sourcecode = self.sourcefile.text() return self._sourcecode SUBSTR = {92: "\\", 47: "/", 58: ":", 42: "*", 63:"?", 34:"\"", 60:"<", 62:">", 124:"|" } @staticmethod def path_to_name(path): name_ = path.name.stripext() for ascii_code, char in AccessObject.SUBSTR.items(): name_ = name_.replace("[{}]".format(ascii_code), char) return name_ class TableObject(AccessObject): type_ = "Table" _valid_file_exts = (".xml", ".lnkd") class QueryObject(AccessObject): type_ = "Query" class FormObject(AccessObject): type_ = "Form" class ReportObject(AccessObject): type_ = "Report" class MacroObject(AccessObject): type_ = "Macro" class ModuleObject(AccessObject): type_ = "Module" @classmethod def from_file(cls, file): obj = super(ModuleObject, cls).from_file(file) rx = re.compile(r"Sub|Function ([^(]+)\(") obj.functions = [fname for fname in rx.findall(file.text()) if fname] return obj class RelationObject(AccessObject): type_ = "Relation" _valid_file_exts = (".txt") class Analyse(): objects = [] index = {} @classmethod def report(cls, current, total, msg=""): pass @classmethod def ended(cls): pass @classmethod def load_objects(cls, source_dir): source_dir = Path(source_dir) cls.objects = [] cls.duplicated_names = [] sourcemap = { "tables": TableObject, "relations": RelationObject, "queries": QueryObject, "forms": FormObject, "reports": ReportObject, "scripts": MacroObject, "modules": ModuleObject, } for dirname, accobj in sourcemap.items(): for file in Path(source_dir / dirname).files(): try: obj = accobj.from_file(file) cls.objects.append(obj) if type(obj) is not ModuleObject: if not obj.name_ in cls.index: cls.index[obj.name_] = [] cls.index[obj.name_].append(obj) else: for fname in obj.functions: if not fname in cls.index: cls.index[fname] = [] cls.index[fname].append(obj) except InvalidFileExt: print("Ignored unrecognized file: {}".format(file)) @classmethod def parse_source(cls, subject): # On cherche le nom de chaque autre objet, ainsi que le nom des fonctions issues des modules look_for = [obj.name_ for obj in cls.objects if obj is not subject] + list(sum([obj.functions for obj in cls.objects if obj is not subject], [])) names = "|".join(list(set(look_for))) rx = re.compile("""(.*(?:^|\t| |\[|\]|&|\(|\)|\.|!|"|')({})(?:$|\t| |\[|\]|&|\(|\)|\.|!|"|').*)""".format(names), MULTILINE) # Indexe la position des lignes line_ends = [m.end() for m in re.finditer('.*\n', subject.sourcecode)] for match in rx.finditer(subject.sourcecode): line = next(i for i in range(len(line_ends)) if line_ends[i] > match.start(1)) + 1 quote = match.group(1).strip() objname = match.group(2) if len(cls.index[objname]) == 1: obj = cls.index[objname][0] else: # plusieurs objets portent le même nom # si l'objet mentionné porte le même nom que le sujet, on part du principe qu'il se mentione lui-même if objname == subject.name_: obj = subject else: obj = cls.index[objname][0] subject.mentions.append(Mention(line, objname, quote, obj)) @classmethod def parse_all(cls): # Mise à jour des dépendances: # # parcourt les objets, et recherche dans le code source de chacun des mentions du nom des autres objets. for index, subject in enumerate(cls.objects): cls.report(index, len(cls.objects), "* {}: {}".format(subject.type_, subject.name_)) cls.parse_source(subject) @classmethod def build_trees(cls): total = len(cls.objects) for index, subject in enumerate(cls.objects): cls.report(index, total * 2) subject.deps = [] for mention in subject.mentions: if not mention.obj in subject.deps and not mention.obj is subject: subject.deps.append(mention.obj) for index, subject in enumerate(cls.objects): cls.report(total + index, total * 2) subject.refs = [] for obj in cls.objects: if obj is subject: continue if subject in obj.deps: subject.refs.append(obj) @classmethod def run(cls, source_dir): # Liste les objets à partir de l'arborescence du repertoire des sources cls.report(0, 100, "Chargement des données") cls.load_objects(source_dir) cls.report(0, 100, "> {} objets trouvés".format(len(cls.objects))) cls.report(0, 100, "Analyse du code source".format(len(cls.objects))) cls.parse_all() cls.report(0, 100, "Construction de l'arbre des dépendances".format(len(cls.objects))) cls.build_trees() cls.report(100, 100, "Analyse terminée") cls.ended() return cls.objects @classmethod def duplicates(cls): return {k: v for k, v in cls.index.items() if len(v) > 1} if __name__ == "__main__": here = Path(__file__).parent.abspath() source_dir = here / r"test\source" resultfile = here / r"test\analyse.txt" resultfile.remove_p() def print_(i, total, msg=""): if msg: print("({}/{}) {}".format(i, total, msg)) Analyse.report = print_ Analyse.run(source_dir) with open(resultfile, "w+", encoding='utf-8') as f: for obj in Analyse.objects: msg = "# '{}' [{}]".format(obj.name_, obj.type_) if obj.deps: msg += "\n\tMentionne: {}".format(", ".join(["'{}' [{}]".format(dep.name_, dep.type_) for dep in obj.deps])) else: msg += "\n\t (ne mentionne aucun autre objet)" if obj.refs: msg += "\n\tEst mentionné par: {}".format(", ".join(["'{}' [{}]".format(ref.name_, ref.type_) for ref in obj.refs])) else: msg += "\n\t (n'est mentionné nul part ailleurs)" if obj.mentions: msg += "\n\t Détail:" for mention in obj.mentions: msg += "\n\t\t'{}'\t\tLine: {}\t>>\t{}".format(mention.objname, mention.line, mention.quote) msg += "\n" f.write(msg) print("# Terminé")