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', '.yaml',
230 '.sh', '.py', '.pyc'):
231 # We only keep files with extension
232 filenames.append(filename)
233 return filenames
234
235 @debugDecor
236 def _build(self):
237 """
238 Builds the self._* variable
239 """
240 # Loop on directory and files
241 for filename in self.getFiles():
242 self._analyseFile(filename)
243
244 @debugDecor
245 def update(self, file):
246 """
247 Updates the object when a file has changed
248 :param file: name of the file (or list of names) with updated content
249 or PYFTscope object
250 """
251 if self.isValid:
252 if not isinstance(file, (list, set)):
253 file = [file]
254 if len(file) != 0:
255 for onefile in file:
256 self._analyseFile(onefile)
257 self._emptyCache()
258
259 def _emptyCache(self):
260 """Empties cached values"""
261 self._cacheCompilationTree = None
262 self._cacheExecutionTree = None
263 self._cacheIncInScope = None
264
265 @property
266 def _incInScope(self):
267 """Fill and return the self._cacheIncInScope cached value"""
268 if self.isValid and self._cacheIncInScope is None:
269 # pylint: disable-next=pointless-statement
270 self._compilationTree_compilationTree # self._cacheIncInScope computed at the same time
271 return self._cacheIncInScope
272
273 @property
274 @debugDecor
275 def _compilationTree(self):
276 """Fill and return the self._cacheCompilationTree cached value"""
277 if self.isValid and self._cacheCompilationTree is None:
278 self._cacheCompilationTree = {f: [] for f in self._scopes}
279 self._cacheIncInScope = {}
280 # Compilation_tree computation: include
281 for filename, incScopePaths in self._includeList.items():
282 # Loop on scopes
283 for scopePath, incList in incScopePaths.items():
284 # Loop on each included file
285 self._cacheIncInScope[scopePath] = []
286 for inc in incList:
287 # Try to guess the right file
288 same = []
289 subdir = []
290 basename = []
291 # Loop on each file found in the source tree
292 for file in self._cacheCompilationTree:
293 if os.path.normpath(inc) == os.path.normpath(file):
294 # Exactly the same file name (including directories)
295 same.append(file)
296 elif ((not os.path.isabs(file)) and
297 os.path.realpath(inc) == os.path.realpath(os.path.join(
298 os.path.dirname(inc), file))):
299 # The include statement refers to a file contained in the
300 # directory where inc is
301 subdir.append(file)
302 elif os.path.basename(inc) == os.path.basename(file):
303 # Same name excluding the directories
304 basename.append(file)
305 if len(same) > 1:
306 same = subdir = basename = []
307 if len(subdir) > 1:
308 subdir = basename = []
309 if len(basename) > 1:
310 basename = []
311 found = True
312 if len(same) > 0:
313 incFilename = same[0]
314 elif len(subdir) > 0:
315 incFilename = subdir[0]
316 elif len(basename) > 0:
317 incFilename = basename[0]
318 else:
319 # We haven't found the file in the tree, we keep the inc untouched
320 found = False
321 incFilename = inc
322 self._cacheCompilationTree[filename].append(incFilename)
323 if found:
324 self._cacheIncInScope[scopePath].append(incFilename)
325
326 # Compilation_tree computation: use
327 for filename, uList in self._useList.items():
328 # Loop on each use statement
329 for modName, _ in [use for li in uList.values() for use in li]:
330 moduleScopePath = 'module:' + modName
331 # Loop on scopes to find the module
332 found = []
333 for file, scopes in self._scopes.items():
334 if moduleScopePath in scopes:
335 found.append(file)
336 if len(found) == 1:
337 self._cacheCompilationTree[filename].append(found[0])
338 else:
339 logging.info('Several or none file containing the scope path ' +
340 '%s have been found for file %s',
341 moduleScopePath, filename)
342
343 # Compilation_tree: cleaning (uniq values)
344 for filename, depList in self._cacheCompilationTree.items():
345 self._cacheCompilationTree[filename] = list(set(depList))
346
347 return self._cacheCompilationTree
348
349 @property
350 @debugDecor
351 def _executionTree(self):
352 """Fill and return the self._cacheCompilationTree cached value"""
353 if self.isValid and self._cacheExecutionTree is None:
354 self._cacheExecutionTree = {}
355 # Execution_tree: call statements
356 allScopes = [scopePath for _, l in self._scopes.items() for scopePath in l]
357 self._cacheExecutionTree = {scopePath: [] for scopePath in allScopes}
358 for canonicKind, progList in (('sub', self._callList), ('func', self._funcList)):
359 for filename, callScopes in progList.items():
360 # Loop on scopes
361 for scopePath, cList in callScopes.items():
362 # Loop on calls
363 for call in set(cList):
364 foundInUse = []
365 foundElsewhere = []
366 foundInInclude = []
367 foundInContains = []
368 foundInSameScope = []
369
370 # We look for sub:c or interface:c
371 for kind in (canonicKind, 'interface'):
372 # Loop on each use statement in scope or in upper scopes
373 uList = [self._useList[filename][sc]
374 for sc in self._useList[filename]
375 if (sc == scopePath or scopePath.startswith(sc + '/'))]
376 for modName, only in [use for li in uList for use in li]:
377 moduleScope = 'module:' + modName
378 callScope = moduleScope + '/' + kind + ':' + call
379 if len(only) > 0:
380 # There is a "ONLY" keyword
381 if call in only and callScope in allScopes:
382 foundInUse.append(callScope)
383 else:
384 # There is no "ONLY"
385 for _, scopes in self._scopes.items():
386 if callScope in scopes:
387 foundInUse.append(callScope)
388
389 # Look for subroutine directly accessible
390 callScope = kind + ':' + call
391 for _, scopes in self._scopes.items():
392 if callScope in scopes:
393 foundElsewhere.append(callScope)
394
395 # Look for include files
396 callScope = kind + ':' + call
397 for incFile in self._incInScope[scopePath]:
398 if callScope in self._scopes[incFile]:
399 foundInInclude.append(callScope)
400
401 # Look for contained routines
402 callScope = scopePath + '/' + kind + ':' + call
403 if callScope in self._scopes[filename]:
404 foundInContains.append(callScope)
405
406 # Look for routine in the same scope
407 if '/' in scopePath:
408 callScope = scopePath.rsplit('/', 1)[0] + '/' + kind + \
409 ':' + call
410 else:
411 callScope = kind + ':' + call
412 if callScope in self._scopes[filename]:
413 foundInSameScope.append(callScope)
414
415 # Final selection
416 foundInUse = list(set(foundInUse)) # If a module is used several times
417 if len(foundInUse + foundInInclude +
418 foundInContains + foundInSameScope) > 1:
419 logging.error('Several definition of the program unit found for '
420 '%s called in %s:', call, scopePath)
421 logging.error(' found %i time(s) in USE statements',
422 len(foundInUse))
423 logging.error(' found %i time(s) in include files',
424 len(foundInInclude))
425 logging.error(' found %i time(s) in CONTAINS block',
426 len(foundInContains))
427 logging.error(' found %i time(s) in the same scope',
428 len(foundInSameScope))
429 self._cacheExecutionTree[scopePath].append('??')
430 elif len(foundInUse + foundInInclude +
431 foundInContains + foundInSameScope) == 1:
432 rr = (foundInUse + foundInInclude +
433 foundInContains + foundInSameScope)[0]
434 if canonicKind != 'func' or rr in allScopes:
435 self._cacheExecutionTree[scopePath].append(rr)
436 elif len(foundElsewhere) > 1:
437 logging.info('Several definition of the program unit found for '
438 '%s called in %s', call, scopePath)
439 elif len(foundElsewhere) == 1:
440 self._cacheExecutionTree[scopePath].append(foundElsewhere[0])
441 else:
442 if canonicKind != 'func':
443 logging.info('No definition of the program unit found for '
444 '%s called in %s', call, scopePath)
445
446 # Execution_tree: named interface
447 # We replace named interface by the list of routines declared in this interface
448 # This is not perfect because only one routine is called and not all
449 for _, execList in self._cacheExecutionTree.items():
450 for item in list(execList):
451 itemSplt = item.split('/')[-1].split(':')
452 if itemSplt[0] == 'interface' and itemSplt[1] != '--UNKNOWN--':
453 # This is a named interface
454 filenames = [k for (k, v) in self._scopes.items() if item in v]
455 if len(filenames) == 1:
456 # We have found in which file this interface is declared
457 execList.remove(item)
458 for sub in [sub for sub in self._scopes[filenames[0]]
459 if sub.startswith(item + '/')]:
460 subscopeIn = sub.rsplit('/', 2)[0] + '/' + sub.split('/')[-1]
461 if subscopeIn in self._scopes[filenames[0]]:
462 # Routine found in the same scope as the interface
463 execList.append(subscopeIn)
464 else:
465 execList.append(sub.split('/')[-1])
466
467 # Execution_tree: cleaning (uniq values)
468 for scopePath, execList in self._cacheExecutionTree.items():
469 self._cacheExecutionTree[scopePath] = list(set(execList))
470
471 return self._cacheExecutionTree
472
473 @debugDecor
474 def _analyseFile(self, file):
475 """
476 :param file: Name of the file to explore, or PYFTscope object
477 :return: dict of use, include, call, function and scope list
478 """
479 def extractString(text):
480 text = text.strip()
481 if text[0] in ('"', "'"):
482 assert text[-1] == text[0]
483 text = text[1, -1]
484 return text
485
486 # Loop on directory and files
487 if isinstance(file, pyfortool.scope.PYFTscope) or os.path.isfile(file):
488 if isinstance(file, pyfortool.scope.PYFTscope):
489 pft = file.mainScope
490 filename = pft.getFileName()
491 mustClose = False
492 else:
494 self._wrapH, verbosity=self._verbosity)
495 filename = file
496 mustClose = True
497 filename = filename[2:] if filename.startswith('./') else filename
498
499 # Loop on scopes
500 self._scopes[filename] = []
501 self._includeList[filename] = {}
502 self._useList[filename] = {}
503 self._callList[filename] = {}
504 self._funcList[filename] = {}
505 scopes = pft.getScopes()
506 for scope in scopes:
507 # Scope found in file
508 self._scopes[filename].append(scope.path)
509 # We add, to this list, the "MODULE PROCEDURE" declared in INTERFACE statements
510 if scope.path.split('/')[-1].split(':')[0] == 'interface':
511 for name in [n2name(nodeN).upper()
512 for moduleproc in scope.findall('./{*}procedure-stmt')
513 for nodeN in moduleproc.findall('./{*}module-procedure-N-LT/' +
514 '{*}N')]:
515 for sc in scopes:
516 if re.search(scope.path.rsplit('/', 1)[0] + '/[a-zA-Z]*:' + name,
517 sc.path):
518 self._scopes[filename].append(scope.path + '/' +
519 sc.path.split('/')[-1])
520
521 # include, use, call and functions
522 # Fill compilation_tree
523 # Includes give directly the name of the source file but possibly without
524 # the directory
525 self._includeList[filename][scope.path] = \
526 [file.text for file in scope.findall('.//{*}include/{*}filename')] # cpp
527 self._includeList[filename][scope.path].extend(
528 [extractString(file.text)
529 for file in scope.findall('.//{*}include/{*}filename/{*}S')]) # FORTRAN
530
531 # For use statements, we need to scan all the files to know which one
532 # contains the module
533 self._useList[filename][scope.path] = []
534 for use in scope.findall('.//{*}use-stmt'):
535 modName = n2name(use.find('./{*}module-N/{*}N')).upper()
536 only = [n2name(n).upper() for n in use.findall('.//{*}use-N//{*}N')]
537 self._useList[filename][scope.path].append((modName, only))
538
539 # Fill execution tree
540 # We need to scan all the files to find which one contains the subroutine/function
541 self._callList[filename][scope.path] = \
542 list(set(n2name(call.find('./{*}procedure-designator/{*}named-E/{*}N')).upper()
543 for call in scope.findall('.//{*}call-stmt')))
544 # We cannot distinguish function from arrays
545 self._funcList[filename][scope.path] = set()
546 for name in [n2name(call.find('./{*}N')).upper()
547 for call in scope.findall('.//{*}named-E/{*}R-LT/{*}parens-R/../..')]:
548 # But we can exclude some names if they are declared as arrays
549 var = scope.varList.findVar(name)
550 if var is None or var['as'] is None:
551 self._funcList[filename][scope.path].add(name)
552 self._funcList[filename][scope.path] = list(self._funcList[filename][scope.path])
553 if mustClose:
554 pft.close()
555 else:
556 if filename in self._scopes:
557 del self._scopes[filename], self._includeList[filename], \
558 self._useList[filename], self._callList[filename], \
559 self._funcList[filename]
560
561 @debugDecor
562 def fromJson(self, filename):
563 """read from json"""
564 with open(filename, 'r', encoding='utf-8') as file:
565 descTree = json.load(file)
566 self._cwd = descTree['cwd']
567 self._scopes = descTree['scopes']
568 self._useList = descTree['useList']
569 self._includeList = descTree['includeList']
570 self._callList = descTree['callList']
571 self._funcList = descTree['funcList']
572
573 @debugDecor
574 def toJson(self, filename):
575 """save to json"""
576 descTree = {'cwd': self._cwd,
577 'scopes': self._scopes,
578 'useList': self._useList,
579 'includeList': self._includeList,
580 'callList': self._callList,
581 'funcList': self._funcList,
582 }
583 # Order dict keys and list values
584 descTree['scopes'] = {k: sorted(descTree['scopes'][k]) for k in sorted(descTree['scopes'])}
585 for cat in ('useList', 'includeList', 'callList', 'funcList'):
586 descTree[cat] = {file: {scope: sorted(descTree[cat][file][scope])
587 for scope in sorted(descTree[cat][file])}
588 for file in sorted(descTree[cat])}
589 # Write json on disk with indentation
590 with open(filename, 'w', encoding='utf-8') as file:
591 json.dump(descTree, file, indent=2)
592
593 # No @debugDecor for this low-level method
594 def scopeToFiles(self, scopePath):
595 """
596 Return the name of the file defining the scope
597 :param scopePath: scope path to search for
598 :return: list file names in which scope is defined
599 """
600 return [filename for filename, scopes in self._scopes.items() if scopePath in scopes]
601
602 @debugDecor
603 def fileToScopes(self, filename):
604 """
605 Return the scopes contained in the file
606 :param filename: name of the file tn inspect
607 :return: list of scopes defined in the file
608 """
609 return self._scopes[filename]
610
611 @staticmethod
612 def _recurList(node, descTreePart, level, down):
613 """
614 :param node: initial node
615 :param descTreePart: 'compilation_tree' or 'execution_tree' part of a descTree object
616 :param level: number of levels (0 to get only the initial node, None to get all nodes)
617 :param down: True to get the nodes lower in the tree, False to get the upper ones
618 :return: list of nodes lower or upper tahn initial node (recursively)
619 """
620 def recur(nnn, level, currentList):
621 if down:
622 result = descTreePart.get(nnn, [])
623 else:
624 result = [item for (item, l) in descTreePart.items() if nnn in l]
625 if level is None or level > 1:
626 for res in list(result):
627 if res not in currentList: # for FORTRAN recursive calls
628 result.extend(recur(res, None if level is None else level - 1, result))
629 return result
630 return recur(node, level, [])
631
632 @debugDecor
633 def needsFile(self, filename, level=1):
634 """
635 :param filename: initial file name
636 :param level: number of levels (0 to get only the initial file, None to get all files)
637 :return: list of file names needed by the initial file (recursively)
638 """
639 return self._recurList(filename, self._compilationTree_compilationTree, level, True)
640
641 @debugDecor
642 def neededByFile(self, filename, level=1):
643 """
644 :param filename: initial file name
645 :param level: number of levels (0 to get only the initial file, None to get all files)
646 :return: list of file names that needs the initial file (recursively)
647 """
648 return self._recurList(filename, self._compilationTree_compilationTree, level, False)
649
650 @debugDecor
651 def callsScopes(self, scopePath, level=1):
652 """
653 :param scopePath: initial scope path
654 :param level: number of levels (0 to get only the initial scope path,
655 None to get all scopes)
656 :return: list of scopes called by the initial scope path (recursively)
657 """
658 return self._recurList(scopePath, self._executionTree_executionTree, level, True)
659
660 @debugDecor
661 def calledByScope(self, scopePath, level=1):
662 """
663 :param scopePath: initial scope path
664 :param level: number of levels (0 to get only the initial scope path,
665 None to get all scopes)
666 :return: list of scopes that calls the initial scope path (recursively)
667 """
668 return self._recurList(scopePath, self._executionTree_executionTree, level, False)
669
670 @debugDecor
671 def isUnderStopScopes(self, scopePath, stopScopes,
672 includeInterfaces=False, includeStopScopes=False):
673 """
674 :param scopePath: scope path to test
675 :param stopScopes: list of scopes
676 :param includeInterfaces: if True, interfaces of positive scopes are also positive
677 :param includeInterfaces: if True, scopes that are in stopScopes return True
678 :return: True if the scope path is called directly or indirectly by one of the scope
679 paths listed in stopScopes
680 """
681 scopeSplt = scopePath.split('/')
682 if includeInterfaces and len(scopeSplt) >= 2 and scopeSplt[-2].split(':')[0] == 'interface':
683 # This scope declares an interface, we look for the scope corresponding
684 # to this interface
685 scopeI = scopeSplt[-1]
686 if scopeI in self._executionTree_executionTree:
687 # The actual code for the routine exists
688 return self.isUnderStopScopes(scopeI, stopScopes,
689 includeStopScopes=includeStopScopes)
690 # No code found for this interface
691 return False
692 upperScopes = self.calledByScope(scopePath, None)
693 return (any(scp in upperScopes for scp in stopScopes) or
694 (includeStopScopes and scopePath in stopScopes))
695
696 @debugDecor
697 def plotTree(self, centralNodeList, output, plotMaxUpper, plotMaxLower, kind, frame=False):
698 """
699 Compute a dependency graph
700 :param centralNodeList: file, scope path, list of files or list of scope paths
701 :param output: output file name (.dot or .png extension)
702 :param plotMaxUpper: Maximum number of elements to plot, upper than the central element
703 :param plotMaxLower: Maximum number of elements to plot, lower than the central element
704 :param kind: must be 'compilation_tree' or 'execution_tree'
705 :param frame: True to plot a frame grouping the central nodes
706 """
707 assert kind in ('compilation_tree', 'execution_tree')
708
709 hashValues = {0: 1}
710
711 def myHash(obj):
712 objHash = hash(obj)
713 if objHash not in hashValues:
714 hashValues[0] += 1
715 hashValues[objHash] = hashValues[0]
716 return str(hashValues[objHash])
717
718 def createNode(node, label=None):
719 result = ""
720 if label is not None:
721 result += "subgraph cluster_" + myHash(node) + " {\n"
722 result += f'label="{label}"\n'
723 if kind == 'execution_tree':
724 color = 'blue' if node.split('/')[-1].split(':')[0] == 'func' else 'green'
725 else:
726 color = 'black'
727 result += myHash(node) + f' [label="{node}" color="{color}"]\n'
728 if label is not None:
729 result += "}\n"
730 return result
731
732 def createLink(file1, file2):
733 return myHash(file1) + ' -> ' + myHash(file2) + '\n'
734
735 def createCluster(nodes, label=None):
736 result = "subgraph cluster_R {\n"
737 result += "{rank=same " + (' '.join([myHash(node) for node in nodes])) + "}\n"
738 if label is not None:
739 result += f'label="{label}"\n'
740 result += "}\n"
741 return result
742
743 def add(item):
744 if item not in dot:
745 dot.append(item)
746
747 def filename(scopePath):
748 if kind == 'compilation_tree':
749 return None
750 return [f for f, l in self._scopes.items() if scopePath in l][0]
751
752 def recur(node, level, down, var):
753 if level is None or level > 0:
754 if down:
755 result = var.get(node, [])
756 else:
757 result = [f for f, l in var.items() if node in l]
758 for res in result:
759 add(createNode(res, filename(res)))
760 add(createLink(node, res) if down else createLink(res, node))
761 if level is None or level > 1:
762 recur(res, None if level is None else level - 1, down, var)
763
764 # Are all the central scopes in the same file
765 printInFrame = False
766 if kind == 'execution_tree':
767 centralScopeFilenames = []
768 for scopePath in centralNodeList:
769 centralScopeFilenames.append(filename(scopePath))
770 centralScopeFilenames = list(set(centralScopeFilenames))
771 if len(centralScopeFilenames) == 1:
772 frame = True
773 printInFrame = True
774 else:
775 printInFrame = False
776
777 # Order the tree to obtain deterministic graphs
778 var = self._executionTree_executionTree if kind == 'execution_tree' else self._compilationTree_compilationTree
779 var = {k: sorted(var[k]) for k in sorted(var)}
780
781 dot = ["digraph D {\n"]
782 if not isinstance(centralNodeList, list):
783 centralNodeList = [centralNodeList]
784 for centralNode in centralNodeList:
785 add(createNode(centralNode, None if printInFrame else filename(centralNode)))
786 recur(centralNode, plotMaxLower, True, var)
787 recur(centralNode, plotMaxUpper, False, var)
788 if frame:
789 if kind == 'compilation_tree':
790 frameText = None
791 else:
792 frameText = centralScopeFilenames[0] if printInFrame else None
793 add(createCluster(centralNodeList, frameText))
794 add("}\n")
795 dot = ''.join(dot)
796 fmt = os.path.splitext(output)[1].lower()[1:]
797 if fmt == 'dot':
798 with open(output, 'w', encoding='utf-8') as file:
799 file.write(dot)
800 else:
801 dotCommand = ['dot', '-T' + fmt, '-o', output]
802 logging.info('Dot command: %s', ' '.join(dotCommand))
803 subprocess.run(dotCommand, input=dot.encode('utf8'), check=True)
804
805 @debugDecor
806 def plotCompilTreeFromFile(self, filename, output, plotMaxUpper, plotMaxLower):
807 """
808 Compute the compilation dependency graph
809 :param filename: central file
810 :param output: output file name (.dot or .png extension)
811 :param plotMaxUpper: Maximum number of elements to plot, upper than the central element
812 :param plotMaxLower: Maximum number of elements to plot, lower than the central element
813 """
814 return self.plotTree(filename, output, plotMaxUpper, plotMaxLower, 'compilation_tree', True)
815
816 @debugDecor
817 def plotExecTreeFromScope(self, scopePath, output, plotMaxUpper, plotMaxLower):
818 """
819 Compute the execution dependency graph
820 :param scopePath: central scope path
821 :param output: output file name (.dot or .png extension)
822 :param plotMaxUpper: Maximum number of elements to plot, upper than the central element
823 :param plotMaxLower: Maximum number of elements to plot, lower than the central element
824 """
825 return self.plotTree(scopePath, output, plotMaxUpper, plotMaxLower, 'execution_tree')
826
827 @debugDecor
828 def plotCompilTreeFromScope(self, scopePath, output, plotMaxUpper, plotMaxLower):
829 """
830 Compute the compilation dependency graph
831 :param scopePath: central scope path
832 :param output: output file name (.dot or .png extension)
833 :param plotMaxUpper: Maximum number of elements to plot, upper than the central element
834 :param plotMaxLower: Maximum number of elements to plot, lower than the central element
835 """
836 return self.plotTree(self.scopeToFiles(scopePath), output, plotMaxUpper, plotMaxLower,
837 'compilation_tree')
838
839 @debugDecor
840 def plotExecTreeFromFile(self, filename, output, plotMaxUpper, plotMaxLower):
841 """
842 Compute the execution dependency graph
843 :param filename: central filename
844 :param output: output file name (.dot or .png extension)
845 :param plotMaxUpper: Maximum number of elements to plot, upper than the central element
846 :param plotMaxLower: Maximum number of elements to plot, lower than the central element
847 """
848 return self.plotTree(self.fileToScopes(filename), output, plotMaxUpper, plotMaxLower,
849 'execution_tree', True)
850
851 @debugDecor
852 def findScopeInterface(self, scopePath):
853 """
854 Return the file name containing an interface for the scope path
855 :param scopePath: scope path for which an interface is searched
856 :return: (file name, interface scope) or (None, None) if not found
857 """
858 for filename, scopes in self._scopes.items():
859 for scopeInterface in scopes:
860 if re.search(r'interface:[a-zA-Z0-9_-]*/' + scopePath + r'$', scopeInterface):
861 return filename, scopeInterface
862 return None, None
plotTree(self, centralNodeList, output, plotMaxUpper, plotMaxLower, kind, frame=False)
Definition tree.py:697
fileToScopes(self, filename)
Definition tree.py:603
needsFile(self, filename, level=1)
Definition tree.py:633
getFullContent(self)
Definition tree.py:123
scopeToFiles(self, scopePath)
Definition tree.py:594
_executionTree(self)
Definition tree.py:351
copyFromOtherTree(self, other)
Definition tree.py:163
callsScopes(self, scopePath, level=1)
Definition tree.py:651
_analyseFile(self, file)
Definition tree.py:474
calledByScope(self, scopePath, level=1)
Definition tree.py:661
popSignaled(self)
Definition tree.py:182
plotExecTreeFromScope(self, scopePath, output, plotMaxUpper, plotMaxLower)
Definition tree.py:817
toJson(self, filename)
Definition tree.py:574
_compilationTree(self)
Definition tree.py:275
update(self, file)
Definition tree.py:245
_recurList(node, descTreePart, level, down)
Definition tree.py:612
_incInScope(self)
Definition tree.py:266
fromJson(self, filename)
Definition tree.py:562
__init__(self, tree=None, descTreeFile=None, parserOptions=None, wrapH=False, verbosity=None)
Definition tree.py:88
findScopeInterface(self, scopePath)
Definition tree.py:852
plotCompilTreeFromFile(self, filename, output, plotMaxUpper, plotMaxLower)
Definition tree.py:806
plotExecTreeFromFile(self, filename, output, plotMaxUpper, plotMaxLower)
Definition tree.py:840
copyToOtherTree(self, other)
Definition tree.py:169
neededByFile(self, filename, level=1)
Definition tree.py:642
signal(self, file)
Definition tree.py:175
isUnderStopScopes(self, scopePath, stopScopes, includeInterfaces=False, includeStopScopes=False)
Definition tree.py:672
_emptyCache(self)
Definition tree.py:259
plotCompilTreeFromScope(self, scopePath, output, plotMaxUpper, plotMaxLower)
Definition tree.py:828
setFullContent(self, content)
Definition tree.py:143
conservativePYFT(filename, parserOptions, wrapH, tree=None, verbosity=None, clsPYFT=None)
Definition pyfortool.py:37