PyForTool
Python-fortran-tool
Loading...
Searching...
No Matches
tree.py
1"""
2Code dependency tree analysis and visualization.
3
4Provides the Tree class for building and navigating compilation and execution
5dependency graphs across multiple FORTRAN source files.
6
7Key Features
8------------
9- Compilation dependency tracking (USE statements)
10- Execution dependency tracking (CALL statements)
11- Source directory scanning
12- Tree serialization to/from JSON
13- Dependency graph visualization (DOT format)
14- Scope reachability analysis
15
16Classes
17-------
18Tree : Manages cross-file dependency analysis
19
20Examples
21--------
22>>> tree = Tree(['/path/to/src'], descTreeFile='tree.json')
23>>> deps = tree.needsFile('file.F90')
24>>> tree.plotExecTreeFromScope('module:MOD/sub:SUB', 'deps.png', 2, 2)
25>>> tree.toJson('tree.json') # Save for later use
26"""
27
28import glob
29import os
30import logging
31import json
32import subprocess
33import re
34from functools import wraps
35
36from pyfortool.util import debugDecor, n2name
37import pyfortool.scope
39
40
41def updateTree(method='file'):
42 """
43 Decorator factory to update the tree after PYFTscope method execution.
44
45 Parameters
46 ----------
47 method : str, optional
48 Update method:
49 - 'file' (default): Analyze current file.
50 - 'scan': Analyze new files, remove info for deleted files.
51 - 'signal': Analyze files signaled via tree.signal().
52 """
53 assert method in ('file', 'scan', 'signal')
54
55 def decorator(func):
56 @wraps(func)
57 def wrapper(self, *args, **kwargs):
58 result = func(self, *args, **kwargs)
59 if self.tree.isValid:
60 if method == 'file':
61 self.tree.update(self)
62 elif method == 'scan':
63 current = set(self.tree.getFiles())
64 old = set(self.tree.knownFiles())
65 self.tree.update(current.symmetric_difference(old))
66 elif method == 'signal':
67 self.tree.update(self.tree.popSignaled())
68 return result
69 return wrapper
70 return decorator
71
72
73class Tree():
74 """
75 Build and navigate the code dependency tree.
76
77 Analyzes FORTRAN source files to build compilation and execution
78 dependency graphs for cross-file analysis.
79
80 Examples
81 --------
82 >>> tree = Tree(['/path/to/src'], descTreeFile='tree.json')
83 >>> deps = tree.needsFile('file.F90')
84 >>> tree.plotExecTreeFromScope('module:MOD/sub:SUB', 'deps.png', 2, 2)
85 """
86 def __init__(self, tree=None, descTreeFile=None,
87 parserOptions=None, wrapH=False,
88 verbosity=None):
89 """
90 :param tree: list of directories composing the tree or None
91 :param descTreeFile: filename where the description of the tree will be stored
92 :param parserOptions, wrapH: see the PYFT class
93 :param verbosity: if not None, sets the verbosity level
94 """
95 # Options
96 self._tree = [] if tree is None else tree
97 self._descTreeFile = descTreeFile
98 self._parserOptions = parserOptions
99 self._wrapH = wrapH
100 self._verbosity = verbosity
101
102 # Files signaled for update
103 self._signaled = set()
104
105 # File analysis
106 self._cwd = os.getcwd()
107 self._emptyCache()
108 self._scopes = {}
109 self._useList = {}
110 self._includeList = {}
111 self._callList = {}
112 self._funcList = {}
113 self._cacheCompilationTree = None
114 self._cacheExecutionTree = None
115 self._cacheIncInScope = None
116 if descTreeFile is not None and os.path.exists(descTreeFile):
117 self.fromJson(descTreeFile)
118 elif tree is not None:
119 self._build()
120 if descTreeFile is not None:
121 self.toJson(descTreeFile)
122
123 def getFullContent(self):
124 """
125 :return: a dict containing the full content of the instance
126 """
127 return {'tree': self._tree,
128 'descTreeFile': self._descTreeFile,
129 'parserOptions': self._parserOptions,
130 'wrapH': self._wrapH,
131 'verbosity': self._verbosity,
132 'cwd': self._cwd,
133 'scopes': self._scopes,
134 'useList': self._useList,
135 'includeList': self._includeList,
136 'callList': self._callList,
137 'funcList': self._funcList,
138 'signaled': self._signaled,
139 'cache_compilationTree': self._cacheCompilationTree,
140 'cacheExecutionTree': self._cacheExecutionTree,
141 'cacheIncScope': self._cacheIncInScope}
142
143 def setFullContent(self, content):
144 """
145 :param content: Fill the current instance with this dict
146 """
147 self._tree = content['tree']
148 self._descTreeFile = content['descTreeFile']
149 self._parserOptions = content['parserOptions']
150 self._wrapH = content['wrapH']
151 self._verbosity = content['verbosity']
152 self._cwd = content['cwd']
153 self._scopes = content['scopes']
154 self._useList = content['useList']
155 self._includeList = content['includeList']
156 self._callList = content['callList']
157 self._funcList = content['funcList']
158 self._signaled = content['signaled']
159 self._cacheCompilationTree = content['cache_compilationTree']
160 self._cacheExecutionTree = content['cacheExecutionTree']
161 self._cacheIncInScope = content['cacheIncScope']
162
163 def copyFromOtherTree(self, other):
164 """
165 Sets self to be a copy of other
166 """
167 self.setFullContent(other.getFullContent())
168
169 def copyToOtherTree(self, other):
170 """
171 Sets other to be a copy of self
172 """
173 other.setFullContent(self.getFullContent())
174
175 def signal(self, file):
176 """
177 Method used for signaling a modified file which needs to be analized
178 :param filename: file name or PYFTscope object
179 """
180 self._signaled.add(file)
181
182 def popSignaled(self):
183 """
184 :return: the list of file signaled for update and empties the list
185 """
186 temp = self._signaled
187 self._signaled = set()
188 return temp
189
190 def knownFiles(self):
191 """
192 :return: the list of analysez file names
193 """
194 return list(self._scopes.keys())
195
196 @property
197 def isValid(self):
198 """Is the Tree object valid"""
199 return len(self._scopes) != 0
200
201 @property
202 def tree(self):
203 """List of directories"""
204 return self._tree
205
206 @debugDecor
207 def getDirs(self):
208 """
209 :param tree: list of directories composing the tree or None
210 :return: list of directories and subdirectories
211 """
212 result = []
213 if self.tree is not None:
214 for tDir in self.tree:
215 result += glob.glob(tDir + '/**/', recursive=True)
216 return result
217
218 @debugDecor
219 def getFiles(self):
220 """
221 :param tree: list of directories composing the tree or None
222 :return: list of directories and subdirectories
223 """
224 filenames = []
225 for tDir in self.tree:
226 for filename in glob.glob(tDir + '/**/*', recursive=True):
227 if os.path.splitext(filename)[1] not in ('', '.json', '.fypp',
228 '.txt', '.cmake',
229 '.in'):
230 # We only keep files with extension
231 filenames.append(filename)
232 return filenames
233
234 @debugDecor
235 def _build(self):
236 """
237 Builds the self._* variable
238 """
239 # Loop on directory and files
240 for filename in self.getFiles():
241 self._analyseFile(filename)
242
243 @debugDecor
244 def update(self, file):
245 """
246 Updates the object when a file has changed
247 :param file: name of the file (or list of names) with updated content
248 or PYFTscope object
249 """
250 if self.isValid:
251 if not isinstance(file, (list, set)):
252 file = [file]
253 if len(file) != 0:
254 for onefile in file:
255 self._analyseFile(onefile)
256 self._emptyCache()
257
258 def _emptyCache(self):
259 """Empties cached values"""
260 self._cacheCompilationTree = None
261 self._cacheExecutionTree = None
262 self._cacheIncInScope = None
263
264 @property
265 def _incInScope(self):
266 """Fill and return the self._cacheIncInScope cached value"""
267 if self.isValid and self._cacheIncInScope is None:
268 # pylint: disable-next=pointless-statement
269 self._compilationTree_compilationTree # self._cacheIncInScope computed at the same time
270 return self._cacheIncInScope
271
272 @property
273 @debugDecor
274 def _compilationTree(self):
275 """Fill and return the self._cacheCompilationTree cached value"""
276 if self.isValid and self._cacheCompilationTree is None:
277 self._cacheCompilationTree = {f: [] for f in self._scopes}
278 self._cacheIncInScope = {}
279 # Compilation_tree computation: include
280 for filename, incScopePaths in self._includeList.items():
281 # Loop on scopes
282 for scopePath, incList in incScopePaths.items():
283 # Loop on each included file
284 self._cacheIncInScope[scopePath] = []
285 for inc in incList:
286 # Try to guess the right file
287 same = []
288 subdir = []
289 basename = []
290 # Loop on each file found in the source tree
291 for file in self._cacheCompilationTree:
292 if os.path.normpath(inc) == os.path.normpath(file):
293 # Exactly the same file name (including directories)
294 same.append(file)
295 elif ((not os.path.isabs(file)) and
296 os.path.realpath(inc) == os.path.realpath(os.path.join(
297 os.path.dirname(inc), file))):
298 # The include statement refers to a file contained in the
299 # directory where inc is
300 subdir.append(file)
301 elif os.path.basename(inc) == os.path.basename(file):
302 # Same name excluding the directories
303 basename.append(file)
304 if len(same) > 1:
305 same = subdir = basename = []
306 if len(subdir) > 1:
307 subdir = basename = []
308 if len(basename) > 1:
309 basename = []
310 found = True
311 if len(same) > 0:
312 incFilename = same[0]
313 elif len(subdir) > 0:
314 incFilename = subdir[0]
315 elif len(basename) > 0:
316 incFilename = basename[0]
317 else:
318 # We haven't found the file in the tree, we keep the inc untouched
319 found = False
320 incFilename = inc
321 self._cacheCompilationTree[filename].append(incFilename)
322 if found:
323 self._cacheIncInScope[scopePath].append(incFilename)
324
325 # Compilation_tree computation: use
326 for filename, uList in self._useList.items():
327 # Loop on each use statement
328 for modName, _ in [use for li in uList.values() for use in li]:
329 moduleScopePath = 'module:' + modName
330 # Loop on scopes to find the module
331 found = []
332 for file, scopes in self._scopes.items():
333 if moduleScopePath in scopes:
334 found.append(file)
335 if len(found) == 1:
336 self._cacheCompilationTree[filename].append(found[0])
337 else:
338 logging.info('Several or none file containing the scope path ' +
339 '%s have been found for file %s',
340 moduleScopePath, filename)
341
342 # Compilation_tree: cleaning (uniq values)
343 for filename, depList in self._cacheCompilationTree.items():
344 self._cacheCompilationTree[filename] = list(set(depList))
345
346 return self._cacheCompilationTree
347
348 @property
349 @debugDecor
350 def _executionTree(self):
351 """Fill and return the self._cacheCompilationTree cached value"""
352 if self.isValid and self._cacheExecutionTree is None:
353 self._cacheExecutionTree = {}
354 # Execution_tree: call statements
355 allScopes = [scopePath for _, l in self._scopes.items() for scopePath in l]
356 self._cacheExecutionTree = {scopePath: [] for scopePath in allScopes}
357 for canonicKind, progList in (('sub', self._callList), ('func', self._funcList)):
358 for filename, callScopes in progList.items():
359 # Loop on scopes
360 for scopePath, cList in callScopes.items():
361 # Loop on calls
362 for call in set(cList):
363 foundInUse = []
364 foundElsewhere = []
365 foundInInclude = []
366 foundInContains = []
367 foundInSameScope = []
368
369 # We look for sub:c or interface:c
370 for kind in (canonicKind, 'interface'):
371 # Loop on each use statement in scope or in upper scopes
372 uList = [self._useList[filename][sc]
373 for sc in self._useList[filename]
374 if (sc == scopePath or scopePath.startswith(sc + '/'))]
375 for modName, only in [use for li in uList for use in li]:
376 moduleScope = 'module:' + modName
377 callScope = moduleScope + '/' + kind + ':' + call
378 if len(only) > 0:
379 # There is a "ONLY" keyword
380 if call in only and callScope in allScopes:
381 foundInUse.append(callScope)
382 else:
383 # There is no "ONLY"
384 for _, scopes in self._scopes.items():
385 if callScope in scopes:
386 foundInUse.append(callScope)
387
388 # Look for subroutine directly accessible
389 callScope = kind + ':' + call
390 for _, scopes in self._scopes.items():
391 if callScope in scopes:
392 foundElsewhere.append(callScope)
393
394 # Look for include files
395 callScope = kind + ':' + call
396 for incFile in self._incInScope[scopePath]:
397 if callScope in self._scopes[incFile]:
398 foundInInclude.append(callScope)
399
400 # Look for contained routines
401 callScope = scopePath + '/' + kind + ':' + call
402 if callScope in self._scopes[filename]:
403 foundInContains.append(callScope)
404
405 # Look for routine in the same scope
406 if '/' in scopePath:
407 callScope = scopePath.rsplit('/', 1)[0] + '/' + kind + \
408 ':' + call
409 else:
410 callScope = kind + ':' + call
411 if callScope in self._scopes[filename]:
412 foundInSameScope.append(callScope)
413
414 # Final selection
415 foundInUse = list(set(foundInUse)) # If a module is used several times
416 if len(foundInUse + foundInInclude +
417 foundInContains + foundInSameScope) > 1:
418 logging.error('Several definition of the program unit found for '
419 '%s called in %s:', call, scopePath)
420 logging.error(' found %i time(s) in USE statements',
421 len(foundInUse))
422 logging.error(' found %i time(s) in include files',
423 len(foundInInclude))
424 logging.error(' found %i time(s) in CONTAINS block',
425 len(foundInContains))
426 logging.error(' found %i time(s) in the same scope',
427 len(foundInSameScope))
428 self._cacheExecutionTree[scopePath].append('??')
429 elif len(foundInUse + foundInInclude +
430 foundInContains + foundInSameScope) == 1:
431 rr = (foundInUse + foundInInclude +
432 foundInContains + foundInSameScope)[0]
433 if canonicKind != 'func' or rr in allScopes:
434 self._cacheExecutionTree[scopePath].append(rr)
435 elif len(foundElsewhere) > 1:
436 logging.info('Several definition of the program unit found for '
437 '%s called in %s', call, scopePath)
438 elif len(foundElsewhere) == 1:
439 self._cacheExecutionTree[scopePath].append(foundElsewhere[0])
440 else:
441 if canonicKind != 'func':
442 logging.info('No definition of the program unit found for '
443 '%s called in %s', call, scopePath)
444
445 # Execution_tree: named interface
446 # We replace named interface by the list of routines declared in this interface
447 # This is not perfect because only one routine is called and not all
448 for _, execList in self._cacheExecutionTree.items():
449 for item in list(execList):
450 itemSplt = item.split('/')[-1].split(':')
451 if itemSplt[0] == 'interface' and itemSplt[1] != '--UNKNOWN--':
452 # This is a named interface
453 filenames = [k for (k, v) in self._scopes.items() if item in v]
454 if len(filenames) == 1:
455 # We have found in which file this interface is declared
456 execList.remove(item)
457 for sub in [sub for sub in self._scopes[filenames[0]]
458 if sub.startswith(item + '/')]:
459 subscopeIn = sub.rsplit('/', 2)[0] + '/' + sub.split('/')[-1]
460 if subscopeIn in self._scopes[filenames[0]]:
461 # Routine found in the same scope as the interface
462 execList.append(subscopeIn)
463 else:
464 execList.append(sub.split('/')[-1])
465
466 # Execution_tree: cleaning (uniq values)
467 for scopePath, execList in self._cacheExecutionTree.items():
468 self._cacheExecutionTree[scopePath] = list(set(execList))
469
470 return self._cacheExecutionTree
471
472 @debugDecor
473 def _analyseFile(self, file):
474 """
475 :param file: Name of the file to explore, or PYFTscope object
476 :return: dict of use, include, call, function and scope list
477 """
478 def extractString(text):
479 text = text.strip()
480 if text[0] in ('"', "'"):
481 assert text[-1] == text[0]
482 text = text[1, -1]
483 return text
484
485 # Loop on directory and files
486 if isinstance(file, pyfortool.scope.PYFTscope) or os.path.isfile(file):
487 if isinstance(file, pyfortool.scope.PYFTscope):
488 pft = file.mainScope
489 filename = pft.getFileName()
490 mustClose = False
491 else:
493 self._wrapH, verbosity=self._verbosity)
494 filename = file
495 mustClose = True
496 filename = filename[2:] if filename.startswith('./') else filename
497
498 # Loop on scopes
499 self._scopes[filename] = []
500 self._includeList[filename] = {}
501 self._useList[filename] = {}
502 self._callList[filename] = {}
503 self._funcList[filename] = {}
504 scopes = pft.getScopes()
505 for scope in scopes:
506 # Scope found in file
507 self._scopes[filename].append(scope.path)
508 # We add, to this list, the "MODULE PROCEDURE" declared in INTERFACE statements
509 if scope.path.split('/')[-1].split(':')[0] == 'interface':
510 for name in [n2name(nodeN).upper()
511 for moduleproc in scope.findall('./{*}procedure-stmt')
512 for nodeN in moduleproc.findall('./{*}module-procedure-N-LT/' +
513 '{*}N')]:
514 for sc in scopes:
515 if re.search(scope.path.rsplit('/', 1)[0] + '/[a-zA-Z]*:' + name,
516 sc.path):
517 self._scopes[filename].append(scope.path + '/' +
518 sc.path.split('/')[-1])
519
520 # include, use, call and functions
521 # Fill compilation_tree
522 # Includes give directly the name of the source file but possibly without
523 # the directory
524 self._includeList[filename][scope.path] = \
525 [file.text for file in scope.findall('.//{*}include/{*}filename')] # cpp
526 self._includeList[filename][scope.path].extend(
527 [extractString(file.text)
528 for file in scope.findall('.//{*}include/{*}filename/{*}S')]) # FORTRAN
529
530 # For use statements, we need to scan all the files to know which one
531 # contains the module
532 self._useList[filename][scope.path] = []
533 for use in scope.findall('.//{*}use-stmt'):
534 modName = n2name(use.find('./{*}module-N/{*}N')).upper()
535 only = [n2name(n).upper() for n in use.findall('.//{*}use-N//{*}N')]
536 self._useList[filename][scope.path].append((modName, only))
537
538 # Fill execution tree
539 # We need to scan all the files to find which one contains the subroutine/function
540 self._callList[filename][scope.path] = \
541 list(set(n2name(call.find('./{*}procedure-designator/{*}named-E/{*}N')).upper()
542 for call in scope.findall('.//{*}call-stmt')))
543 # We cannot distinguish function from arrays
544 self._funcList[filename][scope.path] = set()
545 for name in [n2name(call.find('./{*}N')).upper()
546 for call in scope.findall('.//{*}named-E/{*}R-LT/{*}parens-R/../..')]:
547 # But we can exclude some names if they are declared as arrays
548 var = scope.varList.findVar(name)
549 if var is None or var['as'] is None:
550 self._funcList[filename][scope.path].add(name)
551 self._funcList[filename][scope.path] = list(self._funcList[filename][scope.path])
552 if mustClose:
553 pft.close()
554 else:
555 if filename in self._scopes:
556 del self._scopes[filename], self._includeList[filename], \
557 self._useList[filename], self._callList[filename], \
558 self._funcList[filename]
559
560 @debugDecor
561 def fromJson(self, filename):
562 """read from json"""
563 with open(filename, 'r', encoding='utf-8') as file:
564 descTree = json.load(file)
565 self._cwd = descTree['cwd']
566 self._scopes = descTree['scopes']
567 self._useList = descTree['useList']
568 self._includeList = descTree['includeList']
569 self._callList = descTree['callList']
570 self._funcList = descTree['funcList']
571
572 @debugDecor
573 def toJson(self, filename):
574 """save to json"""
575 descTree = {'cwd': self._cwd,
576 'scopes': self._scopes,
577 'useList': self._useList,
578 'includeList': self._includeList,
579 'callList': self._callList,
580 'funcList': self._funcList,
581 }
582 # Order dict keys and list values
583 descTree['scopes'] = {k: sorted(descTree['scopes'][k]) for k in sorted(descTree['scopes'])}
584 for cat in ('useList', 'includeList', 'callList', 'funcList'):
585 descTree[cat] = {file: {scope: sorted(descTree[cat][file][scope])
586 for scope in sorted(descTree[cat][file])}
587 for file in sorted(descTree[cat])}
588 # Write json on disk with indentation
589 with open(filename, 'w', encoding='utf-8') as file:
590 json.dump(descTree, file, indent=2)
591
592 # No @debugDecor for this low-level method
593 def scopeToFiles(self, scopePath):
594 """
595 Return the name of the file defining the scope
596 :param scopePath: scope path to search for
597 :return: list file names in which scope is defined
598 """
599 return [filename for filename, scopes in self._scopes.items() if scopePath in scopes]
600
601 @debugDecor
602 def fileToScopes(self, filename):
603 """
604 Return the scopes contained in the file
605 :param filename: name of the file tn inspect
606 :return: list of scopes defined in the file
607 """
608 return self._scopes[filename]
609
610 @staticmethod
611 def _recurList(node, descTreePart, level, down):
612 """
613 :param node: initial node
614 :param descTreePart: 'compilation_tree' or 'execution_tree' part of a descTree object
615 :param level: number of levels (0 to get only the initial node, None to get all nodes)
616 :param down: True to get the nodes lower in the tree, False to get the upper ones
617 :return: list of nodes lower or upper tahn initial node (recursively)
618 """
619 def recur(nnn, level, currentList):
620 if down:
621 result = descTreePart.get(nnn, [])
622 else:
623 result = [item for (item, l) in descTreePart.items() if nnn in l]
624 if level is None or level > 1:
625 for res in list(result):
626 if res not in currentList: # for FORTRAN recursive calls
627 result.extend(recur(res, None if level is None else level - 1, result))
628 return result
629 return recur(node, level, [])
630
631 @debugDecor
632 def needsFile(self, filename, level=1):
633 """
634 :param filename: initial file name
635 :param level: number of levels (0 to get only the initial file, None to get all files)
636 :return: list of file names needed by the initial file (recursively)
637 """
638 return self._recurList(filename, self._compilationTree_compilationTree, level, True)
639
640 @debugDecor
641 def neededByFile(self, filename, level=1):
642 """
643 :param filename: initial file name
644 :param level: number of levels (0 to get only the initial file, None to get all files)
645 :return: list of file names that needs the initial file (recursively)
646 """
647 return self._recurList(filename, self._compilationTree_compilationTree, level, False)
648
649 @debugDecor
650 def callsScopes(self, scopePath, level=1):
651 """
652 :param scopePath: initial scope path
653 :param level: number of levels (0 to get only the initial scope path,
654 None to get all scopes)
655 :return: list of scopes called by the initial scope path (recursively)
656 """
657 return self._recurList(scopePath, self._executionTree_executionTree, level, True)
658
659 @debugDecor
660 def calledByScope(self, scopePath, level=1):
661 """
662 :param scopePath: initial scope path
663 :param level: number of levels (0 to get only the initial scope path,
664 None to get all scopes)
665 :return: list of scopes that calls the initial scope path (recursively)
666 """
667 return self._recurList(scopePath, self._executionTree_executionTree, level, False)
668
669 @debugDecor
670 def isUnderStopScopes(self, scopePath, stopScopes,
671 includeInterfaces=False, includeStopScopes=False):
672 """
673 :param scopePath: scope path to test
674 :param stopScopes: list of scopes
675 :param includeInterfaces: if True, interfaces of positive scopes are also positive
676 :param includeInterfaces: if True, scopes that are in stopScopes return True
677 :return: True if the scope path is called directly or indirectly by one of the scope
678 paths listed in stopScopes
679 """
680 scopeSplt = scopePath.split('/')
681 if includeInterfaces and len(scopeSplt) >= 2 and scopeSplt[-2].split(':')[0] == 'interface':
682 # This scope declares an interface, we look for the scope corresponding
683 # to this interface
684 scopeI = scopeSplt[-1]
685 if scopeI in self._executionTree_executionTree:
686 # The actual code for the routine exists
687 return self.isUnderStopScopes(scopeI, stopScopes,
688 includeStopScopes=includeStopScopes)
689 # No code found for this interface
690 return False
691 upperScopes = self.calledByScope(scopePath, None)
692 return (any(scp in upperScopes for scp in stopScopes) or
693 (includeStopScopes and scopePath in stopScopes))
694
695 @debugDecor
696 def plotTree(self, centralNodeList, output, plotMaxUpper, plotMaxLower, kind, frame=False):
697 """
698 Compute a dependency graph
699 :param centralNodeList: file, scope path, list of files or list of scope paths
700 :param output: output file name (.dot or .png extension)
701 :param plotMaxUpper: Maximum number of elements to plot, upper than the central element
702 :param plotMaxLower: Maximum number of elements to plot, lower than the central element
703 :param kind: must be 'compilation_tree' or 'execution_tree'
704 :param frame: True to plot a frame grouping the central nodes
705 """
706 assert kind in ('compilation_tree', 'execution_tree')
707
708 hashValues = {0: 1}
709
710 def myHash(obj):
711 objHash = hash(obj)
712 if objHash not in hashValues:
713 hashValues[0] += 1
714 hashValues[objHash] = hashValues[0]
715 return str(hashValues[objHash])
716
717 def createNode(node, label=None):
718 result = ""
719 if label is not None:
720 result += "subgraph cluster_" + myHash(node) + " {\n"
721 result += f'label="{label}"\n'
722 if kind == 'execution_tree':
723 color = 'blue' if node.split('/')[-1].split(':')[0] == 'func' else 'green'
724 else:
725 color = 'black'
726 result += myHash(node) + f' [label="{node}" color="{color}"]\n'
727 if label is not None:
728 result += "}\n"
729 return result
730
731 def createLink(file1, file2):
732 return myHash(file1) + ' -> ' + myHash(file2) + '\n'
733
734 def createCluster(nodes, label=None):
735 result = "subgraph cluster_R {\n"
736 result += "{rank=same " + (' '.join([myHash(node) for node in nodes])) + "}\n"
737 if label is not None:
738 result += f'label="{label}"\n'
739 result += "}\n"
740 return result
741
742 def add(item):
743 if item not in dot:
744 dot.append(item)
745
746 def filename(scopePath):
747 if kind == 'compilation_tree':
748 return None
749 return [f for f, l in self._scopes.items() if scopePath in l][0]
750
751 def recur(node, level, down, var):
752 if level is None or level > 0:
753 if down:
754 result = var.get(node, [])
755 else:
756 result = [f for f, l in var.items() if node in l]
757 for res in result:
758 add(createNode(res, filename(res)))
759 add(createLink(node, res) if down else createLink(res, node))
760 if level is None or level > 1:
761 recur(res, None if level is None else level - 1, down, var)
762
763 # Are all the central scopes in the same file
764 printInFrame = False
765 if kind == 'execution_tree':
766 centralScopeFilenames = []
767 for scopePath in centralNodeList:
768 centralScopeFilenames.append(filename(scopePath))
769 centralScopeFilenames = list(set(centralScopeFilenames))
770 if len(centralScopeFilenames) == 1:
771 frame = True
772 printInFrame = True
773 else:
774 printInFrame = False
775
776 # Order the tree to obtain deterministic graphs
777 var = self._executionTree_executionTree if kind == 'execution_tree' else self._compilationTree_compilationTree
778 var = {k: sorted(var[k]) for k in sorted(var)}
779
780 dot = ["digraph D {\n"]
781 if not isinstance(centralNodeList, list):
782 centralNodeList = [centralNodeList]
783 for centralNode in centralNodeList:
784 add(createNode(centralNode, None if printInFrame else filename(centralNode)))
785 recur(centralNode, plotMaxLower, True, var)
786 recur(centralNode, plotMaxUpper, False, var)
787 if frame:
788 if kind == 'compilation_tree':
789 frameText = None
790 else:
791 frameText = centralScopeFilenames[0] if printInFrame else None
792 add(createCluster(centralNodeList, frameText))
793 add("}\n")
794 dot = ''.join(dot)
795 fmt = os.path.splitext(output)[1].lower()[1:]
796 if fmt == 'dot':
797 with open(output, 'w', encoding='utf-8') as file:
798 file.write(dot)
799 else:
800 dotCommand = ['dot', '-T' + fmt, '-o', output]
801 logging.info('Dot command: %s', ' '.join(dotCommand))
802 subprocess.run(dotCommand, input=dot.encode('utf8'), check=True)
803
804 @debugDecor
805 def plotCompilTreeFromFile(self, filename, output, plotMaxUpper, plotMaxLower):
806 """
807 Compute the compilation dependency graph
808 :param filename: central file
809 :param output: output file name (.dot or .png extension)
810 :param plotMaxUpper: Maximum number of elements to plot, upper than the central element
811 :param plotMaxLower: Maximum number of elements to plot, lower than the central element
812 """
813 return self.plotTree(filename, output, plotMaxUpper, plotMaxLower, 'compilation_tree', True)
814
815 @debugDecor
816 def plotExecTreeFromScope(self, scopePath, output, plotMaxUpper, plotMaxLower):
817 """
818 Compute the execution dependency graph
819 :param scopePath: central scope path
820 :param output: output file name (.dot or .png extension)
821 :param plotMaxUpper: Maximum number of elements to plot, upper than the central element
822 :param plotMaxLower: Maximum number of elements to plot, lower than the central element
823 """
824 return self.plotTree(scopePath, output, plotMaxUpper, plotMaxLower, 'execution_tree')
825
826 @debugDecor
827 def plotCompilTreeFromScope(self, scopePath, output, plotMaxUpper, plotMaxLower):
828 """
829 Compute the compilation dependency graph
830 :param scopePath: central scope path
831 :param output: output file name (.dot or .png extension)
832 :param plotMaxUpper: Maximum number of elements to plot, upper than the central element
833 :param plotMaxLower: Maximum number of elements to plot, lower than the central element
834 """
835 return self.plotTree(self.scopeToFiles(scopePath), output, plotMaxUpper, plotMaxLower,
836 'compilation_tree')
837
838 @debugDecor
839 def plotExecTreeFromFile(self, filename, output, plotMaxUpper, plotMaxLower):
840 """
841 Compute the execution dependency graph
842 :param filename: central filename
843 :param output: output file name (.dot or .png extension)
844 :param plotMaxUpper: Maximum number of elements to plot, upper than the central element
845 :param plotMaxLower: Maximum number of elements to plot, lower than the central element
846 """
847 return self.plotTree(self.fileToScopes(filename), output, plotMaxUpper, plotMaxLower,
848 'execution_tree', True)
849
850 @debugDecor
851 def findScopeInterface(self, scopePath):
852 """
853 Return the file name containing an interface for the scope path
854 :param scopePath: scope path for which an interface is searched
855 :return: (file name, interface scope) or (None, None) if not found
856 """
857 for filename, scopes in self._scopes.items():
858 for scopeInterface in scopes:
859 if re.search(r'interface:[a-zA-Z0-9_-]*/' + scopePath, scopeInterface):
860 return filename, scopeInterface
861 return None, None
plotTree(self, centralNodeList, output, plotMaxUpper, plotMaxLower, kind, frame=False)
Definition tree.py:696
fileToScopes(self, filename)
Definition tree.py:602
needsFile(self, filename, level=1)
Definition tree.py:632
getFullContent(self)
Definition tree.py:123
scopeToFiles(self, scopePath)
Definition tree.py:593
_executionTree(self)
Definition tree.py:350
copyFromOtherTree(self, other)
Definition tree.py:163
callsScopes(self, scopePath, level=1)
Definition tree.py:650
_analyseFile(self, file)
Definition tree.py:473
calledByScope(self, scopePath, level=1)
Definition tree.py:660
popSignaled(self)
Definition tree.py:182
plotExecTreeFromScope(self, scopePath, output, plotMaxUpper, plotMaxLower)
Definition tree.py:816
toJson(self, filename)
Definition tree.py:573
_compilationTree(self)
Definition tree.py:274
update(self, file)
Definition tree.py:244
_recurList(node, descTreePart, level, down)
Definition tree.py:611
_incInScope(self)
Definition tree.py:265
fromJson(self, filename)
Definition tree.py:561
__init__(self, tree=None, descTreeFile=None, parserOptions=None, wrapH=False, verbosity=None)
Definition tree.py:88
findScopeInterface(self, scopePath)
Definition tree.py:851
plotCompilTreeFromFile(self, filename, output, plotMaxUpper, plotMaxLower)
Definition tree.py:805
plotExecTreeFromFile(self, filename, output, plotMaxUpper, plotMaxLower)
Definition tree.py:839
copyToOtherTree(self, other)
Definition tree.py:169
neededByFile(self, filename, level=1)
Definition tree.py:641
signal(self, file)
Definition tree.py:175
isUnderStopScopes(self, scopePath, stopScopes, includeInterfaces=False, includeStopScopes=False)
Definition tree.py:671
_emptyCache(self)
Definition tree.py:258
plotCompilTreeFromScope(self, scopePath, output, plotMaxUpper, plotMaxLower)
Definition tree.py:827
setFullContent(self, content)
Definition tree.py:143
conservativePYFT(filename, parserOptions, wrapH, tree=None, verbosity=None, clsPYFT=None)
Definition pyfortool.py:37