ntopng/tools/circular_deps.py
2024-02-13 20:45:12 +01:00

110 lines
4.3 KiB
Python
Executable file

#!/usr/bin/env python3
# (C) 2013-21 - ntop.org
# Author: Simone Mainardi <mainardi@ntop.org>
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
import argparse
import re
import sys
from pathlib import Path
import networkx as nx
__version__ = '1.0.0'
class CircularDeps(object):
def __init__(self):
self._G = None # The graph
self._requires = [] # All the dependencies read from files
def _build_requires(self):
# Iterate over all lua files, both community and pro
for available_path in ['./../scripts', './../pro/scripts']:
for path in Path(available_path).rglob('*.lua'):
# Exclude backup files (e.g., starting with #)
if path.name.startswith('.') or path.name.startswith('#') or path.name.startswith('~'):
continue
# Search for requires
with path.open('r', encoding="utf-8") as fid:
for line in fid:
res = [
# require without assignment
re.search(r'^require.*\"(.*?)\"' ,line),
re.search(r'^require.*\'(.*?)\'' ,line),
# require with assignment to local variables
re.search(r'^local.*require.*\"(.*?)\"' ,line),
re.search(r'^local.*require.*\'(.*?)\'' ,line),
# require with assignment to global variables (also ignore comments)
re.search(r'^[^\s\-].*require.*\"(.*?)\"' ,line),
re.search(r'^[^\s\-].*require.*\'(.*?)\'' ,line),
]
# Add requires
for r in res:
if r and r.group:
# Prepare node names
require_node_name = r.group(1)
# If the file is under the REST API, an extra folder is added to the node name
# to avoid clashes with names, .e.g., delete/endpoints.lua with get/endpoints.lua
file_node_name = '/'.join(path.parts['/rest/' in str(path) and -2 or -1:])
file_node_name = file_node_name.replace('.lua', '')
self._requires.append((file_node_name, require_node_name))
def _build_graph(self):
self._G = nx.DiGraph()
self._G.add_edges_from(self._requires)
def _find_cycles(self):
all_cycles = []
# For each node
for n in self._G.nodes():
try:
# Search a cycle with Depth-first traversal
cycle = list(nx.find_cycle(self._G, n))
# If cycle not already found, add it to the results list
found = False
for c in all_cycles:
if cycle == c:
found = True
break
if not found:
all_cycles.append(cycle)
# When no cycles are found for the current node, an exception is thrown and it is safe to ignore it
except nx.exception.NetworkXNoCycle:
pass
print(all_cycles)
def check(self):
self._build_requires()
self._build_graph()
self._find_cycles()
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Find cycles in Lua dependencies. Re-run after removing cycles until no other cycle shows up.')
parser.add_argument('-V', '--version', action='version', version='%(prog)s v' + sys.modules[__name__].__version__)
tf = CircularDeps()
tf.check()