# Chương 2: Incremental CPG Parser Service & Deterministic ID Generation

**Thành viên phụ trách**: Võ Ngọc Tiến

---

## 1. Bối cảnh, mục tiêu và phạm vi

Parser Service xử lý lần lượt các file Python đã tìm được ở Chapter 1 và tạo biểu diễn Code Property Graph (CPG) ở mức phù hợp với pipeline streaming. Kết quả gồm:

- Node biểu diễn file, khai báo, câu lệnh, biểu thức và biến;
- Cạnh AST mô tả cấu trúc mã nguồn;
- Cạnh CFG mô tả thứ tự và nhánh điều khiển;
- Cạnh DFG mô tả thao tác đọc, ghi và phụ thuộc dữ liệu;
- Cạnh `CALLS` mô tả lời gọi hàm;
- Metadata của từng file;
- Sự kiện lỗi để một file không hợp lệ không làm dừng toàn bộ tiến trình.

Dịch vụ hỗ trợ parse toàn bộ repository, parse một file và parse tăng dần các file mới hoặc đã thay đổi. Mỗi sự kiện có định danh ổn định để downstream có thể thực hiện `MERGE` hoặc upsert mà không tạo bản ghi trùng khi dữ liệu được phát lại.

---

## 2. Lựa chọn công nghệ

Nhóm sử dụng module `ast` trong thư viện chuẩn Python thay vì phụ thuộc vào một parser bên ngoài. Lựa chọn này có các ưu điểm:

1. Không phát sinh dependency cho bước phân tích cú pháp;
2. Cung cấp loại node, cấu trúc lồng nhau, dòng và cột;
3. Hỗ trợ cú pháp tương ứng với phiên bản Python đang chạy;
4. Cho phép xây dựng các luật duyệt riêng bằng `ast.NodeVisitor`.

Module `ast` chỉ tạo Abstract Syntax Tree, không tự sinh CFG hoặc DFG. Vì vậy, `cpg_parser.py` bổ sung các quy tắc cho thứ tự statement, nhánh điều kiện, vòng lặp, lời gọi hàm và thao tác đọc/ghi biến.

:::{important}
CFG và DFG của hệ thống là mô hình phân tích tĩnh nhẹ phục vụ bài lab. Chúng không thay thế các công cụ phân tích chương trình đầy đủ và không đảm bảo mô phỏng chính xác mọi hành vi runtime của Python.
:::

---

## 3. Kiến trúc Parser Service

```text
Python source files
        │
        ▼
File Discovery ──► danh sách đường dẫn đã chuẩn hóa
        │
        ▼
PythonCPGParser ──► Node / Edge / Metadata / ParserError
        │
        ├────────► JSONL files
        │
        └────────► Kafka topics
        │
        ▼
ParserStateStore ──► output/parser_state.json
```

Các module chính và trách nhiệm:

| Module | Trách nhiệm |
|---|---|
| `config.py` | Khai báo đường dẫn, tên output, schema version, Kafka topic và thư mục loại trừ |
| `file_discovery.py` | Tìm file Python, chuẩn hóa đường dẫn và tạo báo cáo discovery |
| `cpg_parser.py` | Parse AST, trích xuất node, edge, metadata và lỗi |
| `models.py` | Định nghĩa schema cho bốn loại sự kiện |
| `id_utils.py` | Chuẩn hóa đường dẫn, tính content hash và stable ID |
| `state_store.py` | Lưu hash và thống kê lần parse gần nhất của từng file |
| `event_writer.py` | Ghi JSONL hoặc gửi sự kiện tới Kafka |
| `main.py` | Cung cấp CLI và điều phối toàn bộ quy trình |

---

## 4. Quy trình xử lý một file

Mỗi file được xử lý độc lập theo các bước:

1. Chuẩn hóa đường dẫn tương đối so với thư mục gốc dự án;
2. Đọc nội dung bằng UTF-8;
3. Gọi `ast.parse()` để tạo AST;
4. Duyệt AST và sinh node cùng các cạnh CPG;
5. Tính SHA-256 của nội dung;
6. Tạo metadata gồm kích thước, số dòng, số node và số edge;
7. Ghi các sự kiện tới output đã chọn;
8. Cập nhật `parser_state.json` nếu file được parse thành công.

Parser chỉ giữ AST và danh sách sự kiện của file đang xử lý, thay vì tạo AST cho toàn bộ repository cùng lúc. Vì vậy, phần bộ nhớ chính của bước parse phụ thuộc vào file lớn nhất đang được xử lý. State store vẫn tăng theo số file vì cần giữ một bản ghi hash cho mỗi file.

Nếu `ast.parse()` hoặc thao tác đọc file phát sinh lỗi trong `PythonCPGParser`, kết quả của file đó được chuyển thành `ParserErrorEvent`; vòng lặp có thể tiếp tục với các file còn lại.

---

## 5. Thành phần CPG được trích xuất

### 5.1. Node

| Nhóm | Node tiêu biểu | Vai trò |
|---|---|---|
| Đơn vị mã nguồn | `File`, `Class`, `Function` | Biểu diễn file và khai báo |
| Câu lệnh | `Assign`, `If`, `For`, `AsyncFor`, `While`, `Return` | Biểu diễn các câu lệnh chính |
| Biểu thức | `Call`, `Condition` | Biểu diễn lời gọi và điều kiện |
| Dữ liệu | `Variable`, `Import` | Biểu diễn biến và import |
| Điều khiển | `Break`, `Continue`, branch marker | Làm rõ thân và nhánh điều khiển |

### 5.2. Edge

| Loại graph | Edge | Ý nghĩa |
|---|---|---|
| AST | `AST_CHILD`, `DEFINES`, `IMPORTS` | Quan hệ cấu trúc, khai báo và import |
| Call graph | `CALLS` | Quan hệ từ vị trí gọi tới thực thể được gọi |
| CFG | `NEXT_STATEMENT`, `CONTROL_CONDITION`, `CONTROL_TRUE`, `CONTROL_FALSE`, `LOOP_BODY`, `LOOP_ELSE`, `RETURNS` | Thứ tự và hướng luồng điều khiển |
| DFG | `ASSIGNS`, `PARAMETER`, `READS`, `WRITES`, `DATA_DEPENDENCY` | Thao tác và phụ thuộc dữ liệu cơ bản |

`NEXT_STATEMENT` biểu diễn thứ tự tĩnh giữa các statement trong cùng scope. Các cạnh `CONTROL_*` và `LOOP_*` bổ sung cấu trúc rẽ nhánh và vòng lặp. DFG theo dõi lần ghi gần nhất của biến trong scope hiện tại để tạo quan hệ phụ thuộc cơ bản.

---

## 6. Thiết kế sự kiện

Mỗi dòng JSONL là một sự kiện độc lập. Tất cả sự kiện đều có:

- `event_type`: loại sự kiện;
- `schema_version`: phiên bản schema, hiện tại là `1.0`;
- `event_time`: thời điểm sinh sự kiện.

### 6.1. Node event

```json
{
  "event_type": "node",
  "schema_version": "1.0",
  "event_time": "2026-07-23T15:50:05.682584+00:00",
  "node_id": "node_0123456789abcdef",
  "label": "Function",
  "file_path": "transformers/src/example.py",
  "name": "run",
  "line": 1,
  "column": 0,
  "end_line": 3,
  "properties": {}
}
```

![Mẫu sự kiện Node CPG](task2_node_events_sample.png)

*Hình 2.1: Các Node event được ghi theo định dạng JSONL.*

### 6.2. Edge event

```json
{
  "event_type": "edge",
  "schema_version": "1.0",
  "event_time": "2026-07-23T15:55:38.508290+00:00",
  "edge_id": "edge_0123456789abcdef",
  "type": "CALLS",
  "source_id": "node_source",
  "target_id": "node_target",
  "file_path": "transformers/src/example.py",
  "properties": {}
}
```

![Mẫu sự kiện Edge CPG](task2_edge_events_sample.png)

*Hình 2.2: Các Edge event chứa định danh nguồn, đích và loại quan hệ.*

### 6.2.1. Bổ sung mẫu cho từng loại edge CPG

**AST edge** - quan hệ cấu trúc cú pháp (`AST_CHILD`):
```json
{
  "event_type": "edge",
  "schema_version": "1.0",
  "event_time": "2026-07-23T15:55:38.508290+00:00",
  "edge_id": "edge_ast_a1b2c3d4e5f6a7b8",
  "type": "AST_CHILD",
  "source_id": "node_class_BertModel",
  "target_id": "node_func_forward",
  "file_path": "transformers/src/transformers/models/bert/modeling_bert.py",
  "properties": {"child_index": 0}
}
```
*Ý nghĩa: lớp `BertModel` chứa method `forward` - quan hệ cha-con trong cây AST.*

**CFG edge** - luồng điều khiển (`NEXT_STATEMENT`):
```json
{
  "event_type": "edge",
  "schema_version": "1.0",
  "event_time": "2026-07-23T15:55:38.508290+00:00",
  "edge_id": "edge_cfg_b2c3d4e5f6a7b8c9",
  "type": "NEXT_STATEMENT",
  "source_id": "node_assign_hidden_states",
  "target_id": "node_assign_attention_output",
  "file_path": "transformers/src/transformers/models/bert/modeling_bert.py",
  "properties": {}
}
```
*Ý nghĩa: câu lệnh gán `hidden_states = ...` thực hiện trước `attention_output = ...` - thứ tự luồng điều khiển.*

**DFG edge** - luồng dữ liệu (`READS`):
```json
{
  "event_type": "edge",
  "schema_version": "1.0",
  "event_time": "2026-07-23T15:55:38.508290+00:00",
  "edge_id": "edge_dfg_c3d4e5f6a7b8c9d0",
  "type": "READS",
  "source_id": "node_call_self_attention",
  "target_id": "node_var_hidden_states",
  "file_path": "transformers/src/transformers/models/bert/modeling_bert.py",
  "properties": {"scope": "forward"}
}
```
*Ý nghĩa: lời gọi `self.attention(...)` đọc giá trị từ biến `hidden_states` - phụ thuộc dữ liệu.*

**Call edge** - lời gọi hàm (`CALLS`):
```json
{
  "event_type": "edge",
  "schema_version": "1.0",
  "event_time": "2026-07-23T15:55:38.508290+00:00",
  "edge_id": "edge_call_d4e5f6a7b8c9d0e1",
  "type": "CALLS",
  "source_id": "node_func_forward",
  "target_id": "node_func_self_attention_forward",
  "file_path": "transformers/src/transformers/models/bert/modeling_bert.py",
  "properties": {"call_site_line": 487}
}
```
*Ý nghĩa: method `forward` gọi `self.attention.forward` - quan hệ call graph.*

### 6.3. Metadata event

```json
{
  "event_type": "metadata",
  "schema_version": "1.0",
  "event_time": "2026-07-23T15:55:38.508290+00:00",
  "file_path": "transformers/src/example.py",
  "content_hash": "sha256-of-file-content",
  "size_bytes": 1024,
  "line_count": 32,
  "node_count": 24,
  "edge_count": 41,
  "parser_version": "ast-basic-v1"
}
```

Metadata vừa cung cấp thống kê cho downstream, vừa chứa `content_hash` dùng để phát hiện thay đổi.

### 6.4. Parser error event

```json
{
  "event_type": "parser_error",
  "schema_version": "1.0",
  "event_time": "2026-07-23T15:55:38.508290+00:00",
  "file_path": "transformers/src/invalid.py",
  "error_type": "SyntaxError",
  "message": "invalid syntax"
}
```

Lỗi được cô lập theo file và ghi vào `parser_error_events.jsonl` khi output là file.

---

## 7. Cơ chế định danh ổn định

Các hàm trong `id_utils.py` tạo ID bằng SHA-256 và lấy 16 ký tự hexa đầu:

```text
node_id = "node_" + SHA256(label | stable_parts)[:16]

edge_id = "edge_" + SHA256(
    edge_type | source_id | target_id | stable_parts
)[:16]
```

`stable_parts` thay đổi theo loại node:

- `File` dùng đường dẫn tương đối;
- `Class` và `Function` dùng đường dẫn cùng qualified name;
- `Variable` dùng file, scope và tên biến;
- Các statement có thể dùng file, scope, tên, dòng và cột;
- Edge dùng loại cạnh, source ID, target ID, file và properties.

Thiết kế này giúp `Class` và `Function` giữ ID khi chỉ có thay đổi không liên quan ở các dòng phía trên. Tuy nhiên, ID của statement phụ thuộc dòng/cột có thể thay đổi khi statement được di chuyển. Vì vậy, “stable” ở đây nghĩa là ổn định khi các thành phần đầu vào của ID không đổi, không có nghĩa ID bất biến trước mọi chỉnh sửa mã nguồn.

Stable ID là nền tảng để Neo4j và MongoDB dùng khóa xác định khi `MERGE` hoặc upsert, từ đó hạn chế dữ liệu trùng khi cùng một event được phát lại.

---

## 8. Cơ chế xử lý tăng dần

Parser tính `content_hash` bằng SHA-256 trên toàn bộ nội dung file. Trạng thái gần nhất được lưu tại `output/parser_state.json`:

```json
{
  "updated_at": "2026-07-23T16:00:00+00:00",
  "files": {
    "transformers/src/example.py": {
      "content_hash": "sha256-of-file-content",
      "last_parsed_at": "2026-07-23T16:00:00+00:00",
      "node_count": 24,
      "edge_count": 41,
      "line_count": 32
    }
  }
}
```

Khi chạy `parse-changed`, parser thực hiện:

```text
Tính content_hash hiện tại
        │
        ▼
So sánh với parser_state.json
        │
        ├── Giống nhau ──► skipped
        │
        └── Khác / chưa có ──► parse file
                                  │
                                  ▼
                           cập nhật state
```

Các lệnh hỗ trợ:

```powershell
# Parse toàn bộ repository và ghi JSONL
python -m parser.main parse-all --output-target file

# Chỉ parse file mới hoặc đã thay đổi
python -m parser.main parse-changed --output-target file

# Parse một file cụ thể
python -m parser.main parse-file transformers/src/transformers/__init__.py --output-target file
```

Tùy chọn `--append` nối event mới vào file JSONL. Nếu không có `--append`, Event Writer tạo lại các file event cho lần chạy hiện tại. Vì output mặc định của CLI hiện là `kafka`, báo cáo luôn ghi rõ `--output-target file` khi cần tạo JSONL.

### 8.1. Quy trình kiểm chứng incremental parsing

Thực hiện ba lần liên tiếp trên cùng state:

| Lần chạy | Thay đổi trước khi chạy | Kết quả cần quan sát |
|---:|---|---|
| 1 | Không thay đổi file | `parsed = 0`, toàn bộ file được `skipped` |
| 2 | Sửa có kiểm soát một file | `parsed = 1`, các file còn lại được `skipped` |
| 3 | Không sửa thêm | `parsed = 0`, file vừa xử lý cũng được `skipped` |

Nên dùng một thay đổi có thể hoàn tác trên file thử nghiệm, chụp ba output terminal trong cùng một hình, rồi khôi phục nội dung file sau khi kiểm chứng. Không nên dùng số lượng `skipped` từ một commit khác vì tổng số file phụ thuộc kết quả discovery.

---

## 9. Thực thi toàn bộ repository

Lệnh tạo các file sự kiện vật lý:

```powershell
python -m parser.main parse-all --output-target file
```

Lần thực nghiệm tương ứng với kết quả discovery 4.611 file ghi nhận:

```text
Parsed 4611 file(s), skipped 0, errors 0,
wrote 2029594 node event(s) and 6062224 edge event(s).
```

| Chỉ số | Kết quả |
|---|---:|
| File được parse | **4.611** |
| File bị bỏ qua | **0** |
| Parser error | **0** |
| Node event | **2.029.594** |
| Edge event | **6.062.224** |
| Metadata event | **4.611** |

Vì mỗi file parse thành công tạo đúng một Metadata event và không có parser error, số Metadata event bằng số file được parse.

Các file đầu ra:

```text
output/events/node_events.jsonl
output/events/edge_events.jsonl
output/events/metadata_events.jsonl
output/events/parser_error_events.jsonl
output/parser_state.json
```

---

## 10. Đầu ra JSONL và Kafka

Parser hỗ trợ hai output target:

| Output target | Cách hoạt động |
|---|---|
| `file` | Ghi từng sự kiện vào file JSONL tương ứng |
| `kafka` | Gửi sự kiện trực tiếp tới topic được cấu hình |

Ánh xạ hiện tại trong `parser/config.py`:

| Luồng sự kiện | Kafka topic |
|---|---|
| Node | `cpg_node_events` |
| Edge | `cpg_edge_events` |
| Metadata | `cpg_metadata_events` |
| Parser error | `Error_parser` |

Chapter 2 chịu trách nhiệm bảo đảm payload có schema và stable ID. Việc kiểm tra topic, message delivery và xử lý downstream được trình bày ở các chapter tiếp theo.

---

## 11. Giới hạn của giải pháp

Hệ thống hiện có các giới hạn:

1. CFG là mô hình tĩnh đơn giản, chưa bao phủ đầy đủ exception flow, generator, context manager và mọi cấu trúc điều khiển Python;
2. DFG chỉ theo dõi phụ thuộc cơ bản trong scope hiện tại;
3. Chưa có type inference và phân giải symbol liên file đầy đủ;
4. Cạnh `CALLS` chưa thay thế một call graph liên thủ tục hoàn chỉnh;
5. ID của một số statement phụ thuộc dòng/cột và có thể đổi khi code được di chuyển;
6. Parser chưa phát delete event khi class, function hoặc file bị xóa;
7. State store giữ một bản ghi cho mỗi file nên vẫn tăng theo kích thước repository.

Để graph phản ánh chính xác thao tác xóa, downstream cần dọn dữ liệu theo `file_path`, hoặc parser phải được mở rộng bằng lifecycle/delete event.

---

## 12. Kết luận

Parser Service đã tạo được CPG nhẹ cho repository Python lớn, sinh bốn loại sự kiện có schema rõ ràng và xử lý từng file độc lập. Content hash hỗ trợ phát hiện file thay đổi, trong khi deterministic ID hỗ trợ upsert và replay idempotent ở downstream. Các giới hạn về CFG, DFG, call resolution và delete event được xác định rõ để tránh diễn giải kết quả vượt quá phạm vi của implementation hiện tại.
