core.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. '''
  2. @author: olivier.massot
  3. '''
  4. import re
  5. from _regex_core import MULTILINE
  6. from path import Path
  7. # 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.
  8. # TODO: ignorer les commentaires dans les modules
  9. # TODO: ignorer les labels dans les formulaires et états
  10. # TODO: Gérer les références circulaires
  11. # TODO: Stocker un aperçu du contexte de la ou des références dans le code source, pour contrôle ultérieur
  12. # TODO: Permettre de supprimer / ajouter des réferences
  13. # TODO: Verifier que la recherche puisse être Case sensitive?
  14. here = Path(__file__).parent.abspath()
  15. def recurse(acc_obj):
  16. deptree = []
  17. for dep in acc_obj.deps:
  18. deptree.append(dep)
  19. if dep.deps:
  20. deptree += recurse(dep)
  21. return deptree
  22. class InvalidFileExt(IOError):
  23. pass
  24. class Mention():
  25. def __init__(self, line, objname, quote, obj, warning=""):
  26. self.line = line
  27. self.objname = objname
  28. self.quote = quote
  29. self.obj = obj
  30. self.warning = warning
  31. class AccessObject():
  32. type_ = "<unknown>"
  33. _valid_file_exts = (".bas")
  34. _order = 0
  35. def __init__(self, name_):
  36. self.name_ = name_
  37. self.functions = []
  38. self.sourcefile = ""
  39. self._sourcecode = ""
  40. self.mentions = []
  41. self.deps = []
  42. self.refs = []
  43. def __repr__(self):
  44. return "<{}: {}>".format(self.type_, self.name_)
  45. @classmethod
  46. def from_file(cls, file):
  47. file = Path(file)
  48. if file.ext not in cls._valid_file_exts:
  49. raise InvalidFileExt("Format de fichier d'entrée non valide ({})".format(file.name))
  50. obj = cls(AccessObject.path_to_name(file))
  51. obj.sourcefile = file
  52. obj._sourcecode = file.text()
  53. obj._sourcecode.replace("\r\n", "\n")
  54. return obj
  55. @property
  56. def sourcecode(self):
  57. if not self._sourcecode:
  58. self._sourcecode = self.sourcefile.text()
  59. return self._sourcecode
  60. SUBSTR = {92: "\\", 47: "/", 58: ":", 42: "*", 63:"?", 34:"\"", 60:"<", 62:">", 124:"|" }
  61. @staticmethod
  62. def path_to_name(path):
  63. name_ = path.name.stripext()
  64. for ascii_code, char in AccessObject.SUBSTR.items():
  65. name_ = name_.replace("[{}]".format(ascii_code), char)
  66. return name_
  67. class TableObject(AccessObject):
  68. type_ = "Table"
  69. _valid_file_exts = (".xml", ".lnkd")
  70. _order = 10
  71. class QueryObject(AccessObject):
  72. type_ = "Query"
  73. _order = 30
  74. class FormObject(AccessObject):
  75. type_ = "Form"
  76. _order = 40
  77. class ReportObject(AccessObject):
  78. type_ = "Report"
  79. _order = 50
  80. class MacroObject(AccessObject):
  81. type_ = "Macro"
  82. _order = 60
  83. class ModuleObject(AccessObject):
  84. type_ = "Module"
  85. _order = 70
  86. @classmethod
  87. def from_file(cls, file):
  88. obj = super(ModuleObject, cls).from_file(file)
  89. rx = re.compile(r"Sub|Function ([^(]+)\(")
  90. obj.functions = [fname for fname in rx.findall(file.text()) if fname]
  91. return obj
  92. class RelationObject(AccessObject):
  93. type_ = "Relation"
  94. _valid_file_exts = (".txt")
  95. _order = 20
  96. class Analyse():
  97. objects = []
  98. index = {}
  99. @classmethod
  100. def report(cls, current, total, msg=""):
  101. pass
  102. @classmethod
  103. def ended(cls):
  104. pass
  105. @classmethod
  106. def load_objects(cls, source_dir):
  107. source_dir = Path(source_dir)
  108. cls.objects = []
  109. cls.duplicated_names = []
  110. sourcemap = {
  111. "tables": TableObject,
  112. "relations": RelationObject,
  113. "queries": QueryObject,
  114. "forms": FormObject,
  115. "reports": ReportObject,
  116. "scripts": MacroObject,
  117. "modules": ModuleObject,
  118. }
  119. for dirname, accobj in sourcemap.items():
  120. for file in Path(source_dir / dirname).files():
  121. try:
  122. obj = accobj.from_file(file)
  123. cls.objects.append(obj)
  124. if type(obj) is not ModuleObject:
  125. if not obj.name_ in cls.index:
  126. cls.index[obj.name_] = []
  127. cls.index[obj.name_].append(obj)
  128. else:
  129. for fname in obj.functions:
  130. if not fname in cls.index:
  131. cls.index[fname] = []
  132. cls.index[fname].append(obj)
  133. except InvalidFileExt:
  134. print("Ignored unrecognized file: {}".format(file))
  135. cls.objects.sort(key=lambda x: (x._order, x.name_))
  136. @classmethod
  137. def parse_source(cls, subject):
  138. # On cherche le nom de chaque autre objet, ainsi que le nom des fonctions issues des modules
  139. 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], []))
  140. names = "|".join(list(set(look_for)))
  141. rx = re.compile("""(.*(?:^|\t| |\[|\]|&|\(|\)|\.|!|"|')({})(?:$|\t| |\[|\]|&|\(|\)|\.|!|"|').*)""".format(names), MULTILINE)
  142. # Indexe la position des lignes
  143. line_ends = [m.end() for m in re.finditer('.*\n', subject.sourcecode)]
  144. warning = ""
  145. for match in rx.finditer(subject.sourcecode):
  146. line = next(i for i in range(len(line_ends)) if line_ends[i] > match.start(1)) + 1
  147. quote = match.group(1).strip()
  148. objname = match.group(2)
  149. if len(cls.index[objname]) == 1:
  150. obj = cls.index[objname][0]
  151. else:
  152. # plusieurs objets portent le même nom
  153. warning = "Plusieurs objets portant ce nom ont été trouvés, vérifiez qu'il s'agit bien de l'objet ci-contre."
  154. if objname == subject.name_:
  155. # si l'objet mentionné porte le même nom que le sujet, on part du principe qu'il se mentione lui-même
  156. obj = subject
  157. else:
  158. obj = cls.index[objname][0]
  159. subject.mentions.append(Mention(line, objname, quote, obj, warning))
  160. @classmethod
  161. def parse_all(cls):
  162. # Mise à jour des dépendances:
  163. # # parcourt les objets, et recherche dans le code source de chacun des mentions du nom des autres objets.
  164. for index, subject in enumerate(cls.objects):
  165. cls.report(index, len(cls.objects), "* {}: {}".format(subject.type_, subject.name_))
  166. cls.parse_source(subject)
  167. @classmethod
  168. def build_trees(cls):
  169. total = len(cls.objects)
  170. for index, subject in enumerate(cls.objects):
  171. cls.report(index, total * 2)
  172. subject.deps = []
  173. for mention in subject.mentions:
  174. if not mention.obj in subject.deps and not mention.obj is subject:
  175. subject.deps.append(mention.obj)
  176. for index, subject in enumerate(cls.objects):
  177. cls.report(total + index, total * 2)
  178. subject.refs = []
  179. for obj in cls.objects:
  180. if obj is subject:
  181. continue
  182. if subject in obj.deps:
  183. subject.refs.append(obj)
  184. @classmethod
  185. def run(cls, source_dir):
  186. # Liste les objets à partir de l'arborescence du repertoire des sources
  187. cls.report(0, 100, "Chargement des données")
  188. cls.load_objects(source_dir)
  189. cls.report(0, 100, "> {} objets trouvés".format(len(cls.objects)))
  190. cls.report(0, 100, "Analyse du code source".format(len(cls.objects)))
  191. cls.parse_all()
  192. cls.report(0, 100, "Construction de l'arbre des dépendances".format(len(cls.objects)))
  193. cls.build_trees()
  194. cls.report(100, 100, "Analyse terminée")
  195. cls.ended()
  196. return cls.objects
  197. @classmethod
  198. def duplicates(cls):
  199. return {k: v for k, v in cls.index.items() if len(v) > 1}
  200. if __name__ == "__main__":
  201. source_dir = here / r"test\source"
  202. resultfile = here / r"test\analyse.txt"
  203. resultfile.remove_p()
  204. def print_(i, total, msg=""):
  205. if msg:
  206. print("({}/{}) {}".format(i, total, msg))
  207. Analyse.report = print_
  208. Analyse.run(source_dir)
  209. with open(resultfile, "w+", encoding='utf-8') as f:
  210. for obj in Analyse.objects:
  211. msg = "# '{}' [{}]".format(obj.name_, obj.type_)
  212. if obj.deps:
  213. msg += "\n\tMentionne: {}".format(", ".join(["'{}' [{}]".format(dep.name_, dep.type_) for dep in obj.deps]))
  214. else:
  215. msg += "\n\t (ne mentionne aucun autre objet)"
  216. if obj.refs:
  217. msg += "\n\tEst mentionné par: {}".format(", ".join(["'{}' [{}]".format(ref.name_, ref.type_) for ref in obj.refs]))
  218. else:
  219. msg += "\n\t (n'est mentionné nul part ailleurs)"
  220. if obj.mentions:
  221. msg += "\n\t Détail:"
  222. for mention in obj.mentions:
  223. msg += "\n\t\t'{}'\t\tLine: {}\t>>\t{}".format(mention.objname, mention.line, mention.quote)
  224. msg += "\n"
  225. f.write(msg)
  226. print("# Terminé")