2This module contains the Tree class to browse the tree
11from functools
import wraps
18def updateTree(method='file'):
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
25 - 'signal': analyse files (if any) signaled using
26 the signal method of the tree object
28 assert method
in (
'file',
'scan',
'signal')
32 def wrapper(self, *args, **kwargs):
33 result = func(self, *args, **kwargs)
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())
50 Class to browse the Tree
52 def __init__(self, tree=None, descTreeFile=None,
53 parserOptions=None, wrapH=False,
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 parserOptions, wrapH: see the PYFT class
59 :param verbosity: if not None, sets the verbosity level
62 self.
_tree = []
if tree
is None else tree
72 self.
_cwd = os.getcwd()
82 if descTreeFile
is not None and os.path.exists(descTreeFile):
84 elif tree
is not None:
86 if descTreeFile
is not None:
91 :return: a dict containing the full content of the instance
93 return {
'tree': self.
_tree,
111 :param content: Fill the current instance with this dict
113 self.
_tree = content[
'tree']
116 self.
_wrapH = content[
'wrapH']
118 self.
_cwd = content[
'cwd']
119 self.
_scopes = content[
'scopes']
131 Sets self to be a copy of other
137 Sets other to be a copy of self
143 Method used for signaling a modified file which needs to be analized
144 :param filename: file name or PYFTscope object
150 :return: the list of file signaled for update and empties the list
158 :return: the list of analysez file names
160 return list(self.
_scopes.keys())
164 """Is the Tree object valid"""
169 """List of directories"""
175 :param tree: list of directories composing the tree or None
176 :return: list of directories and subdirectories
179 if self.
tree is not None:
180 for tDir
in self.
tree:
181 result += glob.glob(tDir +
'/**/', recursive=
True)
187 :param tree: list of directories composing the tree or None
188 :return: list of directories and subdirectories
191 for tDir
in self.
tree:
192 for filename
in glob.glob(tDir +
'/**/*', recursive=
True):
193 if os.path.splitext(filename)[1]
not in (
'',
'.json',
'.fypp',
'.txt'):
195 filenames.append(filename)
201 Builds the self._* variable
210 Updates the object when a file has changed
211 :param file: name of the file (or list of names) with updated content
215 if not isinstance(file, (list, set)):
223 """Empties cached values"""
230 """Fill and return the self._cacheIncInScope cached value"""
238 def _compilationTree(self):
239 """Fill and return the self._cacheCompilationTree cached value"""
244 for filename, incScopePaths
in self.
_includeList.items():
246 for scopePath, incList
in incScopePaths.items():
256 if os.path.normpath(inc) == os.path.normpath(file):
259 elif ((
not os.path.isabs(file))
and
260 os.path.realpath(inc) == os.path.realpath(os.path.join(
261 os.path.dirname(inc), file))):
265 elif os.path.basename(inc) == os.path.basename(file):
267 basename.append(file)
269 same = subdir = basename = []
271 subdir = basename = []
272 if len(basename) > 1:
276 incFilename = same[0]
277 elif len(subdir) > 0:
278 incFilename = subdir[0]
279 elif len(basename) > 0:
280 incFilename = basename[0]
290 for filename, uList
in self.
_useList.items():
292 for modName, _
in [use
for li
in uList.values()
for use
in li]:
293 moduleScopePath =
'module:' + modName
296 for file, scopes
in self.
_scopes.items():
297 if moduleScopePath
in scopes:
302 logging.info(
'Several or none file containing the scope path ' +
303 '%s have been found for file %s',
304 moduleScopePath, filename)
314 def _executionTree(self):
315 """Fill and return the self._cacheCompilationTree cached value"""
319 allScopes = [scopePath
for _, l
in self.
_scopes.items()
for scopePath
in l]
322 for filename, callScopes
in progList.items():
324 for scopePath, cList
in callScopes.items():
326 for call
in set(cList):
331 foundInSameScope = []
334 for kind
in (canonicKind,
'interface'):
336 uList = [self.
_useList[filename][sc]
338 if (sc == scopePath
or scopePath.startswith(sc +
'/'))]
339 for modName, only
in [use
for li
in uList
for use
in li]:
340 moduleScope =
'module:' + modName
341 callScope = moduleScope +
'/' + kind +
':' + call
344 if call
in only
and callScope
in allScopes:
345 foundInUse.append(callScope)
348 for _, scopes
in self.
_scopes.items():
349 if callScope
in scopes:
350 foundInUse.append(callScope)
353 callScope = kind +
':' + call
354 for _, scopes
in self.
_scopes.items():
355 if callScope
in scopes:
356 foundElsewhere.append(callScope)
359 callScope = kind +
':' + call
361 if callScope
in self.
_scopes[incFile]:
362 foundInInclude.append(callScope)
365 callScope = scopePath +
'/' + kind +
':' + call
366 if callScope
in self.
_scopes[filename]:
367 foundInContains.append(callScope)
371 callScope = scopePath.rsplit(
'/', 1)[0] +
'/' + kind + \
374 callScope = kind +
':' + call
375 if callScope
in self.
_scopes[filename]:
376 foundInSameScope.append(callScope)
379 foundInUse = list(set(foundInUse))
380 if len(foundInUse + foundInInclude +
381 foundInContains + foundInSameScope) > 1:
382 logging.error(
'Several definition of the program unit found for '
383 '%s called in %s:', call, scopePath)
384 logging.error(
' found %i time(s) in USE statements',
386 logging.error(
' found %i time(s) in include files',
388 logging.error(
' found %i time(s) in CONTAINS block',
389 len(foundInContains))
390 logging.error(
' found %i time(s) in the same scope',
391 len(foundInSameScope))
393 elif len(foundInUse + foundInInclude +
394 foundInContains + foundInSameScope) == 1:
395 rr = (foundInUse + foundInInclude +
396 foundInContains + foundInSameScope)[0]
397 if canonicKind !=
'func' or rr
in allScopes:
399 elif len(foundElsewhere) > 1:
400 logging.info(
'Several definition of the program unit found for '
401 '%s called in %s', call, scopePath)
402 elif len(foundElsewhere) == 1:
405 if canonicKind !=
'func':
406 logging.info(
'No definition of the program unit found for '
407 '%s called in %s', call, scopePath)
413 for item
in list(execList):
414 itemSplt = item.split(
'/')[-1].split(
':')
415 if itemSplt[0] ==
'interface' and itemSplt[1] !=
'--UNKNOWN--':
417 filenames = [k
for (k, v)
in self.
_scopes.items()
if item
in v]
418 if len(filenames) == 1:
420 execList.remove(item)
421 for sub
in [sub
for sub
in self.
_scopes[filenames[0]]
422 if sub.startswith(item +
'/')]:
423 subscopeIn = sub.rsplit(
'/', 2)[0] +
'/' + sub.split(
'/')[-1]
424 if subscopeIn
in self.
_scopes[filenames[0]]:
426 execList.append(subscopeIn)
428 execList.append(sub.split(
'/')[-1])
439 :param file: Name of the file to explore, or PYFTscope object
440 :return: dict of use, include, call, function and scope list
442 def extractString(text):
444 if text[0]
in (
'"',
"'"):
445 assert text[-1] == text[0]
453 filename = pft.getFileName()
460 filename = filename[2:]
if filename.startswith(
'./')
else filename
468 scopes = pft.getScopes()
471 self.
_scopes[filename].append(scope.path)
473 if scope.path.split(
'/')[-1].split(
':')[0] ==
'interface':
474 for name
in [n2name(nodeN).upper()
475 for moduleproc
in scope.findall(
'./{*}procedure-stmt')
476 for nodeN
in moduleproc.findall(
'./{*}module-procedure-N-LT/' +
479 if re.search(scope.path.rsplit(
'/', 1)[0] +
'/[a-zA-Z]*:' + name,
481 self.
_scopes[filename].append(scope.path +
'/' +
482 sc.path.split(
'/')[-1])
489 [file.text
for file
in scope.findall(
'.//{*}include/{*}filename')]
491 [extractString(file.text)
492 for file
in scope.findall(
'.//{*}include/{*}filename/{*}S')])
496 self.
_useList[filename][scope.path] = []
497 for use
in scope.findall(
'.//{*}use-stmt'):
498 modName = n2name(use.find(
'./{*}module-N/{*}N')).upper()
499 only = [n2name(n).upper()
for n
in use.findall(
'.//{*}use-N//{*}N')]
500 self.
_useList[filename][scope.path].append((modName, only))
505 list(set(n2name(call.find(
'./{*}procedure-designator/{*}named-E/{*}N')).upper()
506 for call
in scope.findall(
'.//{*}call-stmt')))
508 self.
_funcList[filename][scope.path] = set()
509 for name
in [n2name(call.find(
'./{*}N')).upper()
510 for call
in scope.findall(
'.//{*}named-E/{*}R-LT/{*}parens-R/../..')]:
512 var = scope.varList.findVar(name)
513 if var
is None or var[
'as']
is None:
514 self.
_funcList[filename][scope.path].add(name)
527 with open(filename,
'r', encoding=
'utf-8')
as file:
528 descTree = json.load(file)
529 self.
_cwd = descTree[
'cwd']
530 self.
_scopes = descTree[
'scopes']
539 descTree = {
'cwd': self.
_cwd,
547 descTree[
'scopes'] = {k: sorted(descTree[
'scopes'][k])
for k
in sorted(descTree[
'scopes'])}
548 for cat
in (
'useList',
'includeList',
'callList',
'funcList'):
549 descTree[cat] = {file: {scope: sorted(descTree[cat][file][scope])
550 for scope
in sorted(descTree[cat][file])}
551 for file
in sorted(descTree[cat])}
553 with open(filename,
'w', encoding=
'utf-8')
as file:
554 json.dump(descTree, file, indent=2)
559 Return the name of the file defining the scope
560 :param scopePath: scope path to search for
561 :return: list file names in which scope is defined
563 return [filename
for filename, scopes
in self.
_scopes.items()
if scopePath
in scopes]
568 Return the scopes contained in the file
569 :param filename: name of the file tn inspect
570 :return: list of scopes defined in the file
577 :param node: initial node
578 :param descTreePart: 'compilation_tree' or 'execution_tree' part of a descTree object
579 :param level: number of levels (0 to get only the initial node, None to get all nodes)
580 :param down: True to get the nodes lower in the tree, False to get the upper ones
581 :return: list of nodes lower or upper tahn initial node (recursively)
583 def recur(nnn, level, currentList):
585 result = descTreePart.get(nnn, [])
587 result = [item
for (item, l)
in descTreePart.items()
if nnn
in l]
588 if level
is None or level > 1:
589 for res
in list(result):
590 if res
not in currentList:
591 result.extend(recur(res,
None if level
is None else level - 1, result))
593 return recur(node, level, [])
598 :param filename: initial file name
599 :param level: number of levels (0 to get only the initial file, None to get all files)
600 :return: list of file names needed by the initial file (recursively)
607 :param filename: initial file name
608 :param level: number of levels (0 to get only the initial file, None to get all files)
609 :return: list of file names that needs the initial file (recursively)
616 :param scopePath: initial scope path
617 :param level: number of levels (0 to get only the initial scope path,
618 None to get all scopes)
619 :return: list of scopes called by the initial scope path (recursively)
626 :param scopePath: initial scope path
627 :param level: number of levels (0 to get only the initial scope path,
628 None to get all scopes)
629 :return: list of scopes that calls the initial scope path (recursively)
635 includeInterfaces=False, includeStopScopes=False):
637 :param scopePath: scope path to test
638 :param stopScopes: list of scopes
639 :param includeInterfaces: if True, interfaces of positive scopes are also positive
640 :param includeInterfaces: if True, scopes that are in stopScopes return True
641 :return: True if the scope path is called directly or indirectly by one of the scope
642 paths listed in stopScopes
644 scopeSplt = scopePath.split(
'/')
645 if includeInterfaces
and len(scopeSplt) >= 2
and scopeSplt[-2].split(
':')[0] ==
'interface':
648 scopeI = scopeSplt[-1]
652 includeStopScopes=includeStopScopes)
656 return (any(scp
in upperScopes
for scp
in stopScopes)
or
657 (includeStopScopes
and scopePath
in stopScopes))
660 def plotTree(self, centralNodeList, output, plotMaxUpper, plotMaxLower, kind, frame=False):
662 Compute a dependency graph
663 :param centralNodeList: file, scope path, list of files or list of scope paths
664 :param output: output file name (.dot or .png extension)
665 :param plotMaxUpper: Maximum number of elements to plot, upper than the central element
666 :param plotMaxLower: Maximum number of elements to plot, lower than the central element
667 :param kind: must be 'compilation_tree' or 'execution_tree'
668 :param frame: True to plot a frame grouping the central nodes
670 assert kind
in (
'compilation_tree',
'execution_tree')
676 if objHash
not in hashValues:
678 hashValues[objHash] = hashValues[0]
679 return str(hashValues[objHash])
681 def createNode(node, label=None):
683 if label
is not None:
684 result +=
"subgraph cluster_" + myHash(node) +
" {\n"
685 result += f
'label="{label}"\n'
686 if kind ==
'execution_tree':
687 color =
'blue' if node.split(
'/')[-1].split(
':')[0] ==
'func' else 'green'
690 result += myHash(node) + f
' [label="{node}" color="{color}"]\n'
691 if label
is not None:
695 def createLink(file1, file2):
696 return myHash(file1) +
' -> ' + myHash(file2) +
'\n'
698 def createCluster(nodes, label=None):
699 result =
"subgraph cluster_R {\n"
700 result +=
"{rank=same " + (
' '.join([myHash(node)
for node
in nodes])) +
"}\n"
701 if label
is not None:
702 result += f
'label="{label}"\n'
710 def filename(scopePath):
711 if kind ==
'compilation_tree':
713 return [f
for f, l
in self.
_scopes.items()
if scopePath
in l][0]
715 def recur(node, level, down, var):
716 if level
is None or level > 0:
718 result = var.get(node, [])
720 result = [f
for f, l
in var.items()
if node
in l]
722 add(createNode(res, filename(res)))
723 add(createLink(node, res)
if down
else createLink(res, node))
724 if level
is None or level > 1:
725 recur(res,
None if level
is None else level - 1, down, var)
729 if kind ==
'execution_tree':
730 centralScopeFilenames = []
731 for scopePath
in centralNodeList:
732 centralScopeFilenames.append(filename(scopePath))
733 centralScopeFilenames = list(set(centralScopeFilenames))
734 if len(centralScopeFilenames) == 1:
742 var = {k: sorted(var[k])
for k
in sorted(var)}
744 dot = [
"digraph D {\n"]
745 if not isinstance(centralNodeList, list):
746 centralNodeList = [centralNodeList]
747 for centralNode
in centralNodeList:
748 add(createNode(centralNode,
None if printInFrame
else filename(centralNode)))
749 recur(centralNode, plotMaxLower,
True, var)
750 recur(centralNode, plotMaxUpper,
False, var)
752 if kind ==
'compilation_tree':
755 frameText = centralScopeFilenames[0]
if printInFrame
else None
756 add(createCluster(centralNodeList, frameText))
759 fmt = os.path.splitext(output)[1].lower()[1:]
761 with open(output,
'w', encoding=
'utf-8')
as file:
764 dotCommand = [
'dot',
'-T' + fmt,
'-o', output]
765 logging.info(
'Dot command: %s',
' '.join(dotCommand))
766 subprocess.run(dotCommand, input=dot.encode(
'utf8'), check=
True)
771 Compute the compilation dependency graph
772 :param filename: central file
773 :param output: output file name (.dot or .png extension)
774 :param plotMaxUpper: Maximum number of elements to plot, upper than the central element
775 :param plotMaxLower: Maximum number of elements to plot, lower than the central element
777 return self.
plotTree(filename, output, plotMaxUpper, plotMaxLower,
'compilation_tree',
True)
782 Compute the execution dependency graph
783 :param scopePath: central scope path
784 :param output: output file name (.dot or .png extension)
785 :param plotMaxUpper: Maximum number of elements to plot, upper than the central element
786 :param plotMaxLower: Maximum number of elements to plot, lower than the central element
788 return self.
plotTree(scopePath, output, plotMaxUpper, plotMaxLower,
'execution_tree')
793 Compute the compilation dependency graph
794 :param scopePath: central scope path
795 :param output: output file name (.dot or .png extension)
796 :param plotMaxUpper: Maximum number of elements to plot, upper than the central element
797 :param plotMaxLower: Maximum number of elements to plot, lower than the central element
805 Compute the execution dependency graph
806 :param filename: central filename
807 :param output: output file name (.dot or .png extension)
808 :param plotMaxUpper: Maximum number of elements to plot, upper than the central element
809 :param plotMaxLower: Maximum number of elements to plot, lower than the central element
812 'execution_tree',
True)
817 Return the file name containing an interface for the scope path
818 :param scopePath: scope path for which an interface is searched
819 :return: (file name, interface scope) or (None, None) if not found
821 for filename, scopes
in self.
_scopes.items():
822 for scopeInterface
in scopes:
823 if re.search(
r'interface:[a-zA-Z0-9_-]*/' + scopePath, scopeInterface):
824 return filename, scopeInterface