Skip to main content

big_code_analysis_ast/lang_helpers/
python.rs

1//! Python: the aliased `block` and `lambda` kind sets, and the shape
2//! of a docstring.
3
4use crate::Python;
5use crate::lang_helpers::doc_comment::node_bytes;
6use crate::node::{AncestorIter, Ancestors, Cursor, FoldLane, Node};
7
8/// Whether `node` is a Python `lambda` expression, under either of the
9/// grammar's two aliased kind_ids: `Lambda` (196, the concrete
10/// production emitted today) and `Lambda2` (197, the currently-unseen
11/// hidden alias). `Lambda3` (73) is the `lambda` *keyword* token, not a
12/// closure node, and is intentionally excluded.
13///
14/// This is the single normalization chokepoint for the lambda-alias set
15/// — mirroring [`python_is_block`] for the block aliases (#419). It
16/// is reused by `PythonCode::is_closure` here and by the cognitive
17/// lambda-scope walks in `big-code-analysis`, so a future grammar bump
18/// that promotes `Lambda2` to a concrete node is handled in exactly one
19/// place rather than drifting across sites (#422). The
20/// `python_hidden_block_and_lambda_aliases_stay_unseen` drift guard in
21/// `checker.rs` trips on such a bump.
22#[inline]
23#[must_use]
24pub fn python_is_lambda(node: &Node) -> bool {
25    matches!(node.kind_id().into(), Python::Lambda | Python::Lambda2)
26}
27
28/// Single normalization point for Python's aliased `block` kind_ids.
29///
30/// tree-sitter-python lists two `kind_id`s that both stringify to
31/// `"block"`: `Block` (135, the hidden `_block` supertype) and
32/// `Block2` (160, the concrete production). Empirically only `Block2`
33/// is ever emitted for real block bodies (function, class, if/for,
34/// while/try/with), so `Block` is dead today — but a future grammar
35/// bump could promote the supertype to a concrete node. Routing every
36/// "is this a block?" check through here means such a bump is handled
37/// at one site instead of silently undercounting at several (issue
38/// #419; lesson 2 / 34 / 56 in docs/development/lessons_learned.md).
39#[inline]
40#[must_use]
41pub fn python_is_block(node: &Node) -> bool {
42    matches!(node.kind_id().into(), Python::Block | Python::Block2)
43}
44
45/// How many parentheses CPython lets one expression nest in: its
46/// tokenizer's `MAXLEVEL` (`Parser/lexer/state.h`). A docstring wrapped
47/// deeper than this is a syntax error, so the climb and the descent
48/// through `parenthesized_expression` stop here, which also keeps each
49/// one constant-time on a file nesting thousands of them. A `u8`, so the
50/// memoised climb ([`bare_string_climb`]) saturates against it.
51const MAX_PARENTHESES: u8 = 200;
52
53/// Whether `node`, past any parentheses around it, is the sole child of
54/// an `expression_statement`: the discarded-value shape a docstring
55/// takes. CPython drops the parentheses, so `("""doc""")` is a docstring
56/// (#1610). `"a", "b"` has two children, so it is a discarded tuple, not
57/// documentation.
58#[inline]
59#[must_use]
60pub fn python_is_bare_statement_value(node: &Node, ancestors: Ancestors<'_, '_>) -> bool {
61    enclosing_statement(node, ancestors)
62        .is_some_and(|((statement, _), _)| is_bare_statement(&statement))
63}
64
65/// Whether `node` is a `parenthesized_expression` around a bare string
66/// statement's string, or one of its `(` and `)`. The parentheses belong
67/// to the docstring they wrap (#1610): neither code rows nor operators.
68///
69/// Decided from the statement down, as the string's own climb is, so a
70/// string nested past CPython's 200-parenthesis cap keeps every one of
71/// its parentheses as code rather than only the outermost. Every
72/// parenthesis of a chain asks, and a climb and a descent per parenthesis
73/// made a statement `d` deep cost `O(d²)`: a walk that lent a `FoldCache`
74/// reads the answer off a memoised fold of the ancestry instead, which
75/// unwraps each statement's value once. Any other caller climbs, which
76/// the cap bounds where a fold without its memo would run from the root.
77#[must_use]
78pub fn python_is_bare_string_parenthesis(node: &Node, ancestors: Ancestors<'_, '_>) -> bool {
79    let token = matches!(node.kind_id().into(), Python::LPAREN | Python::RPAREN);
80    let Some((paren, paren_ancestors)) = (if token {
81        ancestors.iter(node).next()
82    } else {
83        Some((*node, ancestors))
84    }) else {
85        return false;
86    };
87    if paren.kind_id() != Python::ParenthesizedExpression {
88        return false;
89    }
90    if ancestors.memoises_folds() {
91        // A token's fold stops at its parenthesis, whose state it reads.
92        let climb = ancestors.fold_down(
93            node,
94            usize::from(token),
95            FoldLane::BareStringClimb,
96            0,
97            |climb, ancestor, _, _| bare_string_climb(climb, ancestor),
98        );
99        return (1..=MAX_PARENTHESES + 1).contains(&climb);
100    }
101    enclosing_statement(&paren, paren_ancestors)
102        .is_some_and(|((statement, _), _)| statement_string(&statement).is_some())
103}
104
105/// One step of the fold [`python_is_bare_string_parenthesis`] memoises:
106/// the climb from a node through the parentheses above it to the
107/// statement holding them, or 0 when that is no bare string statement. A
108/// parenthesis lengthens the climb, saturating one past the longest climb
109/// [`enclosing_statement`] takes; any other ancestor starts one. A climb
110/// past the cap ends at a statement whose string lies past it too, so the
111/// length decides nothing there, and no recovery the grammar performs
112/// gives a parenthesis holding the string a deeper parenthesis beside it
113/// (searched, #1610): the saturation point is kept for the fold to answer
114/// as the climb does should one appear, and nothing can pin it.
115fn bare_string_climb(climb: u8, ancestor: &Node) -> u8 {
116    if ancestor.kind_id() != Python::ParenthesizedExpression {
117        u8::from(statement_string(ancestor).is_some())
118    } else if climb == 0 {
119        0
120    } else {
121        climb.saturating_add(1).min(MAX_PARENTHESES + 2)
122    }
123}
124
125/// The nearest ancestor of `node` that is not a parenthesis around it,
126/// paired with its ancestry, and the iterator continuing above it.
127fn enclosing_statement<'a, 'c>(
128    node: &Node<'a>,
129    ancestors: Ancestors<'a, 'c>,
130) -> Option<((Node<'a>, Ancestors<'a, 'c>), AncestorIter<'a, 'c>)> {
131    let mut up = ancestors.iter(node);
132    let statement = up
133        .by_ref()
134        .take(usize::from(MAX_PARENTHESES) + 1)
135        .find(|(ancestor, _)| ancestor.kind_id() != Python::ParenthesizedExpression)?;
136    Some((statement, up))
137}
138
139/// `node` with at most [`MAX_PARENTHESES`] parentheses around it removed:
140/// the expression inside, skipping the comments a parenthesized
141/// expression may hold. One cursor serves every layer.
142fn unparenthesized(node: Node) -> Node {
143    let mut cursor = node.cursor();
144    let mut value = node;
145    for _ in 0..MAX_PARENTHESES {
146        let Some(inner) = unparenthesized_once(&value, &mut cursor) else {
147            break;
148        };
149        value = inner;
150    }
151    value
152}
153
154/// The expression one `parenthesized_expression` holds, or `None` when
155/// `node` is not one, read with `cursor`.
156fn unparenthesized_once<'a>(node: &Node<'a>, cursor: &mut Cursor<'a>) -> Option<Node<'a>> {
157    if node.kind_id() != Python::ParenthesizedExpression {
158        return None;
159    }
160    node.children_with(cursor)
161        .find(|child| child.is_named() && child.kind_id() != Python::Comment)
162}
163
164/// The string a bare string statement holds, past any parentheses.
165fn statement_string<'a>(statement: &Node<'a>) -> Option<Node<'a>> {
166    if !is_bare_statement(statement) {
167        return None;
168    }
169    statement.child(0).map(unparenthesized).filter(|value| {
170        matches!(
171            value.kind_id().into(),
172            Python::String | Python::ConcatenatedString
173        )
174    })
175}
176
177/// Whether `node` is a bare string statement's `string`: the statement's
178/// sole value itself, or one fragment of an implicitly concatenated
179/// literal (`"""doc""" "more"`) that is — CPython joins the fragments
180/// into one string, and so does the `concatenated_string` node.
181///
182/// The one definition of "a string whose value is discarded" for both
183/// walks that care: the Halstead getter drops its operands (#695) and
184/// the LOC walk bills its rows as comment rows (#415). Whether those rows
185/// are also *documentation* is narrower and positional — see
186/// [`python_documentation_string_run`] (#1597).
187#[inline]
188#[must_use]
189pub fn python_is_bare_string(node: &Node, ancestors: Ancestors<'_, '_>) -> bool {
190    let Some((parent, parent_ancestors)) = ancestors.iter(node).next() else {
191        return false;
192    };
193    if matches!(parent.kind_id().into(), Python::ConcatenatedString) {
194        return python_is_bare_statement_value(&parent, parent_ancestors);
195    }
196    python_is_bare_statement_value(node, ancestors)
197}
198
199/// The first statement of the documentation-string run that the bare
200/// string statement holding `value` belongs to, or `None` when that
201/// statement is not documentation. `value` is a `string` or
202/// `concatenated_string`, per [`python_is_bare_statement_value`], and the
203/// statement documents where Python tooling reads documentation (#1597):
204///
205/// - **a docstring** (PEP 257): the first statement of a module, class or
206///   function body — the string Python stores as `__doc__`, and the one
207///   `help()`, `inspect` and pydoc read. Only a `str` literal is stored:
208///   an f-string, a template string or a bytes literal is no docstring;
209/// - **an additional docstring** (PEP 257): a string statement directly
210///   after another docstring, including an attribute or additional docstring;
211/// - **an attribute docstring** (PEP 257 and 258, read by Sphinx autodoc):
212///   a string statement directly after an assignment to a name at module
213///   or class level, or to an attribute of `self` at the top level of a
214///   class's `__init__`.
215///
216/// "Directly after" skips comments, which are not statements. Any other
217/// bare string — a triple-quoted string wrapping commented-out code in
218/// the middle of a function, a module docstring written after the
219/// imports — is still a comment row (#415) but is not documentation, so
220/// the maintainability index charges it as it charges a `#` comment.
221/// `code` is read for a literal's prefix and for `__init__`'s names.
222///
223/// One backward scan answers both: the string statements before it,
224/// skipping comments, are the run, and the first other statement ends
225/// the run and is the one an attribute docstring is documented by.
226fn documentation_run_start<'a>(
227    value: &Node<'a>,
228    code: &[u8],
229    ancestors: Ancestors<'a, '_>,
230) -> Option<Node<'a>> {
231    // The literal test is the cheap one; `body_attributes` may read the
232    // enclosing `__init__`'s name and parameters.
233    if !is_str_literal(value, code) {
234        return None;
235    }
236    let ((statement, statement_ancestors), up) = enclosing_statement(value, ancestors)?;
237    let attributes = body_attributes(up, code)?;
238    let mut first = statement;
239    for sibling in statement_ancestors
240        .siblings_before(&statement)
241        .filter(|sibling| sibling.is_named() && sibling.kind_id() != Python::Comment)
242    {
243        if !is_string_statement(&sibling, code) {
244            return attributes.documented_by(&sibling, code).then_some(first);
245        }
246        first = sibling;
247    }
248    Some(first)
249}
250
251/// Credits a consecutive run of documentation strings at its last string.
252/// Comments between statements do not break the run. Deciding once keeps
253/// additional-docstring recognition linear in the number of statements.
254pub fn python_documentation_string_run(
255    value: &Node,
256    code: &[u8],
257    ancestors: Ancestors<'_, '_>,
258    mut credit: impl FnMut(usize, usize),
259) {
260    let Some(((statement, outer), _)) = enclosing_statement(value, ancestors) else {
261        return;
262    };
263    if outer
264        .siblings_after(&statement)
265        .find(|sibling| sibling.is_named() && sibling.kind_id() != Python::Comment)
266        .is_some_and(|next| is_string_statement(&next, code))
267    {
268        return;
269    }
270    let Some(first) = documentation_run_start(value, code, ancestors) else {
271        return;
272    };
273    for member in std::iter::once(first)
274        .chain(outer.siblings_after(&first))
275        .take_while(|member| member.start_byte() <= statement.start_byte())
276        .filter(|member| is_string_statement(member, code))
277    {
278        credit_documentation_rows(member, &mut credit);
279    }
280}
281
282/// Credits the rows of a string statement's value and of the `(` and `)`
283/// around it, which are part of the docstring (#1610); a comment inside
284/// the parentheses stays a comment.
285fn credit_documentation_rows(statement: Node, credit: &mut impl FnMut(usize, usize)) {
286    let Some(mut value) = statement.child(0) else {
287        return;
288    };
289    let mut cursor = value.cursor();
290    for _ in 0..MAX_PARENTHESES {
291        let Some(inner) = unparenthesized_once(&value, &mut cursor) else {
292            break;
293        };
294        for paren in value
295            .children_with(&mut cursor)
296            .filter(|child| !child.is_named())
297        {
298            credit(paren.start_row(), paren.end_row());
299        }
300        value = inner;
301    }
302    credit(value.start_row(), value.end_row());
303}
304
305/// The assignments a body reads an attribute docstring after: PEP 258's
306/// rules for the top level of a module, a class or `__init__`, with the
307/// targets Sphinx's `VariableCommentPicker` accepts there. Sphinx also
308/// reads the suites of an `if`, `with`, loop or `try` at those levels;
309/// PEP 257 does not, and neither does this.
310enum Attributes<'code> {
311    /// A module or class body: an assignment to a name, or to a tuple of
312    /// names — not to a subscript or another object's attribute, which
313    /// declares nothing there.
314    Names,
315    /// The top level of a class's `__init__`: an assignment to an
316    /// attribute of its first parameter, whose name this holds (`self`),
317    /// alone or in a tuple of targets.
318    OfSelf(&'code [u8]),
319    /// Any other function body.
320    Nothing,
321}
322
323impl Attributes<'_> {
324    /// Whether `statement`, directly before a string statement, makes that
325    /// string an attribute docstring.
326    fn documented_by(&self, statement: &Node, code: &[u8]) -> bool {
327        // A `type X = …` alias (PEP 695) declares the name it binds, and
328        // Sphinx's `VariableCommentPicker` reads it like an assignment
329        // (`AssignmentLikeType` holds `ast.TypeAlias`): a docstring after
330        // it or a `#:` comment on or above it documents `X`. It never
331        // binds an attribute of `self`.
332        if statement.kind_id() == Python::TypeAliasStatement {
333            return matches!(self, Self::Names);
334        }
335        let Some(target) = assignment_target(statement) else {
336            return false;
337        };
338        match self {
339            Self::Names => binds(&target, |leaf| leaf.kind_id() == Python::Identifier),
340            Self::OfSelf(name) => binds(&target, |leaf| {
341                leaf.kind_id() == Python::Attribute
342                    && leaf.child_by_field_name("object").is_some_and(|object| {
343                        object.kind_id() == Python::Identifier && node_bytes(&object, code) == *name
344                    })
345            }),
346            Self::Nothing => false,
347        }
348    }
349}
350
351/// What a body makes of the assignments in it, from the ancestors above
352/// the statement: `None` when the body holds no documentation at all (an
353/// `if` or loop body).
354fn body_attributes<'code>(
355    mut up: AncestorIter<'_, '_>,
356    code: &'code [u8],
357) -> Option<Attributes<'code>> {
358    let (container, _) = up.next()?;
359    if container.kind_id() == Python::Module {
360        return Some(Attributes::Names);
361    }
362    let (owner, _) = up.next().filter(|_| python_is_block(&container))?;
363    owner_attributes(&owner, up, code)
364}
365
366/// What the body of `owner` — a class or function definition — makes of
367/// the assignments in it; `None` for any other owner. `up` continues
368/// from `owner`'s parent.
369fn owner_attributes<'code>(
370    owner: &Node,
371    up: AncestorIter<'_, '_>,
372    code: &'code [u8],
373) -> Option<Attributes<'code>> {
374    if owner.kind_id() == Python::ClassDefinition {
375        Some(Attributes::Names)
376    } else if owner.kind_id() == Python::FunctionDefinition {
377        let receiver = is_class_init(owner, up, code)
378            .then(|| first_parameter_name(owner, code))
379            .flatten();
380        Some(receiver.map_or(Attributes::Nothing, Attributes::OfSelf))
381    } else {
382        None
383    }
384}
385
386/// Whether `function` is a class's `__init__`, the one method PEP 257
387/// reads attribute docstrings in: named so, with the class the first
388/// ancestor past its decorators and the class body's block. `up`
389/// continues from `function`'s parent.
390fn is_class_init(function: &Node, up: AncestorIter<'_, '_>, code: &[u8]) -> bool {
391    function
392        .child_by_field_name("name")
393        .is_some_and(|name| node_bytes(&name, code) == b"__init__")
394        && up
395            .map(|(node, _)| node)
396            .find(|node| node.kind_id() != Python::DecoratedDefinition && !python_is_block(node))
397            .is_some_and(|class| class.kind_id() == Python::ClassDefinition)
398}
399
400/// The name of `function`'s first parameter, typed or not. A comment is a
401/// named extra, and formatters put one right after the `(` of an exploded
402/// signature (`def __init__(  # noqa: PLR0913`), so it is skipped.
403fn first_parameter_name<'code>(function: &Node, code: &'code [u8]) -> Option<&'code [u8]> {
404    let first = function
405        .child_by_field_name("parameters")?
406        .children()
407        .find(|child| child.is_named() && child.kind_id() != Python::Comment)?;
408    let name = if first.kind_id() == Python::TypedParameter {
409        first.children().find(Node::is_named)?
410    } else {
411        first
412    };
413    (name.kind_id() == Python::Identifier).then(|| node_bytes(&name, code))
414}
415
416/// Whether `value` is a literal Python stores as a docstring: a `str`, not
417/// an f-string, a template string or bytes — a prefix holding `f`, `t` or
418/// `b` in either case makes the statement none, and one fragment of an
419/// implicit concatenation is enough.
420fn is_str_literal(value: &Node, code: &[u8]) -> bool {
421    let is_str = |literal: &Node| {
422        !node_bytes(literal, code)
423            .iter()
424            .take_while(|byte| byte.is_ascii_alphabetic())
425            .any(|byte| matches!(byte.to_ascii_lowercase(), b'f' | b't' | b'b'))
426    };
427    if value.kind_id() == Python::ConcatenatedString {
428        value
429            .children()
430            .filter(|fragment| fragment.kind_id() == Python::String)
431            .all(|fragment| is_str(&fragment))
432    } else {
433        is_str(value)
434    }
435}
436
437/// An `expression_statement` holding one value and nothing else: the
438/// discarded-value shape a docstring takes.
439fn is_bare_statement(statement: &Node) -> bool {
440    statement.kind_id() == Python::ExpressionStatement && statement.child_count() == 1
441}
442
443/// A bare statement whose value is a `str` literal: a docstring, when it
444/// opens its body.
445fn is_string_statement(statement: &Node, code: &[u8]) -> bool {
446    statement_string(statement).is_some_and(|value| is_str_literal(&value, code))
447}
448
449/// Whether an assignment target binds what Sphinx records an attribute
450/// docstring for, as its `get_lvar_names` reads a target: `leaf` decides a
451/// single one — a name, a subscript or an attribute — and a tuple, list or
452/// starred target binds when any element does. Walked with an explicit
453/// stack, so a target nested thousands of brackets deep cannot exhaust the
454/// thread's stack (#1597).
455fn binds(target: &Node, leaf: impl Fn(&Node) -> bool) -> bool {
456    let mut pending = vec![*target];
457    while let Some(node) = pending.pop() {
458        match node.kind_id().into() {
459            Python::Identifier | Python::Subscript | Python::Attribute => {
460                if leaf(&node) {
461                    return true;
462                }
463            }
464            _ => pending.extend(node.children().filter(Node::is_named)),
465        }
466    }
467    false
468}
469
470/// The target of the assignment `statement` holds, annotated or not.
471fn assignment_target<'a>(statement: &Node<'a>) -> Option<Node<'a>> {
472    (statement.kind_id() == Python::ExpressionStatement)
473        .then(|| statement.child(0))
474        .flatten()
475        .filter(|value| value.kind_id() == Python::Assignment)
476        .and_then(|assignment| assignment.child_by_field_name("left"))
477}
478
479/// Credits the rows of a Sphinx doc comment that `comment` completes
480/// (#1597). Sphinx autodoc reads a comment starting `#:` as an
481/// attribute's documentation when it sits on the line of an assignment,
482/// after the code, or on the consecutive lines directly above one —
483/// every one of them `#:`; a plain `#` line breaks the block. The
484/// assignment must be one an attribute docstring may follow
485/// (module or class level, or `self.<attr>` at the top of `__init__`).
486///
487/// A block is decided once, at its last `#:` line, which walks back
488/// over the block to credit it: `credit(first_row, last_row)` per line.
489/// The grammar places a comment that precedes a body's first statement
490/// outside that body, as a child of the class or function, so the
491/// statement after it is the body block's first statement.
492pub fn python_sphinx_doc_comment<'a>(
493    comment: &Node<'a>,
494    code: &[u8],
495    ancestors: Ancestors<'a, '_>,
496    mut credit: impl FnMut(usize, usize),
497) {
498    if !is_sphinx_doc_comment(comment, code) {
499        return;
500    }
501    let documents = match ancestors.siblings_before(comment).next() {
502        // The same-line form: `x = 1  #: doc`.
503        Some(statement) if statement.end_row() >= comment.start_row() => {
504            body_attributes(ancestors.iter(comment), code)
505                .is_some_and(|attributes| attributes.documented_by(&statement, code))
506        }
507        _ => ends_block_above_assignment(comment, code, ancestors),
508    };
509    if documents {
510        credit_block(comment, code, ancestors, &mut credit);
511    }
512}
513
514/// Whether `comment` is the last line of a `#:` block that sits directly
515/// above an assignment an attribute docstring may follow. A block whose
516/// next line is another `#:` is decided there instead.
517fn ends_block_above_assignment(comment: &Node, code: &[u8], ancestors: Ancestors<'_, '_>) -> bool {
518    let Some(next) = ancestors.siblings_after(comment).next() else {
519        return false;
520    };
521    let below = comment.end_row().saturating_add(1);
522    if next.start_row() != below || is_sphinx_doc_comment(&next, code) {
523        return false;
524    }
525    if !python_is_block(&next) {
526        return body_attributes(ancestors.iter(comment), code)
527            .is_some_and(|attributes| attributes.documented_by(&next, code));
528    }
529    // The comment precedes a body's first statement, so the grammar made
530    // it the class's or function's child, ahead of the body block.
531    let mut up = ancestors.iter(comment);
532    let Some((owner, _)) = up.next() else {
533        return false;
534    };
535    next.children()
536        .find(Node::is_named)
537        .filter(|first| first.start_row() == below)
538        .is_some_and(|first| {
539            owner_attributes(&owner, up, code)
540                .is_some_and(|attributes| attributes.documented_by(&first, code))
541        })
542}
543
544/// Credits `comment` and the `#:` lines directly above it, each the
545/// first token of its row: one backward pass over one sibling iterator.
546fn credit_block(
547    comment: &Node,
548    code: &[u8],
549    ancestors: Ancestors<'_, '_>,
550    credit: &mut impl FnMut(usize, usize),
551) {
552    credit(comment.start_row(), comment.end_row());
553    let mut member = *comment;
554    let mut before = ancestors.siblings_before(comment).peekable();
555    while let Some(above) = before.next() {
556        let leads_its_row = before
557            .peek()
558            .is_none_or(|prev| prev.end_row() < above.start_row());
559        if !(is_sphinx_doc_comment(&above, code)
560            && above.end_row().saturating_add(1) == member.start_row()
561            && leads_its_row)
562        {
563            break;
564        }
565        credit(above.start_row(), above.end_row());
566        member = above;
567    }
568}
569
570/// A comment whose text opens with Sphinx's doc-comment marker, `#:`.
571fn is_sphinx_doc_comment(node: &Node, code: &[u8]) -> bool {
572    node.kind_id() == Python::Comment && node_bytes(node, code).starts_with(b"#:")
573}
574
575#[cfg(test)]
576mod tests {
577    use super::*;
578
579    /// Every `parenthesized_expression`, `(` and `)` of `source`, each
580    /// asked through a memoised walk and through its chain alone, which
581    /// takes the climb. A chainless caller climbs the same way, by
582    /// `Node::parent`, at a cost that would make this test quadratic.
583    #[cfg(feature = "python")]
584    fn bare_string_parenthesis_answers(source: &str) -> Vec<[bool; 2]> {
585        use crate::langs::PythonCode;
586        use crate::node::{FoldCache, Tree};
587        let tree = Tree::new::<PythonCode>(source.as_bytes());
588        let cache = FoldCache::default();
589        let mut answers = Vec::new();
590        // Pre-order with the chain the metric walk keeps, so the fold's
591        // per-depth slots are reused as they are there.
592        let mut stack = vec![(tree.get_root(), 0_usize)];
593        let mut chain: Vec<Node> = Vec::new();
594        while let Some((node, depth)) = stack.pop() {
595            chain.truncate(depth);
596            if matches!(
597                node.kind_id().into(),
598                Python::ParenthesizedExpression | Python::LPAREN | Python::RPAREN
599            ) {
600                answers.push([
601                    python_is_bare_string_parenthesis(
602                        &node,
603                        Ancestors::known(&chain).with_folds(&cache),
604                    ),
605                    python_is_bare_string_parenthesis(&node, Ancestors::known(&chain)),
606                ]);
607            }
608            chain.push(node);
609            let children: Vec<_> = node.children().collect();
610            stack.extend(children.into_iter().rev().map(|child| (child, depth + 1)));
611        }
612        answers
613    }
614
615    /// The memoised fold answers as the climb it replaced does (#1610),
616    /// on both sides of CPython's cap: a bare string under 200
617    /// parentheses keeps all of them as its own, one under 201 none — a
618    /// discarded f-string is a bare string too, if no docstring — and a
619    /// parenthesized value, a tuple and an assignment none.
620    /// The climb's answers are the oracle, so a fold that lost its reset
621    /// (`climb == 0`) fails here before any metric. The saturation point
622    /// and the window's upper edge are not observable: on every tree the
623    /// grammar produces, a parenthesis chain either ends at its string
624    /// within the cap or climbs past it, and both sides agree on each. A
625    /// perturbation of either edge fails nothing (measured), so the
626    /// arithmetic in [`bare_string_climb`] is argued in its doc, not
627    /// pinned here.
628    #[cfg(feature = "python")]
629    #[test]
630    fn memoised_climb_answers_as_the_climb_does() {
631        let nest = |depth: usize, inner: &str| {
632            format!("{}{inner}{}", "(".repeat(depth), ")".repeat(depth))
633        };
634        let cases = [
635            (nest(199, "\"s\""), 199 * 3),
636            (nest(200, "\"s\""), 200 * 3),
637            (nest(201, "\"s\""), 0),
638            (
639                format!("def f():\n    {}\n", nest(3, "\"\"\"d\"\"\"")),
640                3 * 3,
641            ),
642            ("(  # c\n    \"s\"\n)\n".to_owned(), 3),
643            ("(\"a\" \"b\")\n".to_owned(), 3),
644            (nest(3, "x"), 0),
645            ("(\"a\", \"b\")\n".to_owned(), 0),
646            (nest(3, "f\"x\""), 3 * 3),
647            (format!("x = {}\n", nest(1_000, "\"s\"")), 0),
648            (format!("{}\n(\"s\"\n", nest(2, "\"s\"")), 2 * 3),
649        ];
650        for (source, documenting) in cases {
651            let answers = bare_string_parenthesis_answers(&source);
652            assert!(!answers.is_empty(), "{source:.40}");
653            for [memoised, climbed] in &answers {
654                assert_eq!(memoised, climbed, "{source:.40}");
655            }
656            let found = answers.iter().filter(|[memoised, ..]| *memoised).count();
657            assert_eq!(found, documenting, "{source:.40}");
658        }
659    }
660}