core.py 9.4 KB

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