parser-ci-AP 0.1.0__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.
File without changes
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.4
2
+ Name: parser-ci-AP
3
+ Version: 0.1.0
4
+ Summary: C code parser based on Clang
5
+ Home-page: https://github.com/yourusername/parser-ci
6
+ Author: Александр
7
+ Author-email: sanyapetrooo@gmail.com
8
+ License: MIT
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.13
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: chardet>=5.0.0
16
+ Requires-Dist: clang>=15.0.0
17
+ Requires-Dist: libclang>=18.0.0
18
+ Dynamic: author
19
+ Dynamic: author-email
20
+ Dynamic: classifier
21
+ Dynamic: description
22
+ Dynamic: description-content-type
23
+ Dynamic: home-page
24
+ Dynamic: license
25
+ Dynamic: license-file
26
+ Dynamic: requires-dist
27
+ Dynamic: requires-python
28
+ Dynamic: summary
29
+
30
+ # Parser CI — C Parser на базе Clang
31
+
32
+ Инструмент для анализа C-проектов. Работает на базе libclang. Извлекает полезную информацию из исходников: функции, переменные, типы, макросы и контекст.
33
+
34
+ ## Возможности
35
+
36
+ ### 🔧 На уровне проекта:
37
+ 1. Перечислить все пользовательские типы данных (структуры, перечисления, typedef и т.д.)
38
+ 2. Перечислить все файлы проекта
39
+ 3. Подсчитать количество строк (всего / без комментариев / только функциональных)
40
+ 4. Перечислить все define-ы (макросы)
41
+ 5. Перечислить все глобальные, static и extern переменные
42
+ 6. Перечислить все функции (глобальные и static отдельно)
43
+ 7. Разделить define-ы на системные и пользовательские
44
+
45
+ ### 📄 На уровне файла:
46
+ 1. Перечислить все функции в файле
47
+ 2. Перечислить все зависимости (#include)
48
+ 3. Все переменные (глобальные, static, extern)
49
+ 4. Все define-ы
50
+ 5. Все пользовательские типы
51
+ 6. Подсчёт строк (всего / только функциональных)
52
+ 7. Диагностика кода (clang diagnostics)
53
+
54
+ ### 🔁 На уровне функции:
55
+ 1. Аргументы и локальные переменные
56
+ 2. Имя, шапка, возвращаемый тип и значение
57
+ 3. Используемые глобальные переменные
58
+ 4. Вызовы других функций
59
+ 5. Кол-во строк кода
60
+ 6. Извлечение контекста для изолированной компиляции
61
+
62
+ ## Установка
63
+
64
+ ```bash
65
+ pip install parser-ci-AP
66
+
67
+ ## Пример использования
68
+ ```python
69
+ from parser_ci import Project
70
+ project = Project("/path/to/c/project")
71
+ print(project.functions)
72
+
73
+ ## Структура проекта
74
+
75
+ parser-ci/
76
+ ├── parser_ci_AP/
77
+ │ ├── __init__.py
78
+ │ ├── define_info.py
79
+ │ ├── errors_info.py
80
+ │ ├── function_info.py
81
+ │ ├── logger.py
82
+ │ ├── project.py
83
+ │ ├── project_file.py
84
+ │ ├── type_info.py
85
+ │ └── variable_info.py
86
+ ├── README.md
87
+ ├── setup.py
88
+ ├── LICENSE
@@ -0,0 +1,59 @@
1
+ # Parser CI — C Parser на базе Clang
2
+
3
+ Инструмент для анализа C-проектов. Работает на базе libclang. Извлекает полезную информацию из исходников: функции, переменные, типы, макросы и контекст.
4
+
5
+ ## Возможности
6
+
7
+ ### 🔧 На уровне проекта:
8
+ 1. Перечислить все пользовательские типы данных (структуры, перечисления, typedef и т.д.)
9
+ 2. Перечислить все файлы проекта
10
+ 3. Подсчитать количество строк (всего / без комментариев / только функциональных)
11
+ 4. Перечислить все define-ы (макросы)
12
+ 5. Перечислить все глобальные, static и extern переменные
13
+ 6. Перечислить все функции (глобальные и static отдельно)
14
+ 7. Разделить define-ы на системные и пользовательские
15
+
16
+ ### 📄 На уровне файла:
17
+ 1. Перечислить все функции в файле
18
+ 2. Перечислить все зависимости (#include)
19
+ 3. Все переменные (глобальные, static, extern)
20
+ 4. Все define-ы
21
+ 5. Все пользовательские типы
22
+ 6. Подсчёт строк (всего / только функциональных)
23
+ 7. Диагностика кода (clang diagnostics)
24
+
25
+ ### 🔁 На уровне функции:
26
+ 1. Аргументы и локальные переменные
27
+ 2. Имя, шапка, возвращаемый тип и значение
28
+ 3. Используемые глобальные переменные
29
+ 4. Вызовы других функций
30
+ 5. Кол-во строк кода
31
+ 6. Извлечение контекста для изолированной компиляции
32
+
33
+ ## Установка
34
+
35
+ ```bash
36
+ pip install parser-ci-AP
37
+
38
+ ## Пример использования
39
+ ```python
40
+ from parser_ci import Project
41
+ project = Project("/path/to/c/project")
42
+ print(project.functions)
43
+
44
+ ## Структура проекта
45
+
46
+ parser-ci/
47
+ ├── parser_ci_AP/
48
+ │ ├── __init__.py
49
+ │ ├── define_info.py
50
+ │ ├── errors_info.py
51
+ │ ├── function_info.py
52
+ │ ├── logger.py
53
+ │ ├── project.py
54
+ │ ├── project_file.py
55
+ │ ├── type_info.py
56
+ │ └── variable_info.py
57
+ ├── README.md
58
+ ├── setup.py
59
+ ├── LICENSE
@@ -0,0 +1,55 @@
1
+ from .project import Project
2
+ from .project_file import AbstractProjectFile
3
+ from .type_info import BaseTypeInfo
4
+ from .function_info import FunctionInfo
5
+ from .variable_info import VariableInfo
6
+ from .define_info import DefineInfo
7
+ from .errors_info import DiagnosticInfo
8
+
9
+ __all__ = [
10
+ "Project",
11
+ "AbstractProjectFile",
12
+ "BaseTypeInfo",
13
+ "FunctionInfo",
14
+ "VariableInfo",
15
+ "DefineInfo",
16
+ "DiagnosticInfo"
17
+ ]
18
+
19
+
20
+
21
+ # Что может парсер
22
+
23
+ # На уровне проекта:
24
+ # 1) Перечислить все пользовательские типы данных (структуры, перечисления, typedef и т.д.);
25
+ # 2) Перечислить все файлы проекта;
26
+ # 3) Подсчитать количество строк (всего / без комментариев / только функциональных);
27
+ # 4) Перечислить все define-ы (макросы) проекта;
28
+ # 5) Перечислить все глобальные переменные проекта;
29
+ # 6) Перечислить все статические переменные проекта;
30
+ # 7) Перечислить все extern-переменные проекта;
31
+ # 8) Перечислить все функции проекта (глобальные и static отдельно);
32
+ # 9) Разделить все define-ы на системные и пользовательские;
33
+
34
+ # На уровне файла:
35
+ # 1) Перечислить все функции в файле (включая static и обычные);
36
+ # 2) Перечислить все зависимости (файлы, подключённые через #include);
37
+ # 3) Перечислить все переменные в файле (глобальные, static, extern);
38
+ # 4) Перечислить все define-ы в файле;
39
+ # 5) Перечислить все пользовательские типы данных в файле;
40
+ # 6) Подсчитать количество строк (всего / только функциональных);
41
+ # 7) Диагностика кода клангом
42
+
43
+ # На уровне функции:
44
+ # 1) Перечислить аргументы функции;
45
+ # 2) Перечислить локальные переменные;
46
+ # 3) Имя функции;
47
+ # 4) Получить шапку функции (прототип или полное объявление);
48
+ # 5) Извлечь из проекта весь контекст функции, чтобы можно было запустить её изолированно;
49
+ # 6) Перечислить глобальные переменные, используемые внутри функции;
50
+ # 7) Найти вызовы других функций внутри тела;
51
+ # 8) Определить тип возвращаемого значения;
52
+ # 9) Получить выражение, возвращаемое из функции (например: return x + 1 → "x + 1");
53
+ # 10) Число строк кода
54
+
55
+
@@ -0,0 +1,116 @@
1
+ from typing import Optional, List
2
+ from pathlib import Path
3
+ from clang.cindex import Cursor
4
+
5
+
6
+ class DefineInfo:
7
+ def __init__(self, cursor: Cursor, encoding: str):
8
+ self._cursor = cursor
9
+ self.name: str = cursor.spelling
10
+ self.location: Path = (
11
+ Path(cursor.location.file.name).resolve()
12
+ if cursor.location.file
13
+ else Path()
14
+ )
15
+ try:
16
+ self._usr: str = cursor.get_usr()
17
+ except (AttributeError, TypeError, UnicodeDecodeError):
18
+ self._usr = f"{self.name}@{self.location}"
19
+ self.encoding = encoding
20
+
21
+ self._raw_line: str
22
+ self.params: Optional[List[str]]
23
+ self.value: str
24
+ self.comment: str
25
+ self.is_system: bool
26
+
27
+ self._raw_line = self._read_raw_line()
28
+ self.params = self._extract_params()
29
+ self.value = self._extract_value()
30
+ self.comment = self._extract_comment_after()
31
+ self.is_system = self._detect_system_define() # Дефайны которые не используются кодом напрямую
32
+
33
+ def _detect_system_define(self) -> bool:
34
+ """
35
+ Определяет, является ли define системным:
36
+ - начинается с '_'
37
+ - или у него отсутствует значение (value)
38
+ """
39
+ return self.name.startswith("_") or not self.value
40
+
41
+ def _read_raw_line(self) -> str:
42
+ """
43
+ Считывает строку исходника, содержащую #define.
44
+ """
45
+ try:
46
+ line_num = self._cursor.location.line
47
+ with open(self.location, "r", encoding=self.encoding, errors="replace") as f:
48
+ lines = f.readlines()
49
+ return lines[line_num - 1].strip()
50
+ except Exception as e:
51
+ return f"[!] Ошибка чтения строки define {self.name}: {e}"
52
+
53
+ def _extract_params(self) -> Optional[List[str]]:
54
+ """
55
+ Пытается извлечь параметры, если define является макросом с параметрами.
56
+ Пример: #define MAX(a, b) ((a) > (b) ? (a) : (b))
57
+ """
58
+ try:
59
+ after_name = self._raw_line.split(self.name, 1)[1].lstrip()
60
+ if after_name.startswith("("):
61
+ end_idx = after_name.find(")")
62
+ if end_idx != -1:
63
+ param_str = after_name[1:end_idx]
64
+ params = [p.strip() for p in param_str.split(",") if p.strip()]
65
+ return params
66
+ except Exception:
67
+ pass
68
+ return None
69
+
70
+ def _extract_value(self) -> str:
71
+ """
72
+ Извлекает значение define (до начала комментария, если он есть).
73
+ """
74
+ try:
75
+ parts = self._raw_line.split(None, 2)
76
+ if len(parts) < 3:
77
+ return ""
78
+
79
+ value_part = parts[2]
80
+ # обрезаем комментарий (если есть)
81
+ for comment_start in ["//", "/*"]:
82
+ if comment_start in value_part:
83
+ value_part = value_part.split(comment_start, 1)[0].strip()
84
+ return value_part.strip()
85
+ except Exception:
86
+ return ""
87
+
88
+ def _extract_comment_after(self) -> str:
89
+ """
90
+ Извлекает комментарий после значения define (если он есть).
91
+ """
92
+ try:
93
+ for marker in ["//", "/*"]:
94
+ if marker in self._raw_line:
95
+ return self._raw_line.split(marker, 1)[1].strip()
96
+ return ""
97
+ except Exception:
98
+ return ""
99
+
100
+ def describe(self) -> str:
101
+ loc_str = f"{self.location}:{self._cursor.location.line}"
102
+ system_flag = "[system] " if self.is_system else ""
103
+ if self.params:
104
+ return f"{system_flag}#define {self.name}({', '.join(self.params)}) {self.value} // from {loc_str}"
105
+ return f"{system_flag}#define {self.name} {self.value} // from {loc_str}"
106
+
107
+ def __str__(self) -> str:
108
+ return self.describe()
109
+
110
+ def __eq__(self, other):
111
+ if not isinstance(other, DefineInfo):
112
+ return False
113
+ return self._usr == other._usr
114
+
115
+ def __hash__(self):
116
+ return hash(self._usr)
@@ -0,0 +1,32 @@
1
+ from clang.cindex import Diagnostic
2
+
3
+ class DiagnosticInfo:
4
+ def __init__(self, diag: Diagnostic):
5
+ self.severity = diag.severity # 1=warning, 2=error, 3=fatal, ...
6
+ self.spelling = diag.spelling
7
+ self.location = f"{diag.location.file}:{diag.location.line}"
8
+ self.category = diag.category_name
9
+ self.option = diag.option
10
+
11
+ def __eq__(self, other):
12
+ if not isinstance(other, DiagnosticInfo):
13
+ return False
14
+ return (
15
+ self.severity == other.severity and
16
+ self.spelling == other.spelling and
17
+ self.location == other.location and
18
+ self.category == other.category and
19
+ self.option == other.option
20
+ )
21
+
22
+ def __hash__(self):
23
+ return hash((
24
+ self.severity,
25
+ self.spelling,
26
+ self.location,
27
+ self.category,
28
+ self.option
29
+ ))
30
+
31
+ def __str__(self):
32
+ return f"[{self.severity}] {self.location}: {self.spelling}"
@@ -0,0 +1,278 @@
1
+ from clang.cindex import Cursor, CursorKind, StorageClass
2
+ from pathlib import Path
3
+ import re
4
+ from typing import Set, Optional
5
+
6
+ from .variable_info import VariableInfo
7
+
8
+ class FunctionInfo:
9
+ def __init__(self, cursor: Cursor, encoding: str):
10
+ self._cursor = cursor
11
+ self.name = cursor.spelling
12
+ self.location = Path(cursor.location.file.name).resolve() if cursor.location.file else Path()
13
+ try:
14
+ self._usr = cursor.get_usr() # Уникальный идентификатор функции
15
+ except Exception:
16
+ self._usr = f"{self.name}@{self.location}"
17
+ self.encoding = encoding
18
+
19
+ self.return_var: Optional[ReturnInfo] = None
20
+ self.args: list[VariableInfo] = []
21
+ self.source_text: str
22
+ self.is_static: bool = False
23
+ self.comment: str
24
+ self.lines_functional: int
25
+ self.local_variables: list[VariableInfo] = []
26
+ self.called_funcs: Set[str] = set()
27
+ self.lines_pure_code: int
28
+
29
+ self.source_text = self._extract_source_text()
30
+ self.comment = self._extract_comments()
31
+ self._count_functional_lines()
32
+ self._extract_args()
33
+ self.external_variables = set()
34
+ self._extract_external_variables()
35
+ self.return_var = self._extract_return_variable()
36
+ self._detect_static()
37
+ self._count_pure_code_lines()
38
+
39
+ self._extract_from_cursor()
40
+
41
+ def _count_pure_code_lines(self):
42
+ """
43
+ Подсчитывает количество строк, содержащих только чистый код:
44
+ - без комментариев (// и /* */)
45
+ - без пустых строк
46
+ """
47
+ code = self.source_text
48
+
49
+ # Удаляем многострочные комментарии
50
+ code = re.sub(r"/\*.*?\*/", "", code, flags=re.DOTALL)
51
+
52
+ # Удаляем однострочные комментарии
53
+ code = re.sub(r"//.*", "", code)
54
+
55
+ # Оставляем только строки с кодом
56
+ self.lines_pure_code = sum(1 for line in code.splitlines() if line.strip())
57
+
58
+ def _extract_return_variable(self) -> Optional["ReturnInfo"]:
59
+ """
60
+ Извлекает информацию о возвращаемом значении функции:
61
+ - тип возврата
62
+ - выражение после return (если есть)
63
+ """
64
+ return_type = self._cursor.result_type.spelling
65
+ return_expr = None
66
+
67
+ def visit(node: Cursor):
68
+ nonlocal return_expr
69
+ if node.kind.name == "RETURN_STMT":
70
+ tokens = [t.spelling for t in node.get_tokens()]
71
+ if len(tokens) > 1:
72
+ return_expr = " ".join(tokens[1:]) # Пропускаем 'return'
73
+ for child in node.get_children():
74
+ visit(child)
75
+
76
+ visit(self._cursor)
77
+
78
+ if return_expr is None:
79
+ return None
80
+ return ReturnInfo(return_type, return_expr)
81
+
82
+ def _detect_static(self):
83
+ """
84
+ Определяет, является ли функция статической, используя только canonical-курсоры.
85
+ """
86
+ try:
87
+ canonical = self._cursor.canonical
88
+ if canonical and canonical.storage_class == StorageClass.STATIC:
89
+ self.is_static = True
90
+ except Exception:
91
+ pass
92
+
93
+ return False
94
+
95
+ def _extract_args(self):
96
+ """
97
+ Извлекает аргументы функции как объекты VariableInfo.
98
+ """
99
+ self.args = []
100
+ for arg in self._cursor.get_arguments():
101
+ if arg.kind == CursorKind.PARM_DECL and arg.spelling:
102
+ self.args.append(VariableInfo(arg, self.encoding))
103
+
104
+ def _extract_source_text(self):
105
+ extent = self._cursor.extent
106
+ start = extent.start
107
+ end = extent.end
108
+
109
+ def extract_lines(lines):
110
+ lines = lines[start.line - 1:end.line]
111
+ lines[0] = lines[0][start.column - 1:]
112
+ lines[-1] = lines[-1][:end.column - 1]
113
+ return "".join(lines).strip()
114
+
115
+ try:
116
+ with open(self.location, "r", encoding=self.encoding, errors="replace") as f:
117
+ return extract_lines(f.readlines())
118
+ except Exception as e:
119
+ return f"[!] Ошибка извлечения текста функции {self.name}: {e}"
120
+
121
+ def _extract_comments(self) -> str:
122
+ """
123
+ Пытается извлечь комментарии, расположенные перед началом функции.
124
+ Поддерживает:
125
+ - однострочные // и ///
126
+ - многострочные /* ... */
127
+ Останавливается при встрече пустой строки.
128
+ """
129
+ try:
130
+ with open(self.location, "r", encoding=self.encoding, errors="replace") as f:
131
+ lines = f.readlines()
132
+
133
+ start_line = self._cursor.extent.start.line
134
+ comments = []
135
+ inside_block = False
136
+
137
+ for i in range(start_line - 2, -1, -1):
138
+ line = lines[i].rstrip()
139
+
140
+ if not line.strip() and not inside_block:
141
+ break # пустая строка прерывает комментарий
142
+
143
+ # Обрабатываем многострочные комментарии (/* ... */)
144
+ if "*/" in line:
145
+ inside_block = True
146
+ comments.insert(0, line)
147
+ continue
148
+
149
+ if inside_block:
150
+ comments.insert(0, line)
151
+ if "/*" in line:
152
+ inside_block = False
153
+ continue
154
+
155
+ # Однострочные комментарии
156
+ stripped = line.strip()
157
+ if stripped.startswith("//") or stripped.startswith("///"):
158
+ comments.insert(0, line)
159
+ continue
160
+
161
+ break # не комментарий и не внутри блока — прерываем
162
+
163
+ return "\n".join(comments).strip()
164
+
165
+ except Exception as e:
166
+ return f"[!] Ошибка при извлечении комментариев функции {self.name}: {e}"
167
+
168
+ def _count_functional_lines(self):
169
+ """
170
+ Подсчитывает количество строк с функциональным кодом.
171
+ Убирает комментарии `//`, `/* */` и пустые строки.
172
+ """
173
+ code = self.source_text
174
+
175
+ # Удаляем многострочные комментарии (включая переносы строк)
176
+ code = re.sub(r'/\*.*?\*/', '', code, flags=re.DOTALL)
177
+
178
+ # Удаляем однострочные комментарии
179
+ code = re.sub(r'//.*', '', code)
180
+
181
+ # Разбиваем на строки и фильтруем только те, что содержат код
182
+ lines = code.splitlines()
183
+ functional_lines = [line for line in lines if line.strip()] # только непустые
184
+
185
+ self.lines_functional = len(functional_lines)
186
+
187
+ def _extract_from_cursor(self):
188
+
189
+ def visit(node: Cursor):
190
+ if node.kind == CursorKind.VAR_DECL:
191
+ self.local_variables.append(VariableInfo(node, self.encoding))
192
+ elif node.kind == CursorKind.CALL_EXPR:
193
+ called_name = node.spelling or node.displayname or "<unnamed_call>"
194
+ self.called_funcs.add(called_name)
195
+
196
+ for child in node.get_children():
197
+ visit(child)
198
+
199
+ visit(self._cursor)
200
+
201
+ def _extract_external_variables(self):
202
+ """
203
+ Находит все переменные, которые используются в функции, но не являются:
204
+ - её аргументами
205
+ - локальными переменными
206
+
207
+ Это потенциально глобальные или внешние переменные.
208
+ """
209
+ local_and_args = {var.name for var in self.local_variables}
210
+ local_and_args.update(arg.name for arg in self.args)
211
+
212
+ external_vars = set()
213
+
214
+ def visit(node: Cursor):
215
+ if node.kind == CursorKind.DECL_REF_EXPR:
216
+ name = node.spelling
217
+ if name and name not in local_and_args:
218
+ external_vars.add(name)
219
+
220
+ for child in node.get_children():
221
+ visit(child)
222
+
223
+ visit(self._cursor)
224
+
225
+ self.external_variables = external_vars
226
+
227
+ def describe(self):
228
+ out = [f"Function: {self.name} ({self.location})"]
229
+ out.append(f" Return type: {self.return_var}")
230
+ out.append(f" Static: {'yes' if self.is_static else 'no'}")
231
+ out.append(f" Functional lines: {self.lines_functional}")
232
+
233
+
234
+ if self.local_variables:
235
+ out.append(" Local variables:")
236
+ for var in self.local_variables:
237
+ out.append(f" - {var['type']} {var['name']}")
238
+
239
+ out.append(" Arguments:")
240
+ for arg in self.args:
241
+ out.append(f" - {arg['type']} {arg['name']}")
242
+
243
+ if self.called_funcs:
244
+ out.append(" Called functions:")
245
+ for name in self.called_funcs:
246
+ out.append(f" - {name}")
247
+
248
+ if self.external_variables:
249
+ out.append(f" External variables:")
250
+ for name in self.external_variables:
251
+ out.append(f" - {name}")
252
+
253
+ return "\n".join(out)
254
+
255
+ def __str__(self):
256
+ return self.describe()
257
+
258
+ def __eq__(self, other):
259
+ if not isinstance(other, FunctionInfo):
260
+ return False
261
+ return self._usr == other._usr
262
+
263
+ def __hash__(self):
264
+ return hash(self._usr)
265
+
266
+ class ReturnInfo:
267
+ def __init__(self, return_type: str, expression: Optional[str] = None):
268
+ self.type = return_type
269
+ self.expression = expression
270
+
271
+ def describe(self) -> str:
272
+ desc = f"Return type: {self.type}"
273
+ if self.expression:
274
+ desc += f", returns: {self.expression}"
275
+ return desc
276
+
277
+ def __str__(self):
278
+ return self.describe()
@@ -0,0 +1,3 @@
1
+ import logging
2
+
3
+ logger = logging.getLogger("parser_ci_AP")