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.
Files changed (24) hide show
  1. {codinsight-0.0.6 → codinsight-0.0.7}/PKG-INFO +1 -1
  2. {codinsight-0.0.6 → codinsight-0.0.7}/pyproject.toml +1 -1
  3. codinsight-0.0.7/src/code_insight/__init__.py +26 -0
  4. codinsight-0.0.7/src/code_insight/code_analysis/algorithm.py +189 -0
  5. codinsight-0.0.7/src/code_insight/code_analysis/complexity.py +212 -0
  6. codinsight-0.0.7/src/code_insight/code_analysis/readability.py +272 -0
  7. codinsight-0.0.7/src/code_insight/code_analysis/redundancy.py +220 -0
  8. {codinsight-0.0.6 → codinsight-0.0.7}/src/code_insight/core.py +21 -0
  9. codinsight-0.0.7/src/code_insight/trend_analysis/trend_analysis.py +104 -0
  10. {codinsight-0.0.6 → codinsight-0.0.7}/src/codinsight.egg-info/PKG-INFO +1 -1
  11. {codinsight-0.0.6 → codinsight-0.0.7}/src/codinsight.egg-info/SOURCES.txt +4 -0
  12. codinsight-0.0.6/src/code_insight/__init__.py +0 -13
  13. codinsight-0.0.6/src/code_insight/trend_analysis/trend_analysis.py +0 -42
  14. {codinsight-0.0.6 → codinsight-0.0.7}/README.md +0 -0
  15. {codinsight-0.0.6 → codinsight-0.0.7}/setup.cfg +0 -0
  16. {codinsight-0.0.6 → codinsight-0.0.7}/src/code_insight/code_analysis/__init__.py +0 -0
  17. {codinsight-0.0.6 → codinsight-0.0.7}/src/code_insight/code_analysis/abstract.py +0 -0
  18. {codinsight-0.0.6 → codinsight-0.0.7}/src/code_insight/code_analysis/struct.py +0 -0
  19. {codinsight-0.0.6 → codinsight-0.0.7}/src/code_insight/code_analysis/style.py +0 -0
  20. {codinsight-0.0.6 → codinsight-0.0.7}/src/code_insight/py.typed +0 -0
  21. {codinsight-0.0.6 → codinsight-0.0.7}/src/code_insight/trend_analysis/__init__.py +0 -0
  22. {codinsight-0.0.6 → codinsight-0.0.7}/src/codinsight.egg-info/dependency_links.txt +0 -0
  23. {codinsight-0.0.6 → codinsight-0.0.7}/src/codinsight.egg-info/requires.txt +0 -0
  24. {codinsight-0.0.6 → codinsight-0.0.7}/src/codinsight.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codinsight
3
- Version: 0.0.6
3
+ Version: 0.0.7
4
4
  Summary: Add your description here
5
5
  Requires-Python: >=3.12
6
6
  Description-Content-Type: text/markdown
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "codinsight"
7
- version = "0.0.6"
7
+ version = "0.0.7"
8
8
  description = "Add your description here"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -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()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codinsight
3
- Version: 0.0.6
3
+ Version: 0.0.7
4
4
  Summary: Add your description here
5
5
  Requires-Python: >=3.12
6
6
  Description-Content-Type: text/markdown
@@ -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