1use crate::facts::{
10 Declaration, DeclarationKind, Facts, Import, ImportBinding, Reference, ReferenceKind, Span,
11};
12use crate::syntax::Language;
13use crate::token::{Mode, Token, TokenKind, Tokenizer};
14
15#[must_use]
17pub fn extract(source: &str) -> Facts {
18 let tokens = Tokenizer::new(source, Language::Python)
19 .mode(Mode::Lite)
20 .collect::<Vec<_>>();
21 let mut state = Extractor {
22 source,
23 tokens: &tokens,
24 facts: Facts::default(),
25 scopes: Vec::new(),
26 };
27 state.run();
28 state.facts
29}
30
31struct Scope {
33 name: String,
34 column: u32,
35}
36
37struct Extractor<'source, 'tokens> {
38 source: &'source str,
39 tokens: &'tokens [Token],
40 facts: Facts,
41 scopes: Vec<Scope>,
42}
43
44impl Extractor<'_, '_> {
45 fn run(&mut self) {
46 let mut index = 0;
47 while index < self.tokens.len() {
48 index = self.step(index);
49 }
50 }
51
52 fn text(&self, index: usize) -> &str {
53 self.tokens
54 .get(index)
55 .map_or("", |token| token.text(self.source))
56 }
57
58 fn kind(&self, index: usize) -> Option<TokenKind> {
59 self.tokens.get(index).map(|token| token.kind)
60 }
61
62 fn is(&self, index: usize, word: &str) -> bool {
63 self.kind(index) == Some(TokenKind::Identifier) && self.text(index) == word
64 }
65
66 fn punct(&self, index: usize, mark: &str) -> bool {
67 self.kind(index) == Some(TokenKind::Punctuation) && self.text(index) == mark
68 }
69
70 fn span(&self, start: usize, end: usize) -> Span {
71 let last_index = self.tokens.len().saturating_sub(1);
72 let first = &self.tokens[start.min(last_index)];
73 let last = &self.tokens[end.min(last_index)];
74 Span {
75 start: first.start,
76 end: last.end,
77 line: first.line,
78 column: first.column,
79 end_line: last.line,
80 end_column: last.column,
81 }
82 }
83
84 fn close_scopes(&mut self, column: u32) {
86 while self
87 .scopes
88 .last()
89 .is_some_and(|scope| column <= scope.column)
90 {
91 self.scopes.pop();
92 }
93 }
94
95 fn owner(&self) -> Option<String> {
96 self.scopes.last().map(|scope| scope.name.clone())
97 }
98
99 fn step(&mut self, index: usize) -> usize {
100 let column = self.tokens[index].column;
101 if self.kind(index) != Some(TokenKind::Identifier) {
102 return index + 1;
103 }
104 if self.is(index, "def") || self.is(index, "async") && self.is(index + 1, "def") {
105 let keyword = if self.is(index, "async") {
106 index + 1
107 } else {
108 index
109 };
110 return self.definition(index, keyword + 1, DeclarationKind::Function, column);
111 }
112 if self.is(index, "class") {
113 return self.definition(index, index + 1, DeclarationKind::Class, column);
114 }
115 if (self.is(index, "import") || self.is(index, "from"))
116 && let Some(next) = self.import(index)
117 {
118 return next;
119 }
120 if let Some(next) = self.call(index) {
121 return next;
122 }
123 index + 1
124 }
125
126 fn definition(
127 &mut self,
128 start: usize,
129 name_index: usize,
130 kind: DeclarationKind,
131 column: u32,
132 ) -> usize {
133 if self.kind(name_index) != Some(TokenKind::Identifier) {
134 return start + 1;
135 }
136 self.close_scopes(column);
137 let name = self.text(name_index).to_owned();
138 let kind = if kind == DeclarationKind::Function && self.owner().is_some() {
140 DeclarationKind::Method
141 } else {
142 kind
143 };
144 self.facts.declarations.push(Declaration {
145 name: name.clone(),
146 kind,
147 span: self.span(start, name_index),
148 owner: self.owner(),
149 exported: !name.starts_with('_'),
151 });
152 if kind == DeclarationKind::Class && self.punct(name_index + 1, "(") {
155 let limit = (name_index + 64).min(self.tokens.len());
156 let mut cursor = name_index + 2;
157 while cursor < limit && !self.punct(cursor, ")") {
158 if self.kind(cursor) == Some(TokenKind::Identifier)
159 && !self.punct(cursor + 1, "=")
160 && !self.punct(cursor.wrapping_sub(1), ".")
161 {
162 self.facts.references.push(Reference {
163 name: self.text(cursor).to_owned(),
164 kind: ReferenceKind::Inherits,
165 receiver: None,
166 span: self.span(cursor, cursor),
167 owner: Some(name.clone()),
168 string_arguments: Vec::new(),
169 name_arguments: Vec::new(),
170 });
171 }
172 cursor += 1;
173 }
174 }
175 self.scopes.push(Scope { name, column });
176 name_index + 1
177 }
178
179 fn import(&mut self, index: usize) -> Option<usize> {
181 let from_form = self.is(index, "from");
182 let line = self.tokens[index].line;
183 let mut cursor = index + 1;
184 if from_form {
185 let mut specifier = String::new();
186 while cursor < self.tokens.len()
187 && self.tokens[cursor].line == line
188 && !self.is(cursor, "import")
189 {
190 let text = self.text(cursor);
191 if text == "." || self.kind(cursor) == Some(TokenKind::Identifier) {
192 specifier.push_str(text);
193 }
194 cursor += 1;
195 }
196 if specifier.is_empty() || !self.is(cursor, "import") {
197 return None;
198 }
199 cursor += 1;
200 let (bindings, end) = self.python_bindings(cursor, line);
201 self.push_import(&specifier, index, end.saturating_sub(1), bindings);
202 return Some(end);
203 }
204
205 while cursor < self.tokens.len() && self.tokens[cursor].line == line {
206 let start = cursor;
207 let mut specifier = String::new();
208 while cursor < self.tokens.len()
209 && self.tokens[cursor].line == line
210 && !self.punct(cursor, ",")
211 && !self.is(cursor, "as")
212 {
213 let text = self.text(cursor);
214 if text == "." || self.kind(cursor) == Some(TokenKind::Identifier) {
215 specifier.push_str(text);
216 }
217 cursor += 1;
218 }
219 if specifier.is_empty() {
220 return None;
221 }
222 let mut local = specifier
223 .split('.')
224 .next()
225 .unwrap_or(specifier.as_str())
226 .to_owned();
227 if self.is(cursor, "as") && self.kind(cursor + 1) == Some(TokenKind::Identifier) {
228 self.text(cursor + 1).clone_into(&mut local);
229 cursor += 2;
230 }
231 self.push_import(
232 &specifier,
233 start,
234 cursor.saturating_sub(1),
235 vec![ImportBinding {
236 imported: specifier.clone(),
237 local,
238 }],
239 );
240 if self.punct(cursor, ",") {
241 cursor += 1;
242 }
243 }
244 Some(cursor)
245 }
246
247 fn python_bindings(&self, start: usize, line: u32) -> (Vec<ImportBinding>, usize) {
248 let mut bindings = Vec::new();
249 let mut cursor = start;
250 while cursor < self.tokens.len() && self.tokens[cursor].line == line {
251 if self.kind(cursor) != Some(TokenKind::Identifier) {
252 cursor += 1;
253 continue;
254 }
255 let imported = self.text(cursor).to_owned();
256 let mut local = imported.clone();
257 if self.is(cursor + 1, "as") && self.kind(cursor + 2) == Some(TokenKind::Identifier) {
258 self.text(cursor + 2).clone_into(&mut local);
259 cursor += 3;
260 } else {
261 cursor += 1;
262 }
263 bindings.push(ImportBinding { imported, local });
264 }
265 (bindings, cursor)
266 }
267
268 fn push_import(
269 &mut self,
270 specifier: &str,
271 start: usize,
272 end: usize,
273 bindings: Vec<ImportBinding>,
274 ) {
275 let names = bindings
276 .iter()
277 .map(|binding| binding.local.clone())
278 .collect();
279 self.facts.imports.push(Import {
280 specifier: specifier.to_owned(),
281 span: self.span(start, end),
282 type_only: false,
283 reexport: false,
284 names,
285 bindings,
286 });
287 }
288
289 fn call(&mut self, index: usize) -> Option<usize> {
290 if !self.punct(index + 1, "(") {
291 return None;
292 }
293 let name = self.text(index).to_owned();
294 if matches!(
295 name.as_str(),
296 "if" | "while" | "for" | "return" | "print" | "def" | "class" | "except" | "with"
297 ) {
298 return None;
299 }
300 let receiver = (index >= 2
301 && self.punct(index - 1, ".")
302 && self.kind(index - 2) == Some(TokenKind::Identifier))
303 .then(|| self.text(index - 2).to_owned());
304 let mut arguments = Vec::new();
305 let mut scan = index + 2;
306 let mut depth = 1_i32;
307 let limit = (index + 256).min(self.tokens.len());
308 while scan < limit && depth > 0 {
309 if self.punct(scan, "(") {
310 depth += 1;
311 } else if self.punct(scan, ")") {
312 depth -= 1;
313 } else if depth == 1 && self.kind(scan) == Some(TokenKind::String) {
314 let raw = self.text(scan);
315 let trimmed = raw
316 .trim_start_matches(['"', '\''])
317 .trim_end_matches(['"', '\'']);
318 arguments.push(trimmed.to_owned());
319 }
320 scan += 1;
321 }
322 self.facts.references.push(Reference {
323 kind: ReferenceKind::Call,
324 name,
325 receiver,
326 span: self.span(index, index),
327 owner: self.owner(),
328 string_arguments: arguments,
329 name_arguments: Vec::new(),
330 });
331 Some(index + 1)
332 }
333}
334
335#[cfg(test)]
336mod tests {
337 use super::extract;
338 use crate::facts::{DeclarationKind, ImportBinding};
339
340 #[test]
341 fn methods_belong_to_their_class_and_indentation_closes_scopes() {
342 let source = "class Service:\n\
343 \x20 def run(self):\n\
344 \x20 return self.helper()\n\
345 \x20 def helper(self):\n\
346 \x20 return 1\n\
347 \n\
348 def module_level():\n\
349 \x20 return Service()\n";
350 let facts = extract(source);
351 let declared = facts
352 .declarations
353 .iter()
354 .map(|item| (item.name.as_str(), item.kind, item.owner.as_deref()))
355 .collect::<Vec<_>>();
356 assert_eq!(
357 declared,
358 [
359 ("Service", DeclarationKind::Class, None),
360 ("run", DeclarationKind::Method, Some("Service")),
361 ("helper", DeclarationKind::Method, Some("Service")),
362 ("module_level", DeclarationKind::Function, None),
363 ],
364 "dedenting to column one leaves the class"
365 );
366 }
367
368 #[test]
369 fn a_docstring_is_text_even_when_it_looks_like_code() {
370 let source = "def real():\n\
371 \x20 \"\"\"\n\
372 \x20 def fake():\n\
373 \x20 import nothing\n\
374 \x20 \"\"\"\n\
375 \x20 return 1\n";
376 let facts = extract(source);
377 assert_eq!(
378 facts
379 .declarations
380 .iter()
381 .map(|item| item.name.as_str())
382 .collect::<Vec<_>>(),
383 ["real"],
384 "the definition inside the docstring is not a declaration"
385 );
386 assert!(
387 facts.imports.is_empty(),
388 "the import inside the docstring is not a dependency"
389 );
390 }
391
392 #[test]
393 fn reads_the_import_forms_python_writes() {
394 let source = "import os\n\
395 import pkg.module\n\
396 import json, time\n\
397 import numpy as np\n\
398 from .relative import thing as local_thing\n\
399 from ..parent.pkg import other\n";
400 let imports = extract(source).imports;
401 let specifiers = imports
402 .iter()
403 .map(|import| import.specifier.as_str())
404 .collect::<Vec<_>>();
405 assert_eq!(
406 specifiers,
407 [
408 "os",
409 "pkg.module",
410 "json",
411 "time",
412 "numpy",
413 ".relative",
414 "..parent.pkg",
415 ]
416 );
417 let numpy = imports
418 .iter()
419 .find(|import| import.specifier == "numpy")
420 .expect("numpy import");
421 assert_eq!(numpy.names, ["np"]);
422 assert_eq!(
423 numpy.bindings,
424 [ImportBinding {
425 imported: "numpy".to_owned(),
426 local: "np".to_owned(),
427 }]
428 );
429 let relative = imports
430 .iter()
431 .find(|import| import.specifier == ".relative")
432 .expect("relative import");
433 assert_eq!(relative.names, ["local_thing"]);
434 assert_eq!(
435 relative.bindings,
436 [ImportBinding {
437 imported: "thing".to_owned(),
438 local: "local_thing".to_owned(),
439 }]
440 );
441 }
442
443 #[test]
444 fn underscore_names_are_not_exported() {
445 let facts = extract("def public():\n pass\ndef _private():\n pass\n");
446 let exported = facts
447 .declarations
448 .iter()
449 .map(|item| (item.name.as_str(), item.exported))
450 .collect::<Vec<_>>();
451 assert_eq!(exported, [("public", true), ("_private", false)]);
452 }
453}