devlensio 0.6.2 → 1.0.0
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.
- package/README.md +89 -31
- package/dist/extractors/detectLanguage.d.ts +2 -0
- package/dist/extractors/detectLanguage.js +33 -0
- package/dist/extractors/index.d.ts +6 -0
- package/dist/extractors/index.js +116 -0
- package/dist/extractors/runner.d.ts +7 -0
- package/dist/extractors/runner.js +194 -0
- package/dist/extractors/types.d.ts +37 -0
- package/dist/extractors/types.js +8 -0
- package/dist/graph/buildLookup.d.ts +1 -0
- package/dist/graph/buildLookup.js +2 -1
- package/dist/graph/edges/callEdges.js +19 -5
- package/dist/graph/edges/callEdges.test.d.ts +1 -0
- package/dist/graph/edges/callEdges.test.js +200 -0
- package/dist/graph/edges/importEdges.js +12 -0
- package/dist/graph/edges/inheritanceEdges.d.ts +3 -0
- package/dist/graph/edges/inheritanceEdges.js +73 -0
- package/dist/graph/edges/inheritanceEdges.test.d.ts +1 -0
- package/dist/graph/edges/inheritanceEdges.test.js +140 -0
- package/dist/graph/index.js +4 -0
- package/dist/parser/classes.test.d.ts +1 -0
- package/dist/parser/classes.test.js +360 -0
- package/dist/parser/extractors/classes.d.ts +5 -0
- package/dist/parser/extractors/classes.js +241 -0
- package/dist/parser/index.d.ts +2 -0
- package/dist/parser/index.js +7 -1
- package/dist/pipeline/index.d.ts +2 -1
- package/dist/pipeline/index.js +30 -42
- package/dist/scoring/index.js +8 -0
- package/dist/scoring/index.test.js +43 -0
- package/dist/scoring/nodeScorer.js +2 -0
- package/dist/scoring/pruneDisconnected.d.ts +8 -0
- package/dist/scoring/pruneDisconnected.js +66 -0
- package/dist/scoring/pruneDisconnected.test.d.ts +1 -0
- package/dist/scoring/pruneDisconnected.test.js +123 -0
- package/dist/summarizer/prompts.d.ts +1 -1
- package/dist/types.d.ts +5 -5
- package/extractors/go/bin/darwin-amd64/devlens_go_extractor +0 -0
- package/extractors/go/bin/darwin-arm64/devlens_go_extractor +0 -0
- package/extractors/go/bin/linux-amd64/devlens_go_extractor +0 -0
- package/extractors/go/bin/linux-arm64/devlens_go_extractor +0 -0
- package/extractors/go/bin/windows-amd64/devlens_go_extractor.exe +0 -0
- package/extractors/go/build.mjs +43 -0
- package/extractors/go/calls.go +289 -0
- package/extractors/go/contract.go +140 -0
- package/extractors/go/extractor.go +161 -0
- package/extractors/go/fingerprint.go +167 -0
- package/extractors/go/go.mod +3 -0
- package/extractors/go/imports.go +90 -0
- package/extractors/go/inheritance.go +138 -0
- package/extractors/go/lookup.go +106 -0
- package/extractors/go/main.go +57 -0
- package/extractors/go/nodes.go +177 -0
- package/extractors/go/orm_edges.go +277 -0
- package/extractors/go/parser.go +469 -0
- package/extractors/go/routes.go +521 -0
- package/extractors/go/tests.go +45 -0
- package/extractors/go/thirdparty.go +138 -0
- package/extractors/go/typeload.go +178 -0
- package/extractors/go/walker.go +67 -0
- package/extractors/java/build.mjs +90 -0
- package/extractors/java/devlens_java_extractor.jar +0 -0
- package/extractors/java/src/devlens/extractor/Contract.java +171 -0
- package/extractors/java/src/devlens/extractor/Extractor.java +262 -0
- package/extractors/java/src/devlens/extractor/ExtractorResult.java +12 -0
- package/extractors/java/src/devlens/extractor/Fingerprint.java +239 -0
- package/extractors/java/src/devlens/extractor/LookupMaps.java +123 -0
- package/extractors/java/src/devlens/extractor/Main.java +66 -0
- package/extractors/java/src/devlens/extractor/Parser.java +522 -0
- package/extractors/java/src/devlens/extractor/SourceWalker.java +82 -0
- package/extractors/java/src/devlens/extractor/ThirdParty.java +141 -0
- package/extractors/java/src/devlens/extractor/TypeSolverFactory.java +43 -0
- package/extractors/java/src/devlens/extractor/edges/Calls.java +266 -0
- package/extractors/java/src/devlens/extractor/edges/Enrich.java +54 -0
- package/extractors/java/src/devlens/extractor/edges/Imports.java +154 -0
- package/extractors/java/src/devlens/extractor/edges/Inheritance.java +79 -0
- package/extractors/java/src/devlens/extractor/edges/OrmEdges.java +209 -0
- package/extractors/java/src/devlens/extractor/edges/Routes.java +143 -0
- package/extractors/java/src/devlens/extractor/edges/Tests.java +53 -0
- package/extractors/python/devlens_extractors_python/__init__.py +3 -0
- package/extractors/python/devlens_extractors_python/__main__.py +33 -0
- package/extractors/python/devlens_extractors_python/contract.py +94 -0
- package/extractors/python/devlens_extractors_python/edges/__init__.py +28 -0
- package/extractors/python/devlens_extractors_python/edges/calls.py +112 -0
- package/extractors/python/devlens_extractors_python/edges/enrich.py +44 -0
- package/extractors/python/devlens_extractors_python/edges/imports.py +155 -0
- package/extractors/python/devlens_extractors_python/edges/inheritance.py +97 -0
- package/extractors/python/devlens_extractors_python/edges/orm_edges.py +225 -0
- package/extractors/python/devlens_extractors_python/edges/routes/__init__.py +28 -0
- package/extractors/python/devlens_extractors_python/edges/routes/common.py +79 -0
- package/extractors/python/devlens_extractors_python/edges/routes/decorators.py +212 -0
- package/extractors/python/devlens_extractors_python/edges/routes/django_urls.py +213 -0
- package/extractors/python/devlens_extractors_python/edges/routes/drf.py +128 -0
- package/extractors/python/devlens_extractors_python/edges/tests.py +53 -0
- package/extractors/python/devlens_extractors_python/extractor.py +107 -0
- package/extractors/python/devlens_extractors_python/fingerprint.py +201 -0
- package/extractors/python/devlens_extractors_python/lookup.py +72 -0
- package/extractors/python/devlens_extractors_python/parser/__init__.py +72 -0
- package/extractors/python/devlens_extractors_python/parser/classes.py +109 -0
- package/extractors/python/devlens_extractors_python/parser/functions.py +163 -0
- package/extractors/python/devlens_extractors_python/parser/walker.py +30 -0
- package/extractors/python/devlens_extractors_python/third_party.py +103 -0
- package/extractors/python/pyproject.toml +16 -0
- package/extractors/python/setup.mjs +54 -0
- package/extractors/rust/Cargo.toml +29 -0
- package/extractors/rust/bin/darwin-amd64/devlens_rust_extractor +0 -0
- package/extractors/rust/bin/darwin-arm64/devlens_rust_extractor +0 -0
- package/extractors/rust/bin/linux-amd64/devlens_rust_extractor +0 -0
- package/extractors/rust/bin/linux-arm64/devlens_rust_extractor +0 -0
- package/extractors/rust/bin/windows-amd64/devlens_rust_extractor.exe +0 -0
- package/extractors/rust/build.mjs +85 -0
- package/extractors/rust/src/calls.rs +295 -0
- package/extractors/rust/src/contract.rs +236 -0
- package/extractors/rust/src/enrich.rs +107 -0
- package/extractors/rust/src/extractor.rs +199 -0
- package/extractors/rust/src/fingerprint.rs +238 -0
- package/extractors/rust/src/imports.rs +101 -0
- package/extractors/rust/src/inheritance.rs +233 -0
- package/extractors/rust/src/lookup.rs +226 -0
- package/extractors/rust/src/main.rs +53 -0
- package/extractors/rust/src/module_map.rs +131 -0
- package/extractors/rust/src/nodes.rs +174 -0
- package/extractors/rust/src/orm_edges.rs +125 -0
- package/extractors/rust/src/parser.rs +859 -0
- package/extractors/rust/src/routes.rs +1265 -0
- package/extractors/rust/src/tests.rs +109 -0
- package/extractors/rust/src/thirdparty.rs +114 -0
- package/extractors/rust/src/walker.rs +82 -0
- package/package.json +22 -4
|
@@ -0,0 +1,859 @@
|
|
|
1
|
+
// parser.rs — parse-time FACT collection via syn. Everything an edge detector
|
|
2
|
+
// needs is collected HERE in one pass per file; detectors resolve from the
|
|
3
|
+
// facts via lookup maps, never re-walking ASTs (playbook rule).
|
|
4
|
+
//
|
|
5
|
+
// syn is SYNTACTIC — no type resolution (rust-analyzer would be V2). All
|
|
6
|
+
// name-based resolution downstream is flagged where heuristic.
|
|
7
|
+
|
|
8
|
+
use proc_macro2::{Span, TokenStream, TokenTree};
|
|
9
|
+
use quote::ToTokens;
|
|
10
|
+
use std::collections::BTreeSet;
|
|
11
|
+
use syn::spanned::Spanned;
|
|
12
|
+
use syn::visit::Visit;
|
|
13
|
+
|
|
14
|
+
// ── line index: (line, char-col) → byte offset (exact rawCode slicing) ──
|
|
15
|
+
|
|
16
|
+
pub struct LineIndex {
|
|
17
|
+
/// per line: byte offset of the line start, then byte offset of each
|
|
18
|
+
/// char boundary within the line (0-based char index → byte offset)
|
|
19
|
+
lines: Vec<LineEntry>,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
struct LineEntry {
|
|
23
|
+
byte_offset: usize,
|
|
24
|
+
char_offsets: Vec<usize>,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
impl LineIndex {
|
|
28
|
+
pub fn new(source: &str) -> Self {
|
|
29
|
+
let mut lines = vec![];
|
|
30
|
+
let mut line_start = 0usize;
|
|
31
|
+
for line in source.split('\n') {
|
|
32
|
+
let mut char_offsets = vec![0usize];
|
|
33
|
+
for (idx, (byte, _c)) in line.char_indices().enumerate() {
|
|
34
|
+
if idx > 0 {
|
|
35
|
+
char_offsets.push(byte);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
char_offsets.push(line.len());
|
|
39
|
+
lines.push(LineEntry {
|
|
40
|
+
byte_offset: line_start,
|
|
41
|
+
char_offsets,
|
|
42
|
+
});
|
|
43
|
+
line_start += line.len() + 1; // +1 for '\n'
|
|
44
|
+
}
|
|
45
|
+
LineIndex { lines }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/// line: 1-based, col: 1-based char column → byte offset into source.
|
|
49
|
+
fn byte_offset(&self, line: usize, col: usize) -> usize {
|
|
50
|
+
if line == 0 || line > self.lines.len() {
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
let entry = &self.lines[line - 1];
|
|
54
|
+
entry.byte_offset
|
|
55
|
+
+ entry
|
|
56
|
+
.char_offsets
|
|
57
|
+
.get(col.saturating_sub(1))
|
|
58
|
+
.copied()
|
|
59
|
+
.unwrap_or(0)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// Byte range of a proc_macro2 span (span.start()/end() are 1-based).
|
|
63
|
+
pub fn span_range(&self, span: Span) -> (usize, usize) {
|
|
64
|
+
let start = span.start();
|
|
65
|
+
let end = span.end();
|
|
66
|
+
(
|
|
67
|
+
self.byte_offset(start.line, start.column + 1),
|
|
68
|
+
self.byte_offset(end.line, end.column + 1),
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
pub fn line_count(&self) -> usize {
|
|
73
|
+
self.lines.len()
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ── parsed facts ──
|
|
78
|
+
|
|
79
|
+
#[derive(Clone)]
|
|
80
|
+
pub struct UseDecl {
|
|
81
|
+
/// alias in scope (last path segment, or explicit `as` name)
|
|
82
|
+
pub alias: String,
|
|
83
|
+
/// full target path (crate::foo::bar / axum::Router / super::x)
|
|
84
|
+
pub target: String,
|
|
85
|
+
/// true for `use foo::*;` / `use foo::{a::*, b};` glob portions
|
|
86
|
+
pub glob: bool,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
#[derive(Clone)]
|
|
90
|
+
pub struct TableDecl {
|
|
91
|
+
pub name: String,
|
|
92
|
+
pub columns: Vec<(String, String)>,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
#[derive(Clone, Default)]
|
|
96
|
+
pub struct AttrInfo {
|
|
97
|
+
/// attr path joined with :: (e.g. "actix_web::get", "derive", "diesel")
|
|
98
|
+
pub path: String,
|
|
99
|
+
/// raw arg tokens for list metas (e.g. `"/users/{id}"` or `table_name = users`)
|
|
100
|
+
pub args: String,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
#[derive(Clone)]
|
|
104
|
+
pub struct FieldInfo {
|
|
105
|
+
pub name: String,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
pub const KIND_FUNCTION: &str = "FUNCTION";
|
|
109
|
+
pub const KIND_METHOD: &str = "METHOD";
|
|
110
|
+
pub const KIND_STRUCT: &str = "STRUCT";
|
|
111
|
+
pub const KIND_ENUM: &str = "ENUM";
|
|
112
|
+
pub const KIND_TRAIT: &str = "TRAIT";
|
|
113
|
+
pub const KIND_IMPL_BLOCK: &str = "IMPL_BLOCK";
|
|
114
|
+
|
|
115
|
+
#[derive(Clone, Default)]
|
|
116
|
+
pub struct OrmCall {
|
|
117
|
+
/// "read" | "write"
|
|
118
|
+
pub kind: String,
|
|
119
|
+
/// table reference as written (e.g. "users::table", "users::dsl::users",
|
|
120
|
+
/// "users" — or the first arg of diesel::insert_into/update/delete)
|
|
121
|
+
pub table_ref: String,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
#[derive(Clone)]
|
|
125
|
+
pub struct ParsedItem {
|
|
126
|
+
pub kind: &'static str,
|
|
127
|
+
pub name: String,
|
|
128
|
+
pub start_line: i64,
|
|
129
|
+
pub end_line: i64,
|
|
130
|
+
pub raw_code: String,
|
|
131
|
+
pub attrs: Vec<AttrInfo>,
|
|
132
|
+
pub is_pub: bool,
|
|
133
|
+
pub is_async: bool,
|
|
134
|
+
/// method receiver: "&self" / "&mut self" / "self" / "" (free fn)
|
|
135
|
+
pub receiver: String,
|
|
136
|
+
pub calls: Vec<String>,
|
|
137
|
+
pub is_test: bool,
|
|
138
|
+
/// Diesel R/W call facts (collected alongside calls)
|
|
139
|
+
pub orm_calls: Vec<OrmCall>,
|
|
140
|
+
// kind-specific
|
|
141
|
+
pub fields: Vec<FieldInfo>,
|
|
142
|
+
pub variants: Vec<String>,
|
|
143
|
+
pub supertraits: Vec<String>,
|
|
144
|
+
pub derives: Vec<String>,
|
|
145
|
+
/// impl blocks: Some(path) when `impl Trait for Type`
|
|
146
|
+
pub impl_trait: Option<String>,
|
|
147
|
+
/// impl blocks: the target type path (e.g. "User", "Vec < T >" → "Vec<T>")
|
|
148
|
+
pub impl_target: String,
|
|
149
|
+
/// impl blocks: for trait impls, the trait path
|
|
150
|
+
pub impl_trait_path: Option<String>,
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
#[derive(Clone)]
|
|
154
|
+
pub struct ParsedFile {
|
|
155
|
+
pub rel_path: String,
|
|
156
|
+
/// the parsed AST — kept for detection passes that need expression
|
|
157
|
+
/// structure (routes.rs walks fn bodies for axum chains; mirrors go's
|
|
158
|
+
/// `Decl *ast.FuncDecl` retention)
|
|
159
|
+
#[allow(dead_code)]
|
|
160
|
+
pub ast: syn::File,
|
|
161
|
+
pub uses: Vec<UseDecl>,
|
|
162
|
+
pub items: Vec<ParsedItem>,
|
|
163
|
+
pub tables: Vec<TableDecl>,
|
|
164
|
+
pub test_fns: Vec<String>,
|
|
165
|
+
pub is_test_file: bool,
|
|
166
|
+
pub line_count: i64,
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── attribute helpers ──
|
|
170
|
+
|
|
171
|
+
pub fn attr_info(a: &syn::Attribute) -> AttrInfo {
|
|
172
|
+
let path = a
|
|
173
|
+
.path()
|
|
174
|
+
.segments
|
|
175
|
+
.iter()
|
|
176
|
+
.map(|s| s.ident.to_string())
|
|
177
|
+
.collect::<Vec<_>>()
|
|
178
|
+
.join("::");
|
|
179
|
+
let args = match &a.meta {
|
|
180
|
+
syn::Meta::List(l) => l.tokens.to_string(),
|
|
181
|
+
syn::Meta::NameValue(nv) => nv.value.to_token_stream().to_string(),
|
|
182
|
+
syn::Meta::Path(_) => String::new(),
|
|
183
|
+
};
|
|
184
|
+
AttrInfo { path, args }
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/// last path segment of an attr path (e.g. "get" for actix_web::get)
|
|
188
|
+
pub fn attr_last_segment(a: &syn::Attribute) -> String {
|
|
189
|
+
a.path()
|
|
190
|
+
.segments
|
|
191
|
+
.last()
|
|
192
|
+
.map(|s| s.ident.to_string())
|
|
193
|
+
.unwrap_or_default()
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
fn has_cfg_test(attrs: &[syn::Attribute]) -> bool {
|
|
197
|
+
attrs.iter().any(|a| {
|
|
198
|
+
let p = attr_info(a).path;
|
|
199
|
+
p == "cfg" && a.meta.to_token_stream().to_string().contains("test")
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ── call collection (per fn body; nested items are NOT descended into) ──
|
|
204
|
+
|
|
205
|
+
struct CallCollector {
|
|
206
|
+
calls: Vec<String>,
|
|
207
|
+
orm_calls: Vec<OrmCall>,
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/// Diesel query-DSL verbs (documented diesel API — anti-overfit rule).
|
|
211
|
+
const ORM_READ_VERBS: [&str; 9] = [
|
|
212
|
+
"filter",
|
|
213
|
+
"first",
|
|
214
|
+
"get_result",
|
|
215
|
+
"get_results",
|
|
216
|
+
"load",
|
|
217
|
+
"find",
|
|
218
|
+
"select",
|
|
219
|
+
"count",
|
|
220
|
+
"all",
|
|
221
|
+
];
|
|
222
|
+
const ORM_WRITE_VERBS: [&str; 3] = ["insert", "update", "delete"];
|
|
223
|
+
const DIESEL_WRITE_FNS: [&str; 3] = ["insert_into", "update", "delete"];
|
|
224
|
+
|
|
225
|
+
/// Collapse token-stream rendering ("users :: table" → "users::table").
|
|
226
|
+
/// to_token_stream() emits spaces around punctuation; removing ALL whitespace
|
|
227
|
+
/// yields the canonical compact form every detector matches on.
|
|
228
|
+
fn collapse_tokens(s: &str) -> String {
|
|
229
|
+
s.split_whitespace().collect::<Vec<_>>().join("")
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
fn path_string(p: &syn::Path) -> String {
|
|
233
|
+
p.segments
|
|
234
|
+
.iter()
|
|
235
|
+
.map(|s| s.ident.to_string())
|
|
236
|
+
.collect::<Vec<_>>()
|
|
237
|
+
.join("::")
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/// Normalize a table reference to its bare table name:
|
|
241
|
+
/// users::table / users::dsl::users / users::dsl::users::columns::id /
|
|
242
|
+
/// schema::users::table → users
|
|
243
|
+
pub fn table_name_from_ref(r: &str) -> String {
|
|
244
|
+
let parts: Vec<&str> = r.split("::").collect();
|
|
245
|
+
for (i, part) in parts.iter().enumerate() {
|
|
246
|
+
if *part == "table" && i > 0 {
|
|
247
|
+
return parts[i - 1].to_string();
|
|
248
|
+
}
|
|
249
|
+
if *part == "dsl" && i + 1 < parts.len() {
|
|
250
|
+
return parts[i + 1].to_string();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// bare identifier: first segment (e.g. `users` imported via dsl::*)
|
|
254
|
+
parts[0].to_string()
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
impl<'ast> Visit<'ast> for CallCollector {
|
|
258
|
+
// scope rule: nested item definitions (fns/impls inside a body) do NOT
|
|
259
|
+
// belong to the enclosing function — skip them entirely
|
|
260
|
+
fn visit_item(&mut self, _item: &'ast syn::Item) {}
|
|
261
|
+
|
|
262
|
+
fn visit_expr_call(&mut self, e: &'ast syn::ExprCall) {
|
|
263
|
+
let name = match &*e.func {
|
|
264
|
+
syn::Expr::Path(p) => {
|
|
265
|
+
if p.qself.is_none() {
|
|
266
|
+
path_string(&p.path)
|
|
267
|
+
} else {
|
|
268
|
+
collapse_tokens(&p.to_token_stream().to_string())
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
other => collapse_tokens(&other.to_token_stream().to_string()),
|
|
272
|
+
};
|
|
273
|
+
self.calls.push(name.clone());
|
|
274
|
+
// diesel top-level write fns: insert_into(table) / update(table) /
|
|
275
|
+
// delete(table)
|
|
276
|
+
if let Some(first) = DIESEL_WRITE_FNS
|
|
277
|
+
.iter()
|
|
278
|
+
.find(|f| name.ends_with(&format!("::{}", f)))
|
|
279
|
+
{
|
|
280
|
+
let _ = first;
|
|
281
|
+
if let Some(arg) = e.args.first() {
|
|
282
|
+
self.orm_calls.push(OrmCall {
|
|
283
|
+
kind: "write".to_string(),
|
|
284
|
+
table_ref: collapse_tokens(&arg.to_token_stream().to_string()),
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
syn::visit::visit_expr_call(self, e);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
fn visit_expr_method_call(&mut self, e: &'ast syn::ExprMethodCall) {
|
|
292
|
+
let recv = collapse_tokens(&e.receiver.to_token_stream().to_string());
|
|
293
|
+
let method = e.method.to_string();
|
|
294
|
+
self.calls.push(format!("{}.{}", recv, method));
|
|
295
|
+
// Diesel query DSL on a table path (receiver contains ::table or
|
|
296
|
+
// ::dsl::, or is a bare table-name alias)
|
|
297
|
+
let is_table_path = recv.contains("::table")
|
|
298
|
+
|| recv.contains("::dsl::")
|
|
299
|
+
|| (!recv.contains("::") && !recv.contains('.'));
|
|
300
|
+
if is_table_path {
|
|
301
|
+
if ORM_READ_VERBS.contains(&method.as_str()) {
|
|
302
|
+
self.orm_calls.push(OrmCall {
|
|
303
|
+
kind: "read".to_string(),
|
|
304
|
+
table_ref: table_name_from_ref(&recv),
|
|
305
|
+
});
|
|
306
|
+
} else if ORM_WRITE_VERBS.contains(&method.as_str()) {
|
|
307
|
+
self.orm_calls.push(OrmCall {
|
|
308
|
+
kind: "write".to_string(),
|
|
309
|
+
table_ref: table_name_from_ref(&recv),
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
syn::visit::visit_expr_method_call(self, e);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ── file parsing ──
|
|
318
|
+
|
|
319
|
+
pub fn parse_file(rel_path: &str, source: &str) -> Result<ParsedFile, String> {
|
|
320
|
+
let ast = syn::parse_file(source).map_err(|e| e.to_string())?;
|
|
321
|
+
let line_index = LineIndex::new(source);
|
|
322
|
+
let mut pf = ParsedFile {
|
|
323
|
+
rel_path: rel_path.to_string(),
|
|
324
|
+
ast,
|
|
325
|
+
uses: vec![],
|
|
326
|
+
items: vec![],
|
|
327
|
+
tables: vec![],
|
|
328
|
+
test_fns: vec![],
|
|
329
|
+
is_test_file: false,
|
|
330
|
+
line_count: line_index.line_count() as i64,
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
// module path from the file's location (walker::module_path_for_file)
|
|
334
|
+
let module_path = crate::walker::module_path_for_file(rel_path);
|
|
335
|
+
|
|
336
|
+
// collect facts from the AST items (take avoids the immutable-borrow-vs-
|
|
337
|
+
// mutable-pf conflict; put them BACK — routes.rs walks pf.ast for
|
|
338
|
+
// expression-level detection)
|
|
339
|
+
let ast_items = std::mem::take(&mut pf.ast.items);
|
|
340
|
+
for item in &ast_items {
|
|
341
|
+
collect_item(item, &module_path, source, &line_index, &mut pf);
|
|
342
|
+
}
|
|
343
|
+
pf.ast.items = ast_items;
|
|
344
|
+
|
|
345
|
+
pf.is_test_file = !pf.test_fns.is_empty();
|
|
346
|
+
Ok(pf)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
fn slice_range(source: &str, start: usize, end: usize) -> String {
|
|
350
|
+
if start >= end || start >= source.len() {
|
|
351
|
+
return String::new();
|
|
352
|
+
}
|
|
353
|
+
source[start..end.min(source.len())].to_string()
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
fn item_span_code<T: Spanned>(
|
|
357
|
+
item: &T,
|
|
358
|
+
source: &str,
|
|
359
|
+
line_index: &LineIndex,
|
|
360
|
+
) -> (i64, i64, String) {
|
|
361
|
+
let span = item.span();
|
|
362
|
+
let (s, e) = line_index.span_range(span);
|
|
363
|
+
let start_line = span.start().line as i64;
|
|
364
|
+
let end_line = span.end().line as i64;
|
|
365
|
+
(start_line, end_line, slice_range(source, s, e))
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
fn collect_item(
|
|
369
|
+
item: &syn::Item,
|
|
370
|
+
module_path: &str,
|
|
371
|
+
source: &str,
|
|
372
|
+
line_index: &LineIndex,
|
|
373
|
+
pf: &mut ParsedFile,
|
|
374
|
+
) {
|
|
375
|
+
match item {
|
|
376
|
+
syn::Item::Use(u) => {
|
|
377
|
+
collect_use_tree(&u.tree, &mut pf.uses);
|
|
378
|
+
}
|
|
379
|
+
syn::Item::Mod(m) => {
|
|
380
|
+
let cfg_test = has_cfg_test(&m.attrs);
|
|
381
|
+
match &m.content {
|
|
382
|
+
None => {
|
|
383
|
+
// external `mod x;` — the file exists on disk; the module
|
|
384
|
+
// map is filesystem-derived, nothing to record here
|
|
385
|
+
}
|
|
386
|
+
Some((_, items)) => {
|
|
387
|
+
if cfg_test {
|
|
388
|
+
// #[cfg(test)] mod — items stay OUT of the graph, but
|
|
389
|
+
// #[test] fns still feed the file's testCases (leaf
|
|
390
|
+
// rule: a file with tests IS a TEST node)
|
|
391
|
+
collect_test_names(items, pf);
|
|
392
|
+
} else {
|
|
393
|
+
let child_module = if module_path.is_empty() {
|
|
394
|
+
m.ident.to_string()
|
|
395
|
+
} else {
|
|
396
|
+
format!("{}::{}", module_path, m.ident)
|
|
397
|
+
};
|
|
398
|
+
for it in items {
|
|
399
|
+
collect_item(it, &child_module, source, line_index, pf);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
syn::Item::Macro(mac) => {
|
|
406
|
+
// Diesel table! — the extractor's one macro we understand
|
|
407
|
+
if mac
|
|
408
|
+
.mac
|
|
409
|
+
.path
|
|
410
|
+
.segments
|
|
411
|
+
.last()
|
|
412
|
+
.map(|s| s.ident == "table")
|
|
413
|
+
.unwrap_or(false)
|
|
414
|
+
{
|
|
415
|
+
if let Some(t) = parse_table_macro(&mac.mac.tokens) {
|
|
416
|
+
pf.tables.push(t);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
syn::Item::Fn(f) => collect_fn(
|
|
421
|
+
&f.attrs,
|
|
422
|
+
&f.sig,
|
|
423
|
+
&f.block,
|
|
424
|
+
&f.vis,
|
|
425
|
+
module_path,
|
|
426
|
+
source,
|
|
427
|
+
line_index,
|
|
428
|
+
pf,
|
|
429
|
+
),
|
|
430
|
+
syn::Item::Struct(s) => {
|
|
431
|
+
let (sl, el, raw) = item_span_code(s, source, line_index);
|
|
432
|
+
let mut pi = base_item(
|
|
433
|
+
KIND_STRUCT,
|
|
434
|
+
&s.ident.to_string(),
|
|
435
|
+
sl,
|
|
436
|
+
el,
|
|
437
|
+
raw,
|
|
438
|
+
&s.attrs,
|
|
439
|
+
s.vis.clone(),
|
|
440
|
+
);
|
|
441
|
+
pi.derives = collect_derives(&s.attrs);
|
|
442
|
+
if let syn::Fields::Named(named) = &s.fields {
|
|
443
|
+
for f in &named.named {
|
|
444
|
+
if let Some(name) = &f.ident {
|
|
445
|
+
pi.fields.push(FieldInfo {
|
|
446
|
+
name: name.to_string(),
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
// struct generics bounds → metadata (EXTENDS for local traits is
|
|
452
|
+
// resolved in inheritance.rs from the bound paths)
|
|
453
|
+
collect_generic_bounds(&s.generics, &mut pi);
|
|
454
|
+
pf.items.push(pi);
|
|
455
|
+
}
|
|
456
|
+
syn::Item::Enum(e) => {
|
|
457
|
+
let (sl, el, raw) = item_span_code(e, source, line_index);
|
|
458
|
+
let mut pi = base_item(
|
|
459
|
+
KIND_ENUM,
|
|
460
|
+
&e.ident.to_string(),
|
|
461
|
+
sl,
|
|
462
|
+
el,
|
|
463
|
+
raw,
|
|
464
|
+
&e.attrs,
|
|
465
|
+
e.vis.clone(),
|
|
466
|
+
);
|
|
467
|
+
pi.derives = collect_derives(&e.attrs);
|
|
468
|
+
for v in &e.variants {
|
|
469
|
+
pi.variants.push(v.ident.to_string());
|
|
470
|
+
}
|
|
471
|
+
collect_generic_bounds(&e.generics, &mut pi);
|
|
472
|
+
pf.items.push(pi);
|
|
473
|
+
}
|
|
474
|
+
syn::Item::Trait(t) => {
|
|
475
|
+
let (sl, el, raw) = item_span_code(t, source, line_index);
|
|
476
|
+
let mut pi = base_item(
|
|
477
|
+
KIND_TRAIT,
|
|
478
|
+
&t.ident.to_string(),
|
|
479
|
+
sl,
|
|
480
|
+
el,
|
|
481
|
+
raw,
|
|
482
|
+
&t.attrs,
|
|
483
|
+
t.vis.clone(),
|
|
484
|
+
);
|
|
485
|
+
pi.supertraits = t
|
|
486
|
+
.supertraits
|
|
487
|
+
.iter()
|
|
488
|
+
.filter_map(|b| match b {
|
|
489
|
+
syn::TypeParamBound::Trait(tb) => Some(path_string(&tb.path)),
|
|
490
|
+
_ => None,
|
|
491
|
+
})
|
|
492
|
+
.collect();
|
|
493
|
+
pf.items.push(pi);
|
|
494
|
+
}
|
|
495
|
+
syn::Item::Impl(im) => {
|
|
496
|
+
let (sl, el, raw) = item_span_code(im, source, line_index);
|
|
497
|
+
let trait_opt = im.trait_.as_ref().map(|(_, path, _)| path_string(path));
|
|
498
|
+
let target = collapse_tokens(&im.self_ty.to_token_stream().to_string());
|
|
499
|
+
let name = match &trait_opt {
|
|
500
|
+
Some(t) => format!("impl {} for {}", t, target),
|
|
501
|
+
None => format!("impl {}", target),
|
|
502
|
+
};
|
|
503
|
+
let mut pi = base_item(
|
|
504
|
+
KIND_IMPL_BLOCK,
|
|
505
|
+
&name,
|
|
506
|
+
sl,
|
|
507
|
+
el,
|
|
508
|
+
raw,
|
|
509
|
+
&im.attrs,
|
|
510
|
+
// impl blocks carry no visibility in syn (always inherited)
|
|
511
|
+
syn::Visibility::Inherited,
|
|
512
|
+
);
|
|
513
|
+
pi.impl_trait = trait_opt.clone();
|
|
514
|
+
pi.impl_trait_path = trait_opt.clone();
|
|
515
|
+
pi.impl_target = target;
|
|
516
|
+
for ii in &im.items {
|
|
517
|
+
if let syn::ImplItem::Fn(ifn) = ii {
|
|
518
|
+
let mut m = parse_fn_like(
|
|
519
|
+
&ifn.attrs,
|
|
520
|
+
&ifn.sig,
|
|
521
|
+
&ifn.block,
|
|
522
|
+
&ifn.vis,
|
|
523
|
+
module_path,
|
|
524
|
+
source,
|
|
525
|
+
line_index,
|
|
526
|
+
true,
|
|
527
|
+
);
|
|
528
|
+
// the method belongs to the impl's target type (and trait
|
|
529
|
+
// when `impl Trait for Type`) — id + lookup keys need it
|
|
530
|
+
m.impl_target = pi.impl_target.clone();
|
|
531
|
+
m.impl_trait_path = pi.impl_trait_path.clone();
|
|
532
|
+
pf.items.push(m);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
pf.items.push(pi);
|
|
536
|
+
}
|
|
537
|
+
_ => {}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
fn base_item(
|
|
542
|
+
kind: &'static str,
|
|
543
|
+
name: &str,
|
|
544
|
+
start_line: i64,
|
|
545
|
+
end_line: i64,
|
|
546
|
+
raw_code: String,
|
|
547
|
+
attrs: &[syn::Attribute],
|
|
548
|
+
vis: syn::Visibility,
|
|
549
|
+
) -> ParsedItem {
|
|
550
|
+
ParsedItem {
|
|
551
|
+
kind,
|
|
552
|
+
name: name.to_string(),
|
|
553
|
+
start_line,
|
|
554
|
+
end_line,
|
|
555
|
+
raw_code,
|
|
556
|
+
attrs: attrs.iter().map(attr_info).collect(),
|
|
557
|
+
is_pub: matches!(vis, syn::Visibility::Public(_)),
|
|
558
|
+
is_async: false,
|
|
559
|
+
receiver: String::new(),
|
|
560
|
+
calls: vec![],
|
|
561
|
+
is_test: false,
|
|
562
|
+
orm_calls: vec![],
|
|
563
|
+
fields: vec![],
|
|
564
|
+
variants: vec![],
|
|
565
|
+
supertraits: vec![],
|
|
566
|
+
derives: vec![],
|
|
567
|
+
impl_trait: None,
|
|
568
|
+
impl_target: String::new(),
|
|
569
|
+
impl_trait_path: None,
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
#[allow(clippy::too_many_arguments)]
|
|
574
|
+
fn collect_fn(
|
|
575
|
+
attrs: &[syn::Attribute],
|
|
576
|
+
sig: &syn::Signature,
|
|
577
|
+
block: &syn::Block,
|
|
578
|
+
vis: &syn::Visibility,
|
|
579
|
+
module_path: &str,
|
|
580
|
+
source: &str,
|
|
581
|
+
line_index: &LineIndex,
|
|
582
|
+
pf: &mut ParsedFile,
|
|
583
|
+
) {
|
|
584
|
+
let pi = parse_fn_like(
|
|
585
|
+
attrs,
|
|
586
|
+
sig,
|
|
587
|
+
block,
|
|
588
|
+
vis,
|
|
589
|
+
module_path,
|
|
590
|
+
source,
|
|
591
|
+
line_index,
|
|
592
|
+
false,
|
|
593
|
+
);
|
|
594
|
+
if pi.is_test {
|
|
595
|
+
pf.test_fns.push(pi.name.clone());
|
|
596
|
+
}
|
|
597
|
+
pf.items.push(pi);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/// Scan items (recursively through mods) for #[test]-attributed fns — used
|
|
601
|
+
/// for #[cfg(test)] mods whose items are otherwise excluded from the graph.
|
|
602
|
+
fn collect_test_names(items: &[syn::Item], pf: &mut ParsedFile) {
|
|
603
|
+
for item in items {
|
|
604
|
+
match item {
|
|
605
|
+
syn::Item::Fn(f) => {
|
|
606
|
+
if f.attrs.iter().any(|a| attr_last_segment(a) == "test") {
|
|
607
|
+
pf.test_fns.push(f.sig.ident.to_string());
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
syn::Item::Mod(m) => {
|
|
611
|
+
if let Some((_, inner)) = &m.content {
|
|
612
|
+
collect_test_names(inner, pf);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
_ => {}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
#[allow(clippy::too_many_arguments)]
|
|
621
|
+
fn parse_fn_like(
|
|
622
|
+
attrs: &[syn::Attribute],
|
|
623
|
+
sig: &syn::Signature,
|
|
624
|
+
block: &syn::Block,
|
|
625
|
+
vis: &syn::Visibility,
|
|
626
|
+
_module_path: &str,
|
|
627
|
+
source: &str,
|
|
628
|
+
line_index: &LineIndex,
|
|
629
|
+
is_method: bool,
|
|
630
|
+
) -> ParsedItem {
|
|
631
|
+
let sig_span = sig.span();
|
|
632
|
+
let block_span = block.span();
|
|
633
|
+
let start_line = sig_span.start().line as i64;
|
|
634
|
+
let end_line = block_span.end().line as i64;
|
|
635
|
+
let (s, _) = line_index.span_range(sig_span);
|
|
636
|
+
let (_, e) = line_index.span_range(block_span);
|
|
637
|
+
let raw = slice_range(source, s, e);
|
|
638
|
+
|
|
639
|
+
let mut pi = base_item(
|
|
640
|
+
if is_method {
|
|
641
|
+
KIND_METHOD
|
|
642
|
+
} else {
|
|
643
|
+
KIND_FUNCTION
|
|
644
|
+
},
|
|
645
|
+
&sig.ident.to_string(),
|
|
646
|
+
start_line,
|
|
647
|
+
end_line,
|
|
648
|
+
raw,
|
|
649
|
+
attrs,
|
|
650
|
+
vis.clone(),
|
|
651
|
+
);
|
|
652
|
+
pi.is_async = sig.asyncness.is_some();
|
|
653
|
+
|
|
654
|
+
// receiver (methods only): &self / &mut self / self
|
|
655
|
+
if is_method {
|
|
656
|
+
pi.receiver = match sig.receiver() {
|
|
657
|
+
Some(r) => {
|
|
658
|
+
let m = if r.mutability.is_some() { "mut " } else { "" };
|
|
659
|
+
let a = if r.reference.is_some() { "&" } else { "" };
|
|
660
|
+
format!("{}{}self", a, m)
|
|
661
|
+
}
|
|
662
|
+
None => String::new(),
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// #[test] / #[tokio::test] / #[actix_web::test] — last segment "test"
|
|
667
|
+
for a in attrs {
|
|
668
|
+
if attr_last_segment(a) == "test" {
|
|
669
|
+
pi.is_test = true;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// calls + Diesel R/W facts within THIS body only (nested items skipped)
|
|
674
|
+
let mut cc = CallCollector {
|
|
675
|
+
calls: vec![],
|
|
676
|
+
orm_calls: vec![],
|
|
677
|
+
};
|
|
678
|
+
cc.visit_block(block);
|
|
679
|
+
let mut seen: BTreeSet<String> = BTreeSet::new();
|
|
680
|
+
for c in cc.calls {
|
|
681
|
+
seen.insert(c);
|
|
682
|
+
}
|
|
683
|
+
pi.calls = seen.into_iter().collect();
|
|
684
|
+
pi.orm_calls = cc.orm_calls;
|
|
685
|
+
pi
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
fn collect_use_tree(tree: &syn::UseTree, out: &mut Vec<UseDecl>) {
|
|
689
|
+
let mut acc: Vec<UseDecl> = vec![];
|
|
690
|
+
collect_use_tree_inner(tree, "", &mut acc);
|
|
691
|
+
out.extend(acc);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/// Recursively collect use targets, threading the path prefix.
|
|
695
|
+
/// `use crate::models::{user::User, helpers::*};` →
|
|
696
|
+
/// User → crate::models::user::User
|
|
697
|
+
/// (glob) helpers → crate::models::helpers
|
|
698
|
+
fn collect_use_tree_inner(tree: &syn::UseTree, prefix: &str, out: &mut Vec<UseDecl>) {
|
|
699
|
+
match tree {
|
|
700
|
+
syn::UseTree::Path(p) => {
|
|
701
|
+
let seg = p.ident.to_string();
|
|
702
|
+
let next = if prefix.is_empty() {
|
|
703
|
+
seg.clone()
|
|
704
|
+
} else {
|
|
705
|
+
format!("{}::{}", prefix, seg)
|
|
706
|
+
};
|
|
707
|
+
collect_use_tree_inner(&p.tree, &next, out);
|
|
708
|
+
}
|
|
709
|
+
syn::UseTree::Name(n) => {
|
|
710
|
+
let target = if prefix.is_empty() {
|
|
711
|
+
n.ident.to_string()
|
|
712
|
+
} else {
|
|
713
|
+
format!("{}::{}", prefix, n.ident)
|
|
714
|
+
};
|
|
715
|
+
out.push(UseDecl {
|
|
716
|
+
alias: n.ident.to_string(),
|
|
717
|
+
target,
|
|
718
|
+
glob: false,
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
syn::UseTree::Rename(r) => {
|
|
722
|
+
let target = if prefix.is_empty() {
|
|
723
|
+
r.ident.to_string()
|
|
724
|
+
} else {
|
|
725
|
+
format!("{}::{}", prefix, r.ident)
|
|
726
|
+
};
|
|
727
|
+
out.push(UseDecl {
|
|
728
|
+
alias: r.rename.to_string(),
|
|
729
|
+
target,
|
|
730
|
+
glob: false,
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
syn::UseTree::Glob(_) => {
|
|
734
|
+
out.push(UseDecl {
|
|
735
|
+
alias: String::new(),
|
|
736
|
+
target: prefix.to_string(),
|
|
737
|
+
glob: true,
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
syn::UseTree::Group(g) => {
|
|
741
|
+
for item in &g.items {
|
|
742
|
+
collect_use_tree_inner(item, prefix, out);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
fn collect_derives(attrs: &[syn::Attribute]) -> Vec<String> {
|
|
749
|
+
let mut out = vec![];
|
|
750
|
+
for a in attrs {
|
|
751
|
+
if attr_last_segment(a) != "derive" {
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
if let syn::Meta::List(l) = &a.meta {
|
|
755
|
+
let paths = l.parse_args_with(
|
|
756
|
+
syn::punctuated::Punctuated::<syn::Path, syn::Token![,]>::parse_terminated,
|
|
757
|
+
);
|
|
758
|
+
if let Ok(paths) = paths {
|
|
759
|
+
for p in paths {
|
|
760
|
+
out.push(path_string(&p));
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
out
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
fn collect_generic_bounds(generics: &syn::Generics, pi: &mut ParsedItem) {
|
|
769
|
+
for param in &generics.params {
|
|
770
|
+
if let syn::GenericParam::Type(tp) = param {
|
|
771
|
+
for bound in &tp.bounds {
|
|
772
|
+
if let syn::TypeParamBound::Trait(tb) = bound {
|
|
773
|
+
pi.supertraits.push(path_string(&tb.path));
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// ── Diesel table! macro ──
|
|
781
|
+
//
|
|
782
|
+
fn parse_table_macro(tokens: &TokenStream) -> Option<TableDecl> {
|
|
783
|
+
let mut iter = tokens.clone().into_iter();
|
|
784
|
+
// first token: table name ident
|
|
785
|
+
let name = match iter.next()? {
|
|
786
|
+
TokenTree::Ident(i) => i.to_string(),
|
|
787
|
+
_ => return None,
|
|
788
|
+
};
|
|
789
|
+
// optional `(pk)` group, then the `{ columns }` group
|
|
790
|
+
let columns_group: proc_macro2::Group = match iter.next()? {
|
|
791
|
+
TokenTree::Group(g) if g.delimiter() == proc_macro2::Delimiter::Brace => g,
|
|
792
|
+
TokenTree::Group(g) if g.delimiter() == proc_macro2::Delimiter::Parenthesis => {
|
|
793
|
+
match iter.next()? {
|
|
794
|
+
TokenTree::Group(g2) if g2.delimiter() == proc_macro2::Delimiter::Brace => g2,
|
|
795
|
+
_ => return None,
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
_ => return None,
|
|
799
|
+
};
|
|
800
|
+
Some(TableDecl {
|
|
801
|
+
name,
|
|
802
|
+
columns: parse_table_columns(&columns_group.stream()),
|
|
803
|
+
})
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
fn parse_table_columns(tokens: &TokenStream) -> Vec<(String, String)> {
|
|
807
|
+
let mut out = vec![];
|
|
808
|
+
let tokens: Vec<TokenTree> = tokens.clone().into_iter().collect();
|
|
809
|
+
let mut idx = 0;
|
|
810
|
+
while idx < tokens.len() {
|
|
811
|
+
// column name ident
|
|
812
|
+
let name = match &tokens[idx] {
|
|
813
|
+
TokenTree::Ident(i) => i.to_string(),
|
|
814
|
+
_ => {
|
|
815
|
+
idx += 1;
|
|
816
|
+
continue;
|
|
817
|
+
}
|
|
818
|
+
};
|
|
819
|
+
idx += 1;
|
|
820
|
+
// skip `->` (TWO puncts: '-' then '>') and ',' separators
|
|
821
|
+
while idx < tokens.len() {
|
|
822
|
+
match &tokens[idx] {
|
|
823
|
+
TokenTree::Punct(p) if p.as_char() == '-' || p.as_char() == '>' => {
|
|
824
|
+
idx += 1;
|
|
825
|
+
}
|
|
826
|
+
TokenTree::Punct(p) if p.as_char() == ',' => {
|
|
827
|
+
idx += 1;
|
|
828
|
+
break;
|
|
829
|
+
}
|
|
830
|
+
_ => break,
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
// type: idents (possibly with ::)
|
|
834
|
+
let mut ty_parts: Vec<String> = vec![];
|
|
835
|
+
while idx < tokens.len() {
|
|
836
|
+
match &tokens[idx] {
|
|
837
|
+
TokenTree::Ident(i) => {
|
|
838
|
+
ty_parts.push(i.to_string());
|
|
839
|
+
idx += 1;
|
|
840
|
+
}
|
|
841
|
+
TokenTree::Punct(p) if p.as_char() == ':' => {
|
|
842
|
+
idx += 1; // skip second ':'
|
|
843
|
+
if idx < tokens.len() {
|
|
844
|
+
idx += 1;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
TokenTree::Punct(p) if p.as_char() == ',' => {
|
|
848
|
+
idx += 1;
|
|
849
|
+
break;
|
|
850
|
+
}
|
|
851
|
+
_ => break,
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
if !ty_parts.is_empty() {
|
|
855
|
+
out.push((name, ty_parts.join("::")));
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
out
|
|
859
|
+
}
|