rustpython_ruff_python_ast/
identifier.rs1use crate::{self as ast, Alias, ExceptHandler, Parameter, ParameterWithDefault, Stmt};
14use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
15
16use ruff_python_trivia::{Cursor, is_python_whitespace};
17
18pub trait Identifier {
19 fn identifier(&self) -> TextRange;
21}
22
23impl Identifier for ast::StmtFunctionDef {
24 fn identifier(&self) -> TextRange {
32 self.name.range()
33 }
34}
35
36impl Identifier for ast::StmtClassDef {
37 fn identifier(&self) -> TextRange {
45 self.name.range()
46 }
47}
48
49impl Identifier for Stmt {
50 fn identifier(&self) -> TextRange {
58 match self {
59 Stmt::ClassDef(class) => class.identifier(),
60 Stmt::FunctionDef(function) => function.identifier(),
61 _ => self.range(),
62 }
63 }
64}
65
66impl Identifier for Parameter {
67 fn identifier(&self) -> TextRange {
75 self.name.range()
76 }
77}
78
79impl Identifier for ParameterWithDefault {
80 fn identifier(&self) -> TextRange {
88 self.parameter.identifier()
89 }
90}
91
92impl Identifier for Alias {
93 fn identifier(&self) -> TextRange {
100 self.asname
101 .as_ref()
102 .map_or_else(|| self.name.range(), Ranged::range)
103 }
104}
105
106pub fn except(handler: &ExceptHandler, source: &str) -> TextRange {
108 IdentifierTokenizer::new(source, handler.range())
109 .next()
110 .expect("Failed to find `except` token in `ExceptHandler`")
111}
112
113pub fn else_(stmt: &Stmt, source: &str) -> Option<TextRange> {
115 let (Stmt::For(ast::StmtFor { body, orelse, .. })
116 | Stmt::While(ast::StmtWhile { body, orelse, .. })) = stmt
117 else {
118 return None;
119 };
120
121 if orelse.is_empty() {
122 return None;
123 }
124
125 IdentifierTokenizer::starts_at(
126 body.last().expect("Expected body to be non-empty").end(),
127 source,
128 )
129 .next()
130}
131
132fn is_python_identifier_start(c: char) -> bool {
136 c.is_alphabetic() || c == '_'
137}
138
139fn is_python_identifier_continue(c: char) -> bool {
144 c.is_alphanumeric() || c == '_'
145}
146
147pub(crate) struct IdentifierTokenizer<'a> {
158 cursor: Cursor<'a>,
159 offset: TextSize,
160}
161
162impl<'a> IdentifierTokenizer<'a> {
163 pub(crate) fn new(source: &'a str, range: TextRange) -> Self {
164 Self {
165 cursor: Cursor::new(&source[range]),
166 offset: range.start(),
167 }
168 }
169
170 pub(crate) fn starts_at(offset: TextSize, source: &'a str) -> Self {
171 let range = TextRange::new(offset, source.text_len());
172 Self::new(source, range)
173 }
174
175 fn next_token(&mut self) -> Option<TextRange> {
176 while let Some(c) = {
177 self.offset += self.cursor.token_len();
178 self.cursor.start_token();
179 self.cursor.bump()
180 } {
181 match c {
182 c if is_python_identifier_start(c) => {
183 self.cursor.eat_while(is_python_identifier_continue);
184 return Some(TextRange::at(self.offset, self.cursor.token_len()));
185 }
186
187 c if is_python_whitespace(c) => {
188 self.cursor.eat_while(is_python_whitespace);
189 }
190
191 '#' => {
192 self.cursor.eat_while(|c| !matches!(c, '\n' | '\r'));
193 }
194
195 '\r' => {
196 self.cursor.eat_char('\n');
197 }
198
199 '\n' => {
200 }
202
203 '\\' => {
204 }
206
207 _ => {
208 }
210 }
211 }
212
213 None
214 }
215}
216
217impl Iterator for IdentifierTokenizer<'_> {
218 type Item = TextRange;
219
220 fn next(&mut self) -> Option<Self::Item> {
221 self.next_token()
222 }
223}
224
225#[cfg(test)]
226mod tests {
227 use super::IdentifierTokenizer;
228 use ruff_text_size::{TextLen, TextRange, TextSize};
229
230 #[test]
231 fn extract_global_names() {
232 let contents = r"global X,Y, Z".trim();
233
234 let mut names = IdentifierTokenizer::new(
235 contents,
236 TextRange::new(TextSize::new(0), contents.text_len()),
237 );
238
239 let range = names.next_token().unwrap();
240 assert_eq!(&contents[range], "global");
241 assert_eq!(range, TextRange::new(TextSize::from(0), TextSize::from(6)));
242
243 let range = names.next_token().unwrap();
244 assert_eq!(&contents[range], "X");
245 assert_eq!(range, TextRange::new(TextSize::from(7), TextSize::from(8)));
246
247 let range = names.next_token().unwrap();
248 assert_eq!(&contents[range], "Y");
249 assert_eq!(range, TextRange::new(TextSize::from(9), TextSize::from(10)));
250
251 let range = names.next_token().unwrap();
252 assert_eq!(&contents[range], "Z");
253 assert_eq!(
254 range,
255 TextRange::new(TextSize::from(12), TextSize::from(13))
256 );
257 }
258}