core.py 9.0 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 références circulaires
  9. here = Path(__file__).parent.abspath()
  10. def recurse(acc_obj):
  11. deptree = []
  12. for dep in acc_obj.deps:
  13. deptree.append(dep)
  14. if dep.deps:
  15. deptree += recurse(dep)
  16. return deptree
  17. class InvalidFileExt(IOError):
  18. pass
  19. class Mention():
  20. def __init__(self, line, objname, quote, obj, warning=""):
  21. self.line = line
  22. self.objname = objname
  23. self.quote = quote
  24. self.obj = obj
  25. self.warning = warning
  26. class AccessObject():
  27. type_ = "<unknown>"
  28. _valid_file_exts = (".bas")
  29. _order = 0
  30. def __init__(self, name_):
  31. self.name_ = name_
  32. self.functions = []
  33. self.sourcefile = ""
  34. self._sourcecode = ""
  35. self.mentions = []
  36. self.deps = []
  37. self.refs = []
  38. def __repr__(self):
  39. return "<{}: {}>".format(self.type_, self.name_)
  40. @classmethod
  41. def from_file(cls, file):
  42. file = Path(file)
  43. if file.ext.lower() not in cls._valid_file_exts:
  44. raise InvalidFileExt("Format de fichier d'entrée non valide ({})".format(file.name))
  45. obj = cls(AccessObject.path_to_name(file))
  46. obj.sourcefile = file
  47. obj._sourcecode = file.text()
  48. obj._sourcecode.replace("\r\n", "\n")
  49. return obj
  50. @property
  51. def sourcecode(self):
  52. if not self._sourcecode:
  53. self._sourcecode = self.sourcefile.text()
  54. return self._sourcecode
  55. SUBSTR = {92: "\\", 47: "/", 58: ":", 42: "*", 63:"?", 34:"\"", 60:"<", 62:">", 124:"|" }
  56. @staticmethod
  57. def path_to_name(path):
  58. name_ = path.name.stripext()
  59. for ascii_code, char in AccessObject.SUBSTR.items():
  60. name_ = name_.replace("[{}]".format(ascii_code), char)
  61. return name_
  62. class TableObject(AccessObject):
  63. type_ = "Table"
  64. _valid_file_exts = (".xml", ".lnkd")
  65. _order = 10
  66. class QueryObject(AccessObject):
  67. type_ = "Query"
  68. _order = 30
  69. class FormObject(AccessObject):
  70. type_ = "Form"
  71. _order = 40
  72. class ReportObject(AccessObject):
  73. type_ = "Report"
  74. _order = 50
  75. class MacroObject(AccessObject):
  76. type_ = "Macro"
  77. _order = 60
  78. class ModuleObject(AccessObject):
  79. type_ = "Module"
  80. _order = 70
  81. @classmethod
  82. def from_file(cls, file):
  83. obj = super(ModuleObject, cls).from_file(file)
  84. rx = re.compile(r"Sub|Function ([^(]+)\(")
  85. obj.functions = [fname for fname in rx.findall(file.text()) if fname]
  86. return obj
  87. class RelationObject(AccessObject):
  88. type_ = "Relation"
  89. _valid_file_exts = (".txt")
  90. _order = 20
  91. class Analyse():
  92. objects = []
  93. index = {}
  94. @classmethod
  95. def report(cls, current, total, msg=""):
  96. pass
  97. @classmethod
  98. def ended(cls):
  99. pass
  100. @classmethod
  101. def dump_to(cls, filepath):
  102. with open(filepath, 'wb') as f:
  103. pickle.dump(cls.objects, f)
  104. @classmethod
  105. def load_from(cls, filepath):
  106. cls.objects = []
  107. with open(filepath, 'rb') as f:
  108. cls.objects = pickle.load(f)
  109. cls.update_index()
  110. @classmethod
  111. def update_index(cls):
  112. cls.index = {}
  113. for obj in cls.objects:
  114. if not obj.name_ in cls.index:
  115. cls.index[obj.name_] = []
  116. cls.index[obj.name_].append(obj)
  117. if type(obj) is ModuleObject:
  118. for fname in obj.functions:
  119. if not fname in cls.index:
  120. cls.index[fname] = []
  121. cls.index[fname].append(obj)
  122. @classmethod
  123. def load_objects(cls, source_dir):
  124. source_dir = Path(source_dir)
  125. cls.objects = []
  126. cls.index = {}
  127. sourcemap = {
  128. "tables": TableObject,
  129. "relations": RelationObject,
  130. "queries": QueryObject,
  131. "forms": FormObject,
  132. "reports": ReportObject,
  133. "scripts": MacroObject,
  134. "modules": ModuleObject,
  135. }
  136. for dirname, accobj in sourcemap.items():
  137. for file in Path(source_dir / dirname).files():
  138. try:
  139. obj = accobj.from_file(file)
  140. cls.objects.append(obj)
  141. except InvalidFileExt:
  142. print("Ignored unrecognized file: {}".format(file))
  143. cls.objects.sort(key=lambda x: (x._order, x.name_))
  144. cls.update_index()
  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 and type(object) is not ModuleObject] + 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é")