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#

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_*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#

{
  "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

Hình 2.1: Các Node event được ghi theo định dạng JSONL.

6.2. Edge event#

{
  "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

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):

{
  "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):

{
  "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):

{
  "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):

{
  "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#

{
  "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#

{
  "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:

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;

  • ClassFunction 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 ClassFunction 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:

{
  "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:

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ợ:

# 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ý:

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:

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:

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.