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