skylos 1.0.10__py3-none-any.whl → 2.5.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. skylos/__init__.py +9 -3
  2. skylos/analyzer.py +674 -168
  3. skylos/cfg_visitor.py +60 -0
  4. skylos/cli.py +719 -235
  5. skylos/codemods.py +277 -0
  6. skylos/config.py +50 -0
  7. skylos/constants.py +78 -0
  8. skylos/gatekeeper.py +147 -0
  9. skylos/linter.py +18 -0
  10. skylos/rules/base.py +20 -0
  11. skylos/rules/danger/calls.py +119 -0
  12. skylos/rules/danger/danger.py +157 -0
  13. skylos/rules/danger/danger_cmd/cmd_flow.py +75 -0
  14. skylos/rules/danger/danger_fs/__init__.py +0 -0
  15. skylos/rules/danger/danger_fs/path_flow.py +79 -0
  16. skylos/rules/danger/danger_net/__init__.py +0 -0
  17. skylos/rules/danger/danger_net/ssrf_flow.py +80 -0
  18. skylos/rules/danger/danger_sql/__init__.py +0 -0
  19. skylos/rules/danger/danger_sql/sql_flow.py +245 -0
  20. skylos/rules/danger/danger_sql/sql_raw_flow.py +96 -0
  21. skylos/rules/danger/danger_web/__init__.py +0 -0
  22. skylos/rules/danger/danger_web/xss_flow.py +170 -0
  23. skylos/rules/danger/taint.py +110 -0
  24. skylos/rules/quality/__init__.py +0 -0
  25. skylos/rules/quality/complexity.py +95 -0
  26. skylos/rules/quality/logic.py +96 -0
  27. skylos/rules/quality/nesting.py +101 -0
  28. skylos/rules/quality/structure.py +99 -0
  29. skylos/rules/secrets.py +325 -0
  30. skylos/server.py +554 -0
  31. skylos/visitor.py +502 -90
  32. skylos/visitors/__init__.py +0 -0
  33. skylos/visitors/framework_aware.py +437 -0
  34. skylos/visitors/test_aware.py +74 -0
  35. skylos-2.5.2.dist-info/METADATA +21 -0
  36. skylos-2.5.2.dist-info/RECORD +42 -0
  37. {skylos-1.0.10.dist-info → skylos-2.5.2.dist-info}/WHEEL +1 -1
  38. {skylos-1.0.10.dist-info → skylos-2.5.2.dist-info}/top_level.txt +0 -1
  39. skylos-1.0.10.dist-info/METADATA +0 -8
  40. skylos-1.0.10.dist-info/RECORD +0 -21
  41. test/compare_tools.py +0 -604
  42. test/diagnostics.py +0 -364
  43. test/sample_repo/app.py +0 -13
  44. test/sample_repo/sample_repo/commands.py +0 -81
  45. test/sample_repo/sample_repo/models.py +0 -122
  46. test/sample_repo/sample_repo/routes.py +0 -89
  47. test/sample_repo/sample_repo/utils.py +0 -36
  48. test/test_skylos.py +0 -456
  49. test/test_visitor.py +0 -220
  50. {test → skylos/rules}/__init__.py +0 -0
  51. {test/sample_repo → skylos/rules/danger}/__init__.py +0 -0
  52. {test/sample_repo/sample_repo → skylos/rules/danger/danger_cmd}/__init__.py +0 -0
  53. {skylos-1.0.10.dist-info → skylos-2.5.2.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,95 @@
1
+ import ast
2
+ from pathlib import Path
3
+ from skylos.rules.base import SkylosRule
4
+
5
+ RULE_ID = "SKY-Q301"
6
+
7
+ _COMPLEX_NODES = (
8
+ ast.If,
9
+ ast.For,
10
+ ast.While,
11
+ ast.Try,
12
+ ast.With,
13
+ ast.ExceptHandler,
14
+ ast.BoolOp,
15
+ ast.IfExp,
16
+ ast.comprehension,
17
+ )
18
+
19
+
20
+ def _func_complexity(node):
21
+ c = 1
22
+ for child in ast.walk(node):
23
+ if isinstance(child, _COMPLEX_NODES):
24
+ if isinstance(child, ast.BoolOp):
25
+ c += len(child.values) - 1
26
+ else:
27
+ c += 1
28
+ return c
29
+
30
+
31
+ def _func_length(node):
32
+ start = getattr(node, "lineno", None)
33
+ end = getattr(node, "end_lineno", None)
34
+
35
+ if start is None:
36
+ return 0
37
+
38
+ if end is None:
39
+ end = start
40
+ for child in ast.walk(node):
41
+ ln = getattr(child, "lineno", None)
42
+ if ln is not None and ln > end:
43
+ end = ln
44
+
45
+ return max(end - start + 1, 0)
46
+
47
+
48
+ class ComplexityRule(SkylosRule):
49
+ rule_id = "SKY-Q301"
50
+ name = "Cyclomatic Complexity"
51
+
52
+ def __init__(self, threshold=10):
53
+ self.threshold = threshold
54
+
55
+ def visit_node(self, node, context):
56
+ if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
57
+ return None
58
+
59
+ complexity = _func_complexity(node)
60
+
61
+ if complexity < self.threshold:
62
+ return None
63
+
64
+ if complexity < 15:
65
+ severity = "WARN"
66
+ elif complexity < 25:
67
+ severity = "HIGH"
68
+ else:
69
+ severity = "CRITICAL"
70
+
71
+ length = _func_length(node)
72
+ mod = context.get("mod", "")
73
+
74
+ if mod:
75
+ func_name = f"{mod}.{node.name}"
76
+ else:
77
+ func_name = node.name
78
+
79
+ return [
80
+ {
81
+ "rule_id": self.rule_id,
82
+ "kind": "complexity",
83
+ "severity": severity,
84
+ "type": "function",
85
+ "name": func_name,
86
+ "simple_name": node.name,
87
+ "value": complexity,
88
+ "threshold": self.threshold,
89
+ "length": length,
90
+ "message": f"Function is complex (McCabe={complexity}). Consider splitting loops/branches.",
91
+ "file": context.get("filename"),
92
+ "basename": Path(context.get("filename", "")).name,
93
+ "line": node.lineno,
94
+ }
95
+ ]
@@ -0,0 +1,96 @@
1
+ import ast
2
+ from pathlib import Path
3
+ from skylos.rules.base import SkylosRule
4
+
5
+ class MutableDefaultRule(SkylosRule):
6
+ rule_id = "SKY-L001"
7
+ name = "Mutable Default Argument"
8
+
9
+ def visit_node(self, node, context):
10
+ if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
11
+ return None
12
+ findings = []
13
+
14
+ kw_defaults_filtered = []
15
+ for d in node.args.kw_defaults:
16
+ if d:
17
+ kw_defaults_filtered.append(d)
18
+
19
+ for default in node.args.defaults + kw_defaults_filtered:
20
+ if isinstance(default, (ast.List, ast.Dict, ast.Set)):
21
+ findings.append({
22
+ "rule_id": self.rule_id,
23
+ "kind": "logic",
24
+ "severity": "HIGH",
25
+ "type": "function",
26
+ "name": node.name,
27
+ "simple_name": node.name,
28
+ "value": "mutable",
29
+ "threshold": 0,
30
+ "message": "Mutable default argument detected (List/Dict/Set). This causes state leaks.",
31
+ "file": context.get("filename"),
32
+ "basename": Path(context.get("filename", "")).name,
33
+ "line": default.lineno,
34
+ "col": default.col_offset
35
+ })
36
+ if findings:
37
+ return findings
38
+ else:
39
+ return None
40
+
41
+ class BareExceptRule(SkylosRule):
42
+ rule_id = "SKY-L002"
43
+ name = "Bare Except Block"
44
+
45
+ def visit_node(self, node, context):
46
+ if isinstance(node, ast.ExceptHandler) and node.type is None:
47
+ return [{
48
+ "rule_id": self.rule_id,
49
+ "kind": "logic",
50
+ "severity": "MEDIUM",
51
+ "type": "block",
52
+ "name": "except",
53
+ "simple_name": "except",
54
+ "value": "bare",
55
+ "threshold": 0,
56
+ "message": "Bare 'except:' block swallows SystemExit and other critical errors.",
57
+ "file": context.get("filename"),
58
+ "basename": Path(context.get("filename", "")).name,
59
+ "line": node.lineno,
60
+ "col": node.col_offset
61
+ }]
62
+ return None
63
+
64
+ class DangerousComparisonRule(SkylosRule):
65
+ rule_id = "SKY-L003"
66
+ name = "Dangerous Comparison"
67
+
68
+ def visit_node(self, node, context):
69
+ if not isinstance(node, ast.Compare):
70
+ return None
71
+
72
+ findings = []
73
+ for op, comparator in zip(node.ops, node.comparators):
74
+ if isinstance(op, (ast.Eq, ast.NotEq)):
75
+ if isinstance(comparator, ast.Constant):
76
+ val = comparator.value
77
+ if val is True or val is False or val is None:
78
+ findings.append({
79
+ "rule_id": self.rule_id,
80
+ "kind": "logic",
81
+ "severity": "LOW",
82
+ "type": "comparison",
83
+ "name": "==",
84
+ "simple_name": "==",
85
+ "value": str(comparator.value),
86
+ "threshold": 0,
87
+ "message": f"Comparison to {comparator.value} should use 'is' or 'is not'.",
88
+ "file": context.get("filename"),
89
+ "basename": Path(context.get("filename", "")).name,
90
+ "line": node.lineno,
91
+ "col": node.col_offset
92
+ })
93
+ if findings:
94
+ return findings
95
+ else:
96
+ return None
@@ -0,0 +1,101 @@
1
+ from __future__ import annotations
2
+ import ast
3
+ from pathlib import Path
4
+ from skylos.rules.base import SkylosRule
5
+
6
+ RULE_ID = "SKY-Q302"
7
+
8
+ NEST_NODES = (ast.If, ast.For, ast.While, ast.Try, ast.With)
9
+
10
+
11
+ def _max_depth(nodes, depth=0):
12
+ max_depth = depth
13
+ for node in nodes:
14
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
15
+ continue
16
+
17
+ if isinstance(node, NEST_NODES):
18
+ branches = []
19
+ if isinstance(node, ast.If):
20
+ branches.append(node.body)
21
+ if node.orelse:
22
+ branches.append(node.orelse)
23
+
24
+ elif isinstance(node, (ast.For, ast.While)):
25
+ branches.append(node.body)
26
+ if node.orelse:
27
+ branches.append(node.orelse)
28
+
29
+ elif isinstance(node, ast.With):
30
+ branches.append(node.body)
31
+
32
+ elif isinstance(node, ast.Try):
33
+ branches.append(node.body)
34
+ for handler in node.handlers:
35
+ branches.append(handler.body)
36
+ if node.orelse:
37
+ branches.append(node.orelse)
38
+ if node.finalbody:
39
+ branches.append(node.finalbody)
40
+
41
+ for branch in branches:
42
+ max_depth = max(max_depth, _max_depth(branch, depth + 1))
43
+
44
+ return max_depth
45
+
46
+
47
+ def _physical_length(node):
48
+ start = getattr(node, "lineno", 0)
49
+ end = getattr(node, "end_lineno", start)
50
+ return max(end - start + 1, 0)
51
+
52
+
53
+ class NestingRule(SkylosRule):
54
+ rule_id = "SKY-Q302"
55
+ name = "Deep Nesting"
56
+
57
+ def __init__(self, threshold=3):
58
+ self.threshold = threshold
59
+
60
+ def visit_node(self, node, context):
61
+ if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
62
+ return None
63
+
64
+ depth = _max_depth(node.body, 0)
65
+
66
+ if depth <= self.threshold:
67
+ return None
68
+
69
+ if depth <= self.threshold + 2:
70
+ severity = "MEDIUM"
71
+ elif depth <= self.threshold + 5:
72
+ severity = "HIGH"
73
+ else:
74
+ severity = "CRITICAL"
75
+
76
+ physical_len = _physical_length(node)
77
+ mod = context.get("mod", "")
78
+
79
+ if mod:
80
+ func_name = f"{mod}.{node.name}"
81
+ else:
82
+ func_name = node.name
83
+
84
+ return [
85
+ {
86
+ "rule_id": self.rule_id,
87
+ "kind": "nesting",
88
+ "severity": severity,
89
+ "type": "function",
90
+ "name": func_name,
91
+ "simple_name": node.name,
92
+ "file": context.get("filename"),
93
+ "basename": Path(context.get("filename", "")).name,
94
+ "line": node.lineno,
95
+ "metric": "max_nesting",
96
+ "value": depth,
97
+ "threshold": self.threshold,
98
+ "length": physical_len,
99
+ "message": f"Nesting depth of {depth} exceeds threshold of {self.threshold}. Consider using early returns.",
100
+ }
101
+ ]
@@ -0,0 +1,99 @@
1
+ import ast
2
+ from skylos.rules.base import SkylosRule
3
+ from pathlib import Path
4
+
5
+
6
+ class ArgCountRule(SkylosRule):
7
+ rule_id = "SKY-C303"
8
+ name = "Too Many Arguments"
9
+
10
+ def __init__(self, max_args=5):
11
+ self.max_args = max_args
12
+
13
+ def visit_node(self, node, context):
14
+ if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
15
+ return None
16
+
17
+ args = node.args.args
18
+ clean_args = []
19
+ for a in args:
20
+ if a.arg not in ("self", "cls"):
21
+ clean_args.append(a)
22
+
23
+ total_count = len(clean_args) + len(node.args.kwonlyargs)
24
+
25
+ if total_count <= self.max_args:
26
+ return None
27
+
28
+ mod = context.get("mod", "")
29
+ if mod:
30
+ func_name = f"{mod}.{node.name}"
31
+ else:
32
+ func_name = node.name
33
+
34
+ return [
35
+ {
36
+ "rule_id": self.rule_id,
37
+ "kind": "structure",
38
+ "type": "function",
39
+ "name": func_name,
40
+ "simple_name": node.name,
41
+ "value": total_count,
42
+ "threshold": self.max_args,
43
+ "severity": "MEDIUM",
44
+ "message": f"Function has {total_count} arguments (limit: {self.max_args}). Refactor.",
45
+ "file": context.get("filename"),
46
+ "basename": Path(context.get("filename", "")).name,
47
+ "line": node.lineno,
48
+ "col": node.col_offset,
49
+ }
50
+ ]
51
+
52
+
53
+ class FunctionLengthRule(SkylosRule):
54
+ rule_id = "SKY-C304"
55
+ name = "Function Too Long"
56
+
57
+ def __init__(self, max_lines=50):
58
+ self.max_lines = max_lines
59
+
60
+ def visit_node(self, node, context):
61
+ if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
62
+ return None
63
+
64
+ start = getattr(node, "lineno", 0)
65
+ end = getattr(node, "end_lineno", start)
66
+ physical_length = max(end - start + 1, 0)
67
+
68
+ if physical_length <= self.max_lines:
69
+ return None
70
+
71
+ if physical_length < 100:
72
+ severity = "MEDIUM"
73
+ else:
74
+ severity = "HIGH"
75
+
76
+ mod = context.get("mod", "")
77
+
78
+ if mod:
79
+ func_name = f"{mod}.{node.name}"
80
+ else:
81
+ func_name = node.name
82
+
83
+ return [
84
+ {
85
+ "rule_id": self.rule_id,
86
+ "kind": "structure",
87
+ "type": "function",
88
+ "name": func_name,
89
+ "simple_name": node.name,
90
+ "value": physical_length,
91
+ "threshold": self.max_lines,
92
+ "severity": severity,
93
+ "message": f"Function is {physical_length} lines long (limit: {self.max_lines}). It is too big.",
94
+ "file": context.get("filename"),
95
+ "basename": Path(context.get("filename", "")).name,
96
+ "line": node.lineno,
97
+ "col": node.col_offset,
98
+ }
99
+ ]