codinsight 0.0.6__tar.gz → 0.0.7__tar.gz
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.
- {codinsight-0.0.6 → codinsight-0.0.7}/PKG-INFO +1 -1
- {codinsight-0.0.6 → codinsight-0.0.7}/pyproject.toml +1 -1
- codinsight-0.0.7/src/code_insight/__init__.py +26 -0
- codinsight-0.0.7/src/code_insight/code_analysis/algorithm.py +189 -0
- codinsight-0.0.7/src/code_insight/code_analysis/complexity.py +212 -0
- codinsight-0.0.7/src/code_insight/code_analysis/readability.py +272 -0
- codinsight-0.0.7/src/code_insight/code_analysis/redundancy.py +220 -0
- {codinsight-0.0.6 → codinsight-0.0.7}/src/code_insight/core.py +21 -0
- codinsight-0.0.7/src/code_insight/trend_analysis/trend_analysis.py +104 -0
- {codinsight-0.0.6 → codinsight-0.0.7}/src/codinsight.egg-info/PKG-INFO +1 -1
- {codinsight-0.0.6 → codinsight-0.0.7}/src/codinsight.egg-info/SOURCES.txt +4 -0
- codinsight-0.0.6/src/code_insight/__init__.py +0 -13
- codinsight-0.0.6/src/code_insight/trend_analysis/trend_analysis.py +0 -42
- {codinsight-0.0.6 → codinsight-0.0.7}/README.md +0 -0
- {codinsight-0.0.6 → codinsight-0.0.7}/setup.cfg +0 -0
- {codinsight-0.0.6 → codinsight-0.0.7}/src/code_insight/code_analysis/__init__.py +0 -0
- {codinsight-0.0.6 → codinsight-0.0.7}/src/code_insight/code_analysis/abstract.py +0 -0
- {codinsight-0.0.6 → codinsight-0.0.7}/src/code_insight/code_analysis/struct.py +0 -0
- {codinsight-0.0.6 → codinsight-0.0.7}/src/code_insight/code_analysis/style.py +0 -0
- {codinsight-0.0.6 → codinsight-0.0.7}/src/code_insight/py.typed +0 -0
- {codinsight-0.0.6 → codinsight-0.0.7}/src/code_insight/trend_analysis/__init__.py +0 -0
- {codinsight-0.0.6 → codinsight-0.0.7}/src/codinsight.egg-info/dependency_links.txt +0 -0
- {codinsight-0.0.6 → codinsight-0.0.7}/src/codinsight.egg-info/requires.txt +0 -0
- {codinsight-0.0.6 → codinsight-0.0.7}/src/codinsight.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from .code_analysis.algorithm import Algorithm, AlgorithmAnalysisResult
|
|
2
|
+
from .code_analysis.complexity import Complexity, ComplexityAnalysisResult
|
|
3
|
+
from .code_analysis.readability import Readability, ReadabilityAnalysisResult
|
|
4
|
+
from .code_analysis.redundancy import Redundancy, RedundancyAnalysisResult
|
|
5
|
+
from .code_analysis.struct import Struct, StructAnalysisResult
|
|
6
|
+
from .code_analysis.style import Style, StyleAnalysisResult
|
|
7
|
+
from .core import CodeAnalysis, CodeAnalysisType
|
|
8
|
+
from .trend_analysis.trend_analysis import TrendAnalysis
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"CodeAnalysis",
|
|
12
|
+
"Readability",
|
|
13
|
+
"ReadabilityAnalysisResult",
|
|
14
|
+
"CodeAnalysisType",
|
|
15
|
+
"Style",
|
|
16
|
+
"StyleAnalysisResult",
|
|
17
|
+
"Struct",
|
|
18
|
+
"StructAnalysisResult",
|
|
19
|
+
"Redundancy",
|
|
20
|
+
"RedundancyAnalysisResult",
|
|
21
|
+
"Algorithm",
|
|
22
|
+
"AlgorithmAnalysisResult",
|
|
23
|
+
"Complexity",
|
|
24
|
+
"ComplexityAnalysisResult",
|
|
25
|
+
"TrendAnalysis",
|
|
26
|
+
]
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
from typing import Set
|
|
3
|
+
|
|
4
|
+
from code_insight.code_analysis.abstract import AbstractAnalysis, BaseAnalysisResult
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AlgorithmAnalysisResult(BaseAnalysisResult):
|
|
8
|
+
"""
|
|
9
|
+
解析結果(アルゴリズム)
|
|
10
|
+
* 制御構文
|
|
11
|
+
* if文の数
|
|
12
|
+
* for文の数
|
|
13
|
+
* while文の数
|
|
14
|
+
* try-except文の数
|
|
15
|
+
* 再帰構造
|
|
16
|
+
* 再帰関数の割合
|
|
17
|
+
* FP的要素
|
|
18
|
+
* lambda式の数
|
|
19
|
+
* リスト内包表記の数
|
|
20
|
+
* map/filter/reduce呼び出しの数
|
|
21
|
+
* 循環的複雑度
|
|
22
|
+
* McCabe複雑度の平均
|
|
23
|
+
* ネスト深度
|
|
24
|
+
* 制御構文の最大ネスト深度
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
if_count: int
|
|
28
|
+
for_count: int
|
|
29
|
+
while_count: int
|
|
30
|
+
try_count: int
|
|
31
|
+
recursion_rate: float
|
|
32
|
+
lambda_count: int
|
|
33
|
+
comprehension_count: int
|
|
34
|
+
functional_call_count: int
|
|
35
|
+
cyclomatic_complexity: float
|
|
36
|
+
max_nesting_depth: int
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Algorithm(AbstractAnalysis[AlgorithmAnalysisResult]):
|
|
40
|
+
"""解析クラス(アルゴリズム)"""
|
|
41
|
+
|
|
42
|
+
def analyze(self, source_code: str) -> AlgorithmAnalysisResult:
|
|
43
|
+
"""コード解析"""
|
|
44
|
+
return AlgorithmAnalysisResult(
|
|
45
|
+
if_count=self.get_if_count(source_code),
|
|
46
|
+
for_count=self.get_for_count(source_code),
|
|
47
|
+
while_count=self.get_while_count(source_code),
|
|
48
|
+
try_count=self.get_try_count(source_code),
|
|
49
|
+
recursion_rate=self.get_recursion_rate(source_code),
|
|
50
|
+
lambda_count=self.get_lambda_count(source_code),
|
|
51
|
+
comprehension_count=self.get_comprehension_count(source_code),
|
|
52
|
+
functional_call_count=self.get_functional_call_count(source_code),
|
|
53
|
+
cyclomatic_complexity=self.get_cyclomatic_complexity(source_code),
|
|
54
|
+
max_nesting_depth=self.get_max_nesting_depth(source_code),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def parse_source_code(self, source_code: str) -> ast.AST:
|
|
58
|
+
"""ソースコードを解析"""
|
|
59
|
+
return ast.parse(source_code)
|
|
60
|
+
|
|
61
|
+
def get_if_count(self, source_code: str) -> int:
|
|
62
|
+
"""if文の数を取得"""
|
|
63
|
+
tree = self.parse_source_code(source_code)
|
|
64
|
+
return sum(isinstance(node, ast.If) for node in ast.walk(tree))
|
|
65
|
+
|
|
66
|
+
def get_for_count(self, source_code: str) -> int:
|
|
67
|
+
"""for文の数を取得"""
|
|
68
|
+
tree = self.parse_source_code(source_code)
|
|
69
|
+
return sum(isinstance(node, (ast.For, ast.AsyncFor)) for node in ast.walk(tree))
|
|
70
|
+
|
|
71
|
+
def get_while_count(self, source_code: str) -> int:
|
|
72
|
+
"""while文の数を取得"""
|
|
73
|
+
tree = self.parse_source_code(source_code)
|
|
74
|
+
return sum(isinstance(node, ast.While) for node in ast.walk(tree))
|
|
75
|
+
|
|
76
|
+
def get_try_count(self, source_code: str) -> int:
|
|
77
|
+
"""try-except文の数を取得"""
|
|
78
|
+
tree = self.parse_source_code(source_code)
|
|
79
|
+
return sum(isinstance(node, ast.Try) for node in ast.walk(tree))
|
|
80
|
+
|
|
81
|
+
def get_recursion_rate(self, source_code: str) -> float:
|
|
82
|
+
"""再帰関数の割合を取得"""
|
|
83
|
+
tree = self.parse_source_code(source_code)
|
|
84
|
+
function_names: Set[str] = set()
|
|
85
|
+
recursive_functions: Set[str] = set()
|
|
86
|
+
|
|
87
|
+
for node in ast.walk(tree):
|
|
88
|
+
if isinstance(node, ast.FunctionDef):
|
|
89
|
+
function_names.add(node.name)
|
|
90
|
+
|
|
91
|
+
for node in ast.walk(tree):
|
|
92
|
+
if isinstance(node, ast.FunctionDef):
|
|
93
|
+
for call_node in ast.walk(node):
|
|
94
|
+
if (
|
|
95
|
+
isinstance(call_node, ast.Call)
|
|
96
|
+
and isinstance(call_node.func, ast.Name)
|
|
97
|
+
and call_node.func.id == node.name
|
|
98
|
+
):
|
|
99
|
+
recursive_functions.add(node.name)
|
|
100
|
+
|
|
101
|
+
if function_names:
|
|
102
|
+
return len(recursive_functions) / len(function_names)
|
|
103
|
+
return 0.0
|
|
104
|
+
|
|
105
|
+
def get_lambda_count(self, source_code: str) -> int:
|
|
106
|
+
"""lambda式の数を取得"""
|
|
107
|
+
tree = self.parse_source_code(source_code)
|
|
108
|
+
return sum(isinstance(node, ast.Lambda) for node in ast.walk(tree))
|
|
109
|
+
|
|
110
|
+
def get_comprehension_count(self, source_code: str) -> int:
|
|
111
|
+
"""内包表記の数を取得"""
|
|
112
|
+
tree = self.parse_source_code(source_code)
|
|
113
|
+
return sum(
|
|
114
|
+
isinstance(
|
|
115
|
+
node, (ast.ListComp, ast.SetComp, ast.DictComp, ast.GeneratorExp)
|
|
116
|
+
)
|
|
117
|
+
for node in ast.walk(tree)
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def get_functional_call_count(self, source_code: str) -> int:
|
|
121
|
+
"""map/filter/reduce呼び出しの数を取得"""
|
|
122
|
+
tree = self.parse_source_code(source_code)
|
|
123
|
+
functional_names = {"map", "filter", "reduce"}
|
|
124
|
+
count = 0
|
|
125
|
+
|
|
126
|
+
for node in ast.walk(tree):
|
|
127
|
+
if (
|
|
128
|
+
isinstance(node, ast.Call)
|
|
129
|
+
and isinstance(node.func, ast.Name)
|
|
130
|
+
and node.func.id in functional_names
|
|
131
|
+
):
|
|
132
|
+
count += 1
|
|
133
|
+
|
|
134
|
+
return count
|
|
135
|
+
|
|
136
|
+
def get_cyclomatic_complexity(self, source_code: str) -> float:
|
|
137
|
+
"""循環的複雑度の平均を取得"""
|
|
138
|
+
tree = self.parse_source_code(source_code)
|
|
139
|
+
complexities = []
|
|
140
|
+
|
|
141
|
+
for node in ast.walk(tree):
|
|
142
|
+
if isinstance(node, ast.FunctionDef):
|
|
143
|
+
complexity = self._calculate_function_complexity(node)
|
|
144
|
+
complexities.append(complexity)
|
|
145
|
+
|
|
146
|
+
return sum(complexities) / len(complexities) if complexities else 0.0
|
|
147
|
+
|
|
148
|
+
def _calculate_function_complexity(self, func_node: ast.FunctionDef) -> int:
|
|
149
|
+
"""関数の循環的複雑度を計算"""
|
|
150
|
+
complexity = 1
|
|
151
|
+
|
|
152
|
+
for node in ast.walk(func_node):
|
|
153
|
+
if isinstance(node, (ast.If, ast.While, ast.For, ast.AsyncFor)):
|
|
154
|
+
complexity += 1
|
|
155
|
+
elif isinstance(node, ast.Try):
|
|
156
|
+
complexity += len(node.handlers)
|
|
157
|
+
elif isinstance(node, ast.BoolOp):
|
|
158
|
+
complexity += len(node.values) - 1
|
|
159
|
+
|
|
160
|
+
return complexity
|
|
161
|
+
|
|
162
|
+
def get_max_nesting_depth(self, source_code: str) -> int:
|
|
163
|
+
"""制御構文の最大ネスト深度を取得"""
|
|
164
|
+
tree = self.parse_source_code(source_code)
|
|
165
|
+
max_depth = 0
|
|
166
|
+
|
|
167
|
+
def calculate_depth(node: ast.AST, current_depth: int = 0) -> None:
|
|
168
|
+
nonlocal max_depth
|
|
169
|
+
|
|
170
|
+
if isinstance(
|
|
171
|
+
node,
|
|
172
|
+
(
|
|
173
|
+
ast.If,
|
|
174
|
+
ast.For,
|
|
175
|
+
ast.AsyncFor,
|
|
176
|
+
ast.While,
|
|
177
|
+
ast.Try,
|
|
178
|
+
ast.With,
|
|
179
|
+
ast.AsyncWith,
|
|
180
|
+
),
|
|
181
|
+
):
|
|
182
|
+
current_depth += 1
|
|
183
|
+
max_depth = max(max_depth, current_depth)
|
|
184
|
+
|
|
185
|
+
for child in ast.iter_child_nodes(node):
|
|
186
|
+
calculate_depth(child, current_depth)
|
|
187
|
+
|
|
188
|
+
calculate_depth(tree)
|
|
189
|
+
return max_depth
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
|
|
3
|
+
import radon.complexity as cc
|
|
4
|
+
import radon.metrics as metrics
|
|
5
|
+
|
|
6
|
+
from code_insight.code_analysis.abstract import AbstractAnalysis, BaseAnalysisResult
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ComplexityAnalysisResult(BaseAnalysisResult):
|
|
10
|
+
"""
|
|
11
|
+
解析結果(複雑度)
|
|
12
|
+
* サイクロマティック複雑度
|
|
13
|
+
* 関数・メソッドの平均サイクロマティック複雑度
|
|
14
|
+
* Halstead複雑度
|
|
15
|
+
* Halstead Volume, Difficulty, Effort
|
|
16
|
+
* ネストの深さ
|
|
17
|
+
* 最大ネスト深度と平均ネスト深度
|
|
18
|
+
* 認知的複雑度
|
|
19
|
+
* 制御構造の複雑さを測定
|
|
20
|
+
* 保守性指数
|
|
21
|
+
* Maintainability Index
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
cyclomatic_complexity: float
|
|
25
|
+
halstead_volume: float
|
|
26
|
+
halstead_difficulty: float
|
|
27
|
+
halstead_effort: float
|
|
28
|
+
max_nesting_depth: int
|
|
29
|
+
avg_nesting_depth: float
|
|
30
|
+
cognitive_complexity: float
|
|
31
|
+
maintainability_index: float
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Complexity(AbstractAnalysis[ComplexityAnalysisResult]):
|
|
35
|
+
"""解析クラス(複雑度)"""
|
|
36
|
+
|
|
37
|
+
def analyze(self, source_code: str) -> ComplexityAnalysisResult:
|
|
38
|
+
"""コード解析"""
|
|
39
|
+
return ComplexityAnalysisResult(
|
|
40
|
+
cyclomatic_complexity=self.get_cyclomatic_complexity(source_code),
|
|
41
|
+
halstead_volume=self.get_halstead_volume(source_code),
|
|
42
|
+
halstead_difficulty=self.get_halstead_difficulty(source_code),
|
|
43
|
+
halstead_effort=self.get_halstead_effort(source_code),
|
|
44
|
+
max_nesting_depth=self.get_max_nesting_depth(source_code),
|
|
45
|
+
avg_nesting_depth=self.get_avg_nesting_depth(source_code),
|
|
46
|
+
cognitive_complexity=self.get_cognitive_complexity(source_code),
|
|
47
|
+
maintainability_index=self.get_maintainability_index(source_code),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def get_cyclomatic_complexity(self, source_code: str) -> float:
|
|
51
|
+
"""サイクロマティック複雑度の平均を取得"""
|
|
52
|
+
if not source_code.strip():
|
|
53
|
+
return 0.0
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
cc_result = cc.cc_visit(source_code)
|
|
57
|
+
if not cc_result:
|
|
58
|
+
return 0.0
|
|
59
|
+
|
|
60
|
+
total_complexity = sum(item.complexity for item in cc_result)
|
|
61
|
+
return total_complexity / len(cc_result)
|
|
62
|
+
except Exception:
|
|
63
|
+
return 0.0
|
|
64
|
+
|
|
65
|
+
def get_halstead_volume(self, source_code: str) -> float:
|
|
66
|
+
"""Halstead Volumeを取得"""
|
|
67
|
+
if not source_code.strip():
|
|
68
|
+
return 0.0
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
h_result = metrics.h_visit(source_code)
|
|
72
|
+
return h_result.total.volume if h_result.total else 0.0
|
|
73
|
+
except Exception:
|
|
74
|
+
return 0.0
|
|
75
|
+
|
|
76
|
+
def get_halstead_difficulty(self, source_code: str) -> float:
|
|
77
|
+
"""Halstead Difficultyを取得"""
|
|
78
|
+
if not source_code.strip():
|
|
79
|
+
return 0.0
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
h_result = metrics.h_visit(source_code)
|
|
83
|
+
return h_result.total.difficulty if h_result.total else 0.0
|
|
84
|
+
except Exception:
|
|
85
|
+
return 0.0
|
|
86
|
+
|
|
87
|
+
def get_halstead_effort(self, source_code: str) -> float:
|
|
88
|
+
"""Halstead Effortを取得"""
|
|
89
|
+
if not source_code.strip():
|
|
90
|
+
return 0.0
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
h_result = metrics.h_visit(source_code)
|
|
94
|
+
return h_result.total.effort if h_result.total else 0.0
|
|
95
|
+
except Exception:
|
|
96
|
+
return 0.0
|
|
97
|
+
|
|
98
|
+
def get_maintainability_index(self, source_code: str) -> float:
|
|
99
|
+
"""保守性指数を取得"""
|
|
100
|
+
if not source_code.strip():
|
|
101
|
+
return 0.0
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
return metrics.mi_visit(source_code, multi=True)
|
|
105
|
+
except Exception:
|
|
106
|
+
return 0.0
|
|
107
|
+
|
|
108
|
+
def get_max_nesting_depth(self, source_code: str) -> int:
|
|
109
|
+
"""最大ネスト深度を取得"""
|
|
110
|
+
if not source_code.strip():
|
|
111
|
+
return 0
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
tree = ast.parse(source_code)
|
|
115
|
+
max_depth = 0
|
|
116
|
+
|
|
117
|
+
def calculate_depth(node: ast.AST, current_depth: int = 0) -> int:
|
|
118
|
+
nonlocal max_depth
|
|
119
|
+
|
|
120
|
+
nesting_nodes = (
|
|
121
|
+
ast.If,
|
|
122
|
+
ast.For,
|
|
123
|
+
ast.While,
|
|
124
|
+
ast.With,
|
|
125
|
+
ast.Try,
|
|
126
|
+
ast.FunctionDef,
|
|
127
|
+
ast.ClassDef,
|
|
128
|
+
ast.AsyncFor,
|
|
129
|
+
ast.AsyncWith,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
if isinstance(node, nesting_nodes):
|
|
133
|
+
current_depth += 1
|
|
134
|
+
max_depth = max(max_depth, current_depth)
|
|
135
|
+
|
|
136
|
+
for child in ast.iter_child_nodes(node):
|
|
137
|
+
calculate_depth(child, current_depth)
|
|
138
|
+
|
|
139
|
+
return max_depth
|
|
140
|
+
|
|
141
|
+
return calculate_depth(tree)
|
|
142
|
+
except Exception:
|
|
143
|
+
return 0
|
|
144
|
+
|
|
145
|
+
def get_avg_nesting_depth(self, source_code: str) -> float:
|
|
146
|
+
"""平均ネスト深度を取得"""
|
|
147
|
+
if not source_code.strip():
|
|
148
|
+
return 0.0
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
tree = ast.parse(source_code)
|
|
152
|
+
depths = []
|
|
153
|
+
|
|
154
|
+
def collect_depths(node: ast.AST, current_depth: int = 0) -> None:
|
|
155
|
+
nesting_nodes = (ast.If, ast.For, ast.While, ast.With, ast.Try)
|
|
156
|
+
|
|
157
|
+
if isinstance(node, nesting_nodes):
|
|
158
|
+
current_depth += 1
|
|
159
|
+
depths.append(current_depth)
|
|
160
|
+
|
|
161
|
+
for child in ast.iter_child_nodes(node):
|
|
162
|
+
collect_depths(child, current_depth)
|
|
163
|
+
|
|
164
|
+
collect_depths(tree)
|
|
165
|
+
return sum(depths) / len(depths) if depths else 0.0
|
|
166
|
+
except Exception:
|
|
167
|
+
return 0.0
|
|
168
|
+
|
|
169
|
+
def get_cognitive_complexity(self, source_code: str) -> float:
|
|
170
|
+
"""認知的複雑度を取得"""
|
|
171
|
+
if not source_code.strip():
|
|
172
|
+
return 0.0
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
tree = ast.parse(source_code)
|
|
176
|
+
complexity = 0
|
|
177
|
+
|
|
178
|
+
def calculate_cognitive_complexity(
|
|
179
|
+
node: ast.AST, nesting_level: int = 0
|
|
180
|
+
) -> int:
|
|
181
|
+
nonlocal complexity
|
|
182
|
+
|
|
183
|
+
if isinstance(node, (ast.If, ast.While, ast.For)):
|
|
184
|
+
complexity += 1 + nesting_level
|
|
185
|
+
elif isinstance(node, ast.Try):
|
|
186
|
+
complexity += 1 + nesting_level
|
|
187
|
+
elif isinstance(node, ast.ExceptHandler):
|
|
188
|
+
complexity += 1 + nesting_level
|
|
189
|
+
elif isinstance(node, ast.BoolOp):
|
|
190
|
+
complexity += len(node.values) - 1
|
|
191
|
+
|
|
192
|
+
nesting_increment_nodes = (
|
|
193
|
+
ast.If,
|
|
194
|
+
ast.For,
|
|
195
|
+
ast.While,
|
|
196
|
+
ast.Try,
|
|
197
|
+
ast.FunctionDef,
|
|
198
|
+
ast.AsyncFunctionDef,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
new_nesting_level = nesting_level
|
|
202
|
+
if isinstance(node, nesting_increment_nodes):
|
|
203
|
+
new_nesting_level += 1
|
|
204
|
+
|
|
205
|
+
for child in ast.iter_child_nodes(node):
|
|
206
|
+
calculate_cognitive_complexity(child, new_nesting_level)
|
|
207
|
+
|
|
208
|
+
return complexity
|
|
209
|
+
|
|
210
|
+
return float(calculate_cognitive_complexity(tree))
|
|
211
|
+
except Exception:
|
|
212
|
+
return 0.0
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import math
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from code_insight.code_analysis.abstract import AbstractAnalysis, BaseAnalysisResult
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ReadabilityAnalysisResult(BaseAnalysisResult):
|
|
9
|
+
"""
|
|
10
|
+
解析結果(可読性)
|
|
11
|
+
* 変数名の長さ
|
|
12
|
+
* 変数名の平均長
|
|
13
|
+
* 変数名の最大長
|
|
14
|
+
* 行の長さ
|
|
15
|
+
* 行の平均長
|
|
16
|
+
* 行の最大長
|
|
17
|
+
* 情報量
|
|
18
|
+
* Halstead Volume
|
|
19
|
+
* Halstead Difficulty
|
|
20
|
+
* Halstead Effort
|
|
21
|
+
* ネスト深度
|
|
22
|
+
* 平均ネスト深度
|
|
23
|
+
* 識別子複雑度
|
|
24
|
+
* 略語使用率や複雑な命名パターンの割合
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
variable_name_length: float
|
|
28
|
+
max_variable_name_length: int
|
|
29
|
+
line_length: float
|
|
30
|
+
max_line_length: int
|
|
31
|
+
halstead_volume: float
|
|
32
|
+
halstead_difficulty: float
|
|
33
|
+
halstead_effort: float
|
|
34
|
+
nesting_depth: float
|
|
35
|
+
identifier_complexity: float
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Readability(AbstractAnalysis[ReadabilityAnalysisResult]):
|
|
39
|
+
"""解析クラス(可読性)"""
|
|
40
|
+
|
|
41
|
+
def analyze(self, source_code: str) -> ReadabilityAnalysisResult:
|
|
42
|
+
"""コード解析"""
|
|
43
|
+
return ReadabilityAnalysisResult(
|
|
44
|
+
variable_name_length=self.get_variable_name_length(source_code),
|
|
45
|
+
max_variable_name_length=self.get_max_variable_name_length(source_code),
|
|
46
|
+
line_length=self.get_line_length(source_code),
|
|
47
|
+
max_line_length=self.get_max_line_length(source_code),
|
|
48
|
+
halstead_volume=self.get_halstead_volume(source_code),
|
|
49
|
+
halstead_difficulty=self.get_halstead_difficulty(source_code),
|
|
50
|
+
halstead_effort=self.get_halstead_effort(source_code),
|
|
51
|
+
nesting_depth=self.get_nesting_depth(source_code),
|
|
52
|
+
identifier_complexity=self.get_identifier_complexity(source_code),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def parse_source_code(self, source_code: str) -> ast.AST:
|
|
56
|
+
"""ソースコードを解析"""
|
|
57
|
+
return ast.parse(source_code)
|
|
58
|
+
|
|
59
|
+
def get_variable_names(self, source_code: str) -> list[str]:
|
|
60
|
+
"""変数名を抽出"""
|
|
61
|
+
if not source_code.strip():
|
|
62
|
+
return []
|
|
63
|
+
|
|
64
|
+
tree = self.parse_source_code(source_code)
|
|
65
|
+
variable_names = []
|
|
66
|
+
|
|
67
|
+
for node in ast.walk(tree):
|
|
68
|
+
if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Store):
|
|
69
|
+
variable_names.append(node.id)
|
|
70
|
+
elif isinstance(node, ast.arg):
|
|
71
|
+
variable_names.append(node.arg)
|
|
72
|
+
elif isinstance(node, ast.Attribute) and isinstance(node.ctx, ast.Store):
|
|
73
|
+
variable_names.append(node.attr)
|
|
74
|
+
|
|
75
|
+
return variable_names
|
|
76
|
+
|
|
77
|
+
def get_variable_name_length(self, source_code: str) -> float:
|
|
78
|
+
"""変数名の平均長を取得"""
|
|
79
|
+
variable_names = self.get_variable_names(source_code)
|
|
80
|
+
if not variable_names:
|
|
81
|
+
return 0.0
|
|
82
|
+
|
|
83
|
+
total_length = sum(len(name) for name in variable_names)
|
|
84
|
+
return total_length / len(variable_names)
|
|
85
|
+
|
|
86
|
+
def get_max_variable_name_length(self, source_code: str) -> int:
|
|
87
|
+
"""変数名の最大長を取得"""
|
|
88
|
+
variable_names = self.get_variable_names(source_code)
|
|
89
|
+
if not variable_names:
|
|
90
|
+
return 0
|
|
91
|
+
|
|
92
|
+
return max(len(name) for name in variable_names)
|
|
93
|
+
|
|
94
|
+
def get_line_length(self, source_code: str) -> float:
|
|
95
|
+
"""行の平均長を取得"""
|
|
96
|
+
lines = source_code.splitlines()
|
|
97
|
+
if not lines:
|
|
98
|
+
return 0.0
|
|
99
|
+
|
|
100
|
+
total_length = sum(len(line) for line in lines)
|
|
101
|
+
return total_length / len(lines)
|
|
102
|
+
|
|
103
|
+
def get_max_line_length(self, source_code: str) -> int:
|
|
104
|
+
"""行の最大長を取得"""
|
|
105
|
+
lines = source_code.splitlines()
|
|
106
|
+
if not lines:
|
|
107
|
+
return 0
|
|
108
|
+
|
|
109
|
+
return max(len(line) for line in lines)
|
|
110
|
+
|
|
111
|
+
def get_halstead_metrics(self, source_code: str) -> tuple[int, int, int, int]:
|
|
112
|
+
"""Halstead メトリクスの基本値を取得"""
|
|
113
|
+
if not source_code.strip():
|
|
114
|
+
return 0, 0, 0, 0
|
|
115
|
+
|
|
116
|
+
tree = self.parse_source_code(source_code)
|
|
117
|
+
|
|
118
|
+
operators = set()
|
|
119
|
+
operands = set()
|
|
120
|
+
operator_count = 0
|
|
121
|
+
operand_count = 0
|
|
122
|
+
|
|
123
|
+
for node in ast.walk(tree):
|
|
124
|
+
if isinstance(
|
|
125
|
+
node,
|
|
126
|
+
(
|
|
127
|
+
ast.Add,
|
|
128
|
+
ast.Sub,
|
|
129
|
+
ast.Mult,
|
|
130
|
+
ast.Div,
|
|
131
|
+
ast.Mod,
|
|
132
|
+
ast.Pow,
|
|
133
|
+
ast.LShift,
|
|
134
|
+
ast.RShift,
|
|
135
|
+
ast.BitOr,
|
|
136
|
+
ast.BitXor,
|
|
137
|
+
ast.BitAnd,
|
|
138
|
+
ast.FloorDiv,
|
|
139
|
+
),
|
|
140
|
+
):
|
|
141
|
+
operators.add(type(node).__name__)
|
|
142
|
+
operator_count += 1
|
|
143
|
+
elif isinstance(
|
|
144
|
+
node,
|
|
145
|
+
(
|
|
146
|
+
ast.And,
|
|
147
|
+
ast.Or,
|
|
148
|
+
ast.Not,
|
|
149
|
+
ast.Eq,
|
|
150
|
+
ast.NotEq,
|
|
151
|
+
ast.Lt,
|
|
152
|
+
ast.LtE,
|
|
153
|
+
ast.Gt,
|
|
154
|
+
ast.GtE,
|
|
155
|
+
ast.Is,
|
|
156
|
+
ast.IsNot,
|
|
157
|
+
ast.In,
|
|
158
|
+
ast.NotIn,
|
|
159
|
+
),
|
|
160
|
+
):
|
|
161
|
+
operators.add(type(node).__name__)
|
|
162
|
+
operator_count += 1
|
|
163
|
+
elif isinstance(
|
|
164
|
+
node,
|
|
165
|
+
(
|
|
166
|
+
ast.If,
|
|
167
|
+
ast.For,
|
|
168
|
+
ast.While,
|
|
169
|
+
ast.Try,
|
|
170
|
+
ast.With,
|
|
171
|
+
ast.FunctionDef,
|
|
172
|
+
ast.ClassDef,
|
|
173
|
+
ast.Return,
|
|
174
|
+
ast.Assign,
|
|
175
|
+
ast.AugAssign,
|
|
176
|
+
),
|
|
177
|
+
):
|
|
178
|
+
operators.add(type(node).__name__)
|
|
179
|
+
operator_count += 1
|
|
180
|
+
elif isinstance(node, ast.Name):
|
|
181
|
+
operands.add(node.id)
|
|
182
|
+
operand_count += 1
|
|
183
|
+
elif isinstance(node, ast.Constant):
|
|
184
|
+
operands.add(str(node.value))
|
|
185
|
+
operand_count += 1
|
|
186
|
+
|
|
187
|
+
n1 = len(operators)
|
|
188
|
+
n2 = len(operands)
|
|
189
|
+
N1 = operator_count
|
|
190
|
+
N2 = operand_count
|
|
191
|
+
|
|
192
|
+
return n1, n2, N1, N2
|
|
193
|
+
|
|
194
|
+
def get_halstead_volume(self, source_code: str) -> float:
|
|
195
|
+
"""Halstead Volume を計算"""
|
|
196
|
+
n1, n2, N1, N2 = self.get_halstead_metrics(source_code)
|
|
197
|
+
|
|
198
|
+
if n1 + n2 == 0:
|
|
199
|
+
return 0.0
|
|
200
|
+
|
|
201
|
+
N = N1 + N2
|
|
202
|
+
n = n1 + n2
|
|
203
|
+
|
|
204
|
+
return N * math.log2(n) if n > 0 else 0.0
|
|
205
|
+
|
|
206
|
+
def get_halstead_difficulty(self, source_code: str) -> float:
|
|
207
|
+
"""Halstead Difficulty を計算"""
|
|
208
|
+
n1, n2, N1, N2 = self.get_halstead_metrics(source_code)
|
|
209
|
+
|
|
210
|
+
if n2 == 0:
|
|
211
|
+
return 0.0
|
|
212
|
+
|
|
213
|
+
return (n1 / 2) * (N2 / n2)
|
|
214
|
+
|
|
215
|
+
def get_halstead_effort(self, source_code: str) -> float:
|
|
216
|
+
"""Halstead Effort を計算"""
|
|
217
|
+
volume = self.get_halstead_volume(source_code)
|
|
218
|
+
difficulty = self.get_halstead_difficulty(source_code)
|
|
219
|
+
|
|
220
|
+
return volume * difficulty
|
|
221
|
+
|
|
222
|
+
def get_nesting_depth(self, source_code: str) -> float:
|
|
223
|
+
"""平均ネスト深度を取得"""
|
|
224
|
+
if not source_code.strip():
|
|
225
|
+
return 0.0
|
|
226
|
+
|
|
227
|
+
tree = self.parse_source_code(source_code)
|
|
228
|
+
depths = []
|
|
229
|
+
|
|
230
|
+
def calculate_depth(node: ast.AST, current_depth: int = 0) -> None:
|
|
231
|
+
if isinstance(
|
|
232
|
+
node,
|
|
233
|
+
(
|
|
234
|
+
ast.If,
|
|
235
|
+
ast.For,
|
|
236
|
+
ast.While,
|
|
237
|
+
ast.Try,
|
|
238
|
+
ast.With,
|
|
239
|
+
ast.FunctionDef,
|
|
240
|
+
ast.ClassDef,
|
|
241
|
+
),
|
|
242
|
+
):
|
|
243
|
+
depths.append(current_depth)
|
|
244
|
+
current_depth += 1
|
|
245
|
+
|
|
246
|
+
for child in ast.iter_child_nodes(node):
|
|
247
|
+
calculate_depth(child, current_depth)
|
|
248
|
+
|
|
249
|
+
calculate_depth(tree)
|
|
250
|
+
|
|
251
|
+
if not depths:
|
|
252
|
+
return 0.0
|
|
253
|
+
|
|
254
|
+
return sum(depths) / len(depths)
|
|
255
|
+
|
|
256
|
+
def get_identifier_complexity(self, source_code: str) -> float:
|
|
257
|
+
"""識別子複雑度を取得"""
|
|
258
|
+
variable_names = self.get_variable_names(source_code)
|
|
259
|
+
if not variable_names:
|
|
260
|
+
return 0.0
|
|
261
|
+
|
|
262
|
+
complex_count = 0
|
|
263
|
+
|
|
264
|
+
for name in variable_names:
|
|
265
|
+
if len(name) <= 2:
|
|
266
|
+
complex_count += 1
|
|
267
|
+
elif re.search(r"[A-Z]{2,}", name):
|
|
268
|
+
complex_count += 1
|
|
269
|
+
elif len(re.findall(r"[aeiouAEIOU]", name)) / len(name) < 0.2:
|
|
270
|
+
complex_count += 1
|
|
271
|
+
|
|
272
|
+
return complex_count / len(variable_names)
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import hashlib
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from typing import Any, Dict, List, Set
|
|
5
|
+
|
|
6
|
+
from radon.complexity import cc_visit
|
|
7
|
+
from radon.metrics import mi_visit
|
|
8
|
+
|
|
9
|
+
from code_insight.code_analysis.abstract import AbstractAnalysis, BaseAnalysisResult
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RedundancyAnalysisResult(BaseAnalysisResult):
|
|
13
|
+
"""
|
|
14
|
+
解析結果(冗長度)
|
|
15
|
+
* 重複コード割合
|
|
16
|
+
* 構造的に類似した関数の割合
|
|
17
|
+
* 未使用コード割合
|
|
18
|
+
* 定義されているが呼び出されていない関数・クラスの割合
|
|
19
|
+
* 長大関数割合
|
|
20
|
+
* 50行以上または循環的複雑度10以上の関数の割合
|
|
21
|
+
* 循環的複雑度
|
|
22
|
+
* 関数の平均循環的複雑度
|
|
23
|
+
* 保守性指数
|
|
24
|
+
* 関数の平均保守性指数
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
duplicate_code_rate: float
|
|
28
|
+
unused_code_rate: float
|
|
29
|
+
long_function_rate: float
|
|
30
|
+
cyclomatic_complexity: float
|
|
31
|
+
maintainability_index: float
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Redundancy(AbstractAnalysis[RedundancyAnalysisResult]):
|
|
35
|
+
"""解析クラス(冗長度)"""
|
|
36
|
+
|
|
37
|
+
def analyze(self, source_code: str) -> RedundancyAnalysisResult:
|
|
38
|
+
"""コード解析"""
|
|
39
|
+
return RedundancyAnalysisResult(
|
|
40
|
+
duplicate_code_rate=self.get_duplicate_code_rate(source_code),
|
|
41
|
+
unused_code_rate=self.get_unused_code_rate(source_code),
|
|
42
|
+
long_function_rate=self.get_long_function_rate(source_code),
|
|
43
|
+
cyclomatic_complexity=self.get_cyclomatic_complexity(source_code),
|
|
44
|
+
maintainability_index=self.get_maintainability_index(source_code),
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def parse_source_code(self, source_code: str) -> ast.AST:
|
|
48
|
+
"""ソースコードを解析"""
|
|
49
|
+
return ast.parse(source_code)
|
|
50
|
+
|
|
51
|
+
def get_duplicate_code_rate(self, source_code: str) -> float:
|
|
52
|
+
"""重複コード割合を取得"""
|
|
53
|
+
if not source_code.strip():
|
|
54
|
+
return 0.0
|
|
55
|
+
|
|
56
|
+
tree = self.parse_source_code(source_code)
|
|
57
|
+
function_hashes: Dict[str, List[str]] = defaultdict(list)
|
|
58
|
+
total_functions = 0
|
|
59
|
+
|
|
60
|
+
for node in ast.walk(tree):
|
|
61
|
+
if isinstance(node, ast.FunctionDef):
|
|
62
|
+
total_functions += 1
|
|
63
|
+
func_hash = self._get_function_structure_hash(node)
|
|
64
|
+
function_hashes[func_hash].append(node.name)
|
|
65
|
+
|
|
66
|
+
if total_functions == 0:
|
|
67
|
+
return 0.0
|
|
68
|
+
|
|
69
|
+
duplicate_functions = sum(
|
|
70
|
+
len(functions) - 1
|
|
71
|
+
for functions in function_hashes.values()
|
|
72
|
+
if len(functions) > 1
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
return duplicate_functions / total_functions
|
|
76
|
+
|
|
77
|
+
def get_unused_code_rate(self, source_code: str) -> float:
|
|
78
|
+
"""未使用コード割合を取得"""
|
|
79
|
+
if not source_code.strip():
|
|
80
|
+
return 0.0
|
|
81
|
+
|
|
82
|
+
tree = self.parse_source_code(source_code)
|
|
83
|
+
defined_names: Set[str] = set()
|
|
84
|
+
called_names: Set[str] = set()
|
|
85
|
+
|
|
86
|
+
for node in ast.walk(tree):
|
|
87
|
+
if isinstance(node, ast.FunctionDef):
|
|
88
|
+
if node.name not in ["main", "__init__", "__main__"]:
|
|
89
|
+
defined_names.add(node.name)
|
|
90
|
+
elif isinstance(node, ast.ClassDef):
|
|
91
|
+
defined_names.add(node.name)
|
|
92
|
+
elif isinstance(node, ast.Call):
|
|
93
|
+
if isinstance(node.func, ast.Name):
|
|
94
|
+
called_names.add(node.func.id)
|
|
95
|
+
elif isinstance(node.func, ast.Attribute):
|
|
96
|
+
called_names.add(node.func.attr)
|
|
97
|
+
|
|
98
|
+
if not defined_names:
|
|
99
|
+
return 0.0
|
|
100
|
+
|
|
101
|
+
unused_names = defined_names - called_names
|
|
102
|
+
return len(unused_names) / len(defined_names)
|
|
103
|
+
|
|
104
|
+
def get_long_function_rate(self, source_code: str) -> float:
|
|
105
|
+
"""長大関数割合を取得"""
|
|
106
|
+
if not source_code.strip():
|
|
107
|
+
return 0.0
|
|
108
|
+
|
|
109
|
+
tree = self.parse_source_code(source_code)
|
|
110
|
+
long_functions = 0
|
|
111
|
+
total_functions = 0
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
complexity_results = cc_visit(source_code)
|
|
115
|
+
complexity_map = {
|
|
116
|
+
result.name: result.complexity for result in complexity_results
|
|
117
|
+
}
|
|
118
|
+
except Exception:
|
|
119
|
+
complexity_map = {}
|
|
120
|
+
|
|
121
|
+
for node in ast.walk(tree):
|
|
122
|
+
if isinstance(node, ast.FunctionDef):
|
|
123
|
+
total_functions += 1
|
|
124
|
+
|
|
125
|
+
func_lines = self._count_function_lines(node, source_code)
|
|
126
|
+
func_complexity = complexity_map.get(node.name, 1)
|
|
127
|
+
|
|
128
|
+
if func_lines >= 50 or func_complexity >= 10:
|
|
129
|
+
long_functions += 1
|
|
130
|
+
|
|
131
|
+
if total_functions == 0:
|
|
132
|
+
return 0.0
|
|
133
|
+
|
|
134
|
+
return long_functions / total_functions
|
|
135
|
+
|
|
136
|
+
def get_cyclomatic_complexity(self, source_code: str) -> float:
|
|
137
|
+
"""循環的複雑度の平均を取得"""
|
|
138
|
+
if not source_code.strip():
|
|
139
|
+
return 0.0
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
complexity_results = cc_visit(source_code)
|
|
143
|
+
if not complexity_results:
|
|
144
|
+
return 0.0
|
|
145
|
+
|
|
146
|
+
total_complexity = sum(result.complexity for result in complexity_results)
|
|
147
|
+
return total_complexity / len(complexity_results)
|
|
148
|
+
except Exception:
|
|
149
|
+
return 0.0
|
|
150
|
+
|
|
151
|
+
def get_maintainability_index(self, source_code: str) -> float:
|
|
152
|
+
"""保守性指数の平均を取得"""
|
|
153
|
+
if not source_code.strip():
|
|
154
|
+
return 0.0
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
mi_results: list[Any] = mi_visit(
|
|
158
|
+
source_code, multi=True
|
|
159
|
+
) # pyright: ignore[reportAssignmentType]
|
|
160
|
+
if not mi_results:
|
|
161
|
+
return 0.0
|
|
162
|
+
|
|
163
|
+
total_mi = sum(result.mi for result in mi_results)
|
|
164
|
+
return total_mi / len(mi_results)
|
|
165
|
+
except Exception:
|
|
166
|
+
return 0.0
|
|
167
|
+
|
|
168
|
+
def _get_function_structure_hash(self, func_node: ast.FunctionDef) -> str:
|
|
169
|
+
"""関数の構造的ハッシュを取得"""
|
|
170
|
+
structure_elements = []
|
|
171
|
+
|
|
172
|
+
for node in ast.walk(func_node):
|
|
173
|
+
if isinstance(node, (ast.If, ast.For, ast.While, ast.Try, ast.With)):
|
|
174
|
+
structure_elements.append(type(node).__name__)
|
|
175
|
+
elif isinstance(node, ast.Return):
|
|
176
|
+
if isinstance(node.value, ast.Constant):
|
|
177
|
+
node_value = node.value.value
|
|
178
|
+
if isinstance(node_value, bytes):
|
|
179
|
+
node_value = node_value.decode()
|
|
180
|
+
structure_elements.append(
|
|
181
|
+
f"return_const_{type(node.value.value).__name__}_"
|
|
182
|
+
f"{node_value}"
|
|
183
|
+
)
|
|
184
|
+
elif isinstance(node.value, ast.BinOp):
|
|
185
|
+
structure_elements.append(
|
|
186
|
+
f"return_binop_{type(node.value.op).__name__}"
|
|
187
|
+
)
|
|
188
|
+
else:
|
|
189
|
+
structure_elements.append("return_other")
|
|
190
|
+
elif isinstance(node, ast.Assign):
|
|
191
|
+
structure_elements.append("assign")
|
|
192
|
+
elif isinstance(node, ast.BinOp):
|
|
193
|
+
structure_elements.append(f"binop_{type(node.op).__name__}")
|
|
194
|
+
|
|
195
|
+
arg_count = len(func_node.args.args)
|
|
196
|
+
structure_elements.append(f"args_{arg_count}")
|
|
197
|
+
|
|
198
|
+
if len(structure_elements) < 3:
|
|
199
|
+
structure_elements.append(f"simple_{len(func_node.body)}")
|
|
200
|
+
|
|
201
|
+
structure_str = "_".join(structure_elements)
|
|
202
|
+
return hashlib.md5(structure_str.encode(), usedforsecurity=False).hexdigest()
|
|
203
|
+
|
|
204
|
+
def _count_function_lines(
|
|
205
|
+
self, func_node: ast.FunctionDef, source_code: str
|
|
206
|
+
) -> int:
|
|
207
|
+
"""関数の行数をカウント"""
|
|
208
|
+
if hasattr(func_node, "end_lineno") and func_node.end_lineno:
|
|
209
|
+
return func_node.end_lineno - func_node.lineno + 1
|
|
210
|
+
|
|
211
|
+
lines = source_code.splitlines()
|
|
212
|
+
if func_node.lineno <= len(lines):
|
|
213
|
+
func_start = func_node.lineno - 1
|
|
214
|
+
for i in range(func_start + 1, len(lines)):
|
|
215
|
+
line = lines[i].strip()
|
|
216
|
+
if line and not line.startswith(" ") and not line.startswith("\t"):
|
|
217
|
+
return i - func_start
|
|
218
|
+
return len(lines) - func_start
|
|
219
|
+
|
|
220
|
+
return 1
|
|
@@ -2,6 +2,10 @@ from enum import StrEnum, auto
|
|
|
2
2
|
from typing import Any, Type
|
|
3
3
|
|
|
4
4
|
from code_insight.code_analysis.abstract import AbstractAnalysis, BaseAnalysisResult
|
|
5
|
+
from code_insight.code_analysis.algorithm import Algorithm
|
|
6
|
+
from code_insight.code_analysis.complexity import Complexity
|
|
7
|
+
from code_insight.code_analysis.readability import Readability
|
|
8
|
+
from code_insight.code_analysis.redundancy import Redundancy
|
|
5
9
|
from code_insight.code_analysis.struct import Struct
|
|
6
10
|
from code_insight.code_analysis.style import Style
|
|
7
11
|
|
|
@@ -11,6 +15,7 @@ class CodeAnalysisType(StrEnum):
|
|
|
11
15
|
コード解析タイプ
|
|
12
16
|
* スタイル
|
|
13
17
|
* 構造
|
|
18
|
+
* アルゴリズム
|
|
14
19
|
* 複雑度
|
|
15
20
|
* 冗長度
|
|
16
21
|
* 可読性
|
|
@@ -19,6 +24,10 @@ class CodeAnalysisType(StrEnum):
|
|
|
19
24
|
|
|
20
25
|
STYLE = auto()
|
|
21
26
|
STRUCT = auto()
|
|
27
|
+
READABILITY = auto()
|
|
28
|
+
REDUNDANCY = auto()
|
|
29
|
+
ALGORITHM = auto()
|
|
30
|
+
COMPLEXITY = auto()
|
|
22
31
|
|
|
23
32
|
@staticmethod
|
|
24
33
|
def get_code_analysis_class(type: str) -> AbstractAnalysis[Any]:
|
|
@@ -27,6 +36,14 @@ class CodeAnalysisType(StrEnum):
|
|
|
27
36
|
return Style()
|
|
28
37
|
elif type == CodeAnalysisType.STRUCT:
|
|
29
38
|
return Struct()
|
|
39
|
+
elif type == CodeAnalysisType.READABILITY:
|
|
40
|
+
return Readability()
|
|
41
|
+
elif type == CodeAnalysisType.REDUNDANCY:
|
|
42
|
+
return Redundancy()
|
|
43
|
+
elif type == CodeAnalysisType.ALGORITHM:
|
|
44
|
+
return Algorithm()
|
|
45
|
+
elif type == CodeAnalysisType.COMPLEXITY:
|
|
46
|
+
return Complexity()
|
|
30
47
|
else:
|
|
31
48
|
raise ValueError(f"Invalid code analysis type: {type}")
|
|
32
49
|
|
|
@@ -36,6 +53,10 @@ class CodeAnalysis:
|
|
|
36
53
|
|
|
37
54
|
source_code: str
|
|
38
55
|
|
|
56
|
+
def __init__(self, source_code: str) -> None:
|
|
57
|
+
"""コンストラクタ"""
|
|
58
|
+
self.source_code = source_code
|
|
59
|
+
|
|
39
60
|
def analyze(
|
|
40
61
|
self, types: list[CodeAnalysisType]
|
|
41
62
|
) -> dict[CodeAnalysisType, Type[BaseAnalysisResult]]:
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
from typing import Sequence
|
|
2
|
+
|
|
3
|
+
import matplotlib.pyplot as plt
|
|
4
|
+
import numpy as np
|
|
5
|
+
from sklearn.cluster import KMeans
|
|
6
|
+
from sklearn.decomposition import PCA
|
|
7
|
+
|
|
8
|
+
from code_insight.code_analysis.abstract import BaseAnalysisResult
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TrendAnalysis:
|
|
12
|
+
"""コード解析結果分析"""
|
|
13
|
+
|
|
14
|
+
code_labels: list[str]
|
|
15
|
+
code_analysis_list: list[dict[str, float]]
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
code_analysis_results: Sequence[Sequence[BaseAnalysisResult]],
|
|
20
|
+
code_labels: list[str] | None = None,
|
|
21
|
+
) -> None:
|
|
22
|
+
"""コンストラクタ"""
|
|
23
|
+
self.code_labels = code_labels if code_labels else []
|
|
24
|
+
self.code_analysis_list: list[dict[str, float]] = [
|
|
25
|
+
{
|
|
26
|
+
**{
|
|
27
|
+
k: float(v)
|
|
28
|
+
for d in [res.model_dump() for res in code_analysis_result]
|
|
29
|
+
for k, v in d.items()
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
for code_analysis_result in code_analysis_results
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
def extract_value(self, keys: list[str] | None = None) -> np.ndarray:
|
|
36
|
+
"""
|
|
37
|
+
任意のkeyの値を抽出
|
|
38
|
+
* keysが空ならすべてのkeyを抽出する
|
|
39
|
+
"""
|
|
40
|
+
if not keys:
|
|
41
|
+
return np.array(
|
|
42
|
+
[
|
|
43
|
+
[value for value in code_analysis.values()]
|
|
44
|
+
for code_analysis in self.code_analysis_list
|
|
45
|
+
]
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
return np.array(
|
|
49
|
+
[
|
|
50
|
+
[code_analysis[key] for key in keys]
|
|
51
|
+
for code_analysis in self.code_analysis_list
|
|
52
|
+
]
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def compress(self, keys: list[str] | None = None, dimention: int = 2) -> np.ndarray:
|
|
56
|
+
"""任意のkeyの値を圧縮"""
|
|
57
|
+
pca = PCA(n_components=dimention)
|
|
58
|
+
return pca.fit_transform(self.extract_value(keys))
|
|
59
|
+
|
|
60
|
+
def cluster_values(
|
|
61
|
+
self, keys: list[str] | None = None, cluster: int = 2
|
|
62
|
+
) -> np.ndarray:
|
|
63
|
+
"""任意のkeyの値をクラスタリング"""
|
|
64
|
+
kmeans = KMeans(n_clusters=cluster)
|
|
65
|
+
return kmeans.fit_predict(self.extract_value(keys))
|
|
66
|
+
|
|
67
|
+
def output_image(
|
|
68
|
+
self,
|
|
69
|
+
output_file: str = "clusters.png",
|
|
70
|
+
keys: list[str] | None = None,
|
|
71
|
+
cluster: int = 2,
|
|
72
|
+
dimention: int = 2,
|
|
73
|
+
) -> None:
|
|
74
|
+
"""任意のkeyの値を圧縮して画像として出力"""
|
|
75
|
+
X = self.extract_value(keys)
|
|
76
|
+
|
|
77
|
+
# KMeansクラスタリング
|
|
78
|
+
kmeans = KMeans(n_clusters=cluster, random_state=42)
|
|
79
|
+
cluster_labels = kmeans.fit_predict(X)
|
|
80
|
+
|
|
81
|
+
# 高次元なら2次元に圧縮して可視化 (PCA)
|
|
82
|
+
if X.shape[1] > 2:
|
|
83
|
+
X_vis = PCA(n_components=dimention).fit_transform(X)
|
|
84
|
+
else:
|
|
85
|
+
X_vis = X
|
|
86
|
+
|
|
87
|
+
# プロット
|
|
88
|
+
plt.figure(figsize=(8, 6))
|
|
89
|
+
for c in range(cluster):
|
|
90
|
+
idx = cluster_labels == c
|
|
91
|
+
plt.scatter(X_vis[idx, 0], X_vis[idx, 1], label=f"Cluster {c}", alpha=0.6)
|
|
92
|
+
# 各点にラベルを描画
|
|
93
|
+
for i in np.where(idx)[0]:
|
|
94
|
+
plt.text(
|
|
95
|
+
X_vis[i, 0] + 0.02,
|
|
96
|
+
X_vis[i, 1] + 0.02,
|
|
97
|
+
self.code_labels[i],
|
|
98
|
+
fontsize=8,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
plt.title("Clustering Result")
|
|
102
|
+
plt.legend(title="Clusters")
|
|
103
|
+
plt.savefig(output_file, dpi=150, bbox_inches="tight")
|
|
104
|
+
plt.close()
|
|
@@ -5,6 +5,10 @@ src/code_insight/core.py
|
|
|
5
5
|
src/code_insight/py.typed
|
|
6
6
|
src/code_insight/code_analysis/__init__.py
|
|
7
7
|
src/code_insight/code_analysis/abstract.py
|
|
8
|
+
src/code_insight/code_analysis/algorithm.py
|
|
9
|
+
src/code_insight/code_analysis/complexity.py
|
|
10
|
+
src/code_insight/code_analysis/readability.py
|
|
11
|
+
src/code_insight/code_analysis/redundancy.py
|
|
8
12
|
src/code_insight/code_analysis/struct.py
|
|
9
13
|
src/code_insight/code_analysis/style.py
|
|
10
14
|
src/code_insight/trend_analysis/__init__.py
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
from .code_analysis.struct import Struct, StructAnalysisResult
|
|
2
|
-
from .code_analysis.style import Style, StyleAnalysisResult
|
|
3
|
-
from .core import CodeAnalysis
|
|
4
|
-
from .trend_analysis.trend_analysis import TrendAnalysis
|
|
5
|
-
|
|
6
|
-
__all__ = [
|
|
7
|
-
"CodeAnalysis",
|
|
8
|
-
"Style",
|
|
9
|
-
"StyleAnalysisResult",
|
|
10
|
-
"Struct",
|
|
11
|
-
"StructAnalysisResult",
|
|
12
|
-
"TrendAnalysis",
|
|
13
|
-
]
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
from typing import Sequence
|
|
2
|
-
|
|
3
|
-
import numpy as np
|
|
4
|
-
from sklearn.cluster import KMeans
|
|
5
|
-
from sklearn.decomposition import PCA
|
|
6
|
-
|
|
7
|
-
from code_insight.code_analysis.abstract import BaseAnalysisResult
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class TrendAnalysis:
|
|
11
|
-
"""コード解析結果分析"""
|
|
12
|
-
|
|
13
|
-
code_analysis: list[dict[str, float]]
|
|
14
|
-
|
|
15
|
-
def __init__(
|
|
16
|
-
self, code_analysis_results: Sequence[Sequence[BaseAnalysisResult]]
|
|
17
|
-
) -> None:
|
|
18
|
-
"""コンストラクタ"""
|
|
19
|
-
self.code_analysis: list[dict[str, float]] = [
|
|
20
|
-
{
|
|
21
|
-
**{
|
|
22
|
-
k: float(v)
|
|
23
|
-
for d in [res.model_dump() for res in code_analysis_result]
|
|
24
|
-
for k, v in d.items()
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
for code_analysis_result in code_analysis_results
|
|
28
|
-
]
|
|
29
|
-
|
|
30
|
-
def extract_value(self, keys: list[str]) -> np.ndarray:
|
|
31
|
-
"""任意のkeyの値を抽出"""
|
|
32
|
-
return np.array([[d[key] for key in keys] for d in self.code_analysis])
|
|
33
|
-
|
|
34
|
-
def compress(self, keys: list[str], dimention: int = 2) -> np.ndarray:
|
|
35
|
-
"""任意のkeyの値を圧縮"""
|
|
36
|
-
pca = PCA(n_components=dimention)
|
|
37
|
-
return pca.fit_transform(self.extract_value(keys))
|
|
38
|
-
|
|
39
|
-
def cluster_values(self, keys: list[str], cluster: int = 2) -> np.ndarray:
|
|
40
|
-
"""任意のkeyの値をクラスタリング"""
|
|
41
|
-
kmeans = KMeans(n_clusters=cluster)
|
|
42
|
-
return kmeans.fit_predict(self.extract_value(keys))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|