Repository: nix-community/nixpkgs-fmt (rust)

Issue:
sample: function argument comments
src/rules.rs `apply_arg` should probably also look if the node is a comment:

## Input
```nix
{
  foo =
    # describe bar
    bar:
    # describe baz
    baz:
      fnbody;
}
```

## Output
```nix
{
  foo =
  # describe bar
      bar:
      # describe baz
          baz:
            fnbody;
}

```

## Desired output

See input


Pull request description:
implement new version of indentation DLS
I've looked how Kotlin handles this and I liked the approach:

https://github.com/JetBrains/kotlin/blob/7d173ed3856e429739b55d8c3892e1b85ca41571/idea/formatter/src/org/jetbrains/kotlin/idea/formatter/KotlinCommonBlock.kt#L682-L690

The main trick they use is negative rules: "indent all children except
X". They nicely handle comments and in general simplify things a bit.

I've also added names to rules to as a step towards future debug mode,
as well as an ability to specify simple tests in-line, which should
help with long-term rule maintainability.

closes #71, #65 

prerequisite of #74

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index e4bdda6..9e0e2aa 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -129,6 +129,7 @@ version = "0.1.0"
 dependencies = [
  "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "rnix 0.6.0 (git+https://gitlab.com/jD91mZM2/rnix.git)",
+ "unindent 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -426,6 +427,11 @@ name = "unicode-xid"
 version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
+[[package]]
+name = "unindent"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
 [[package]]
 name = "unreachable"
 version = "1.0.0"
@@ -577,6 +583,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 "checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
 "checksum unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "882386231c45df4700b275c7ff55b6f3698780a650026380e72dabe76fa46526"
 "checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc"
+"checksum unindent 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "834b4441326c660336850c5c0926cc20548e848967a5f57bc20c2b741c8d41f4"
 "checksum unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56"
 "checksum vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a"
 "checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
diff --git a/Cargo.toml b/Cargo.toml
index 62bc0c9..08d8fdf 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -16,3 +16,6 @@ members = [ "./wasm" ]
 [dependencies]
 clap = "2.33.0"
 rnix = { git = "https://gitlab.com/jD91mZM2/rnix.git" }
+
+[dev-dependencies]
+unindent = "0.1.3"
diff --git a/src/dsl.rs b/src/dsl.rs
index b1426b2..8f512ab 100644
--- a/src/dsl.rs
+++ b/src/dsl.rs
@@ -194,6 +194,28 @@ impl<'a> SpacingRuleBuilder<'a> {
     }
 }
 
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub(crate) enum Modality {
+    Positive,
+    Negative,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub(crate) enum IndentValue {
+    Indent,
+}
+
+#[derive(Debug, Clone, Copy)]
+pub(crate) struct RuleName(&'static str);
+
+impl RuleName {
+    fn new(name: &'static str) -> RuleName {
+        assert!(name.chars().next().unwrap().is_uppercase(), "rule names should be capitalized");
+        assert!(name.chars().last().unwrap() != '.', "rule names should not end in `.`");
+        RuleName(name)
+    }
+}
+
 /// `IndentRule` describes how an element should be indented.
 ///
 /// `IndentRule`s are only effective for elements which begin the line.
@@ -202,11 +224,16 @@ impl<'a> SpacingRuleBuilder<'a> {
 /// * the same, as parent (default)
 /// * indent relative to the parent.
 ///
-/// For this reason, `IndentRule` specifies only the pattern.
+/// For this reason, `indent_value` is mostly unused.
 #[derive(Debug)]
 pub(crate) struct IndentRule {
-    /// Pattern that should match the element which is indented
-    pub(crate) pattern: Pattern,
+    pub(crate) name: RuleName,
+    pub(crate) parent: Pattern,
+
+    /// Depending on `child_modality`, this pattern selects/discards elements
+    pub(crate) child: Option<Pattern>,
+    pub(crate) child_modality: Modality,
+
     /// Pattern that should match the anchoring element, relative to which we
     /// calculate the indent
     ///
@@ -224,13 +251,7 @@ pub(crate) struct IndentRule {
     /// applies and `f = x ...` is the thing to which the `anchor_pattern`
     /// applies.
     pub(crate) anchor_pattern: Option<Pattern>,
-}
-
-/// Make `IndentRule` usable with `PatternSet`
-impl AsRef<Pattern> for IndentRule {
-    fn as_ref(&self) -> &Pattern {
-        &self.pattern
-    }
+    pub(crate) indent_value: IndentValue,
 }
 
 /// A builder to conveniently specify a set of `IndentRule`s.
@@ -238,6 +259,8 @@ impl AsRef<Pattern> for IndentRule {
 pub(crate) struct IndentDsl {
     pub(crate) rules: Vec<IndentRule>,
     pub(crate) anchors: Vec<Pattern>,
+    #[cfg(test)]
+    pub(crate) tests: Vec<(&'static str, &'static str)>,
 }
 
 impl IndentDsl {
@@ -260,46 +283,87 @@ impl IndentDsl {
         self.anchors.push(pattern.into());
         self
     }
-    /// Specify a rule for an element which is a child of `parent`.
-    pub(crate) fn inside(&mut self, parent: impl Into<Pattern>) -> IndentRuleBuilder<'_> {
-        IndentRuleBuilder { dsl: self, parent: parent.into(), when: None, when_anchor: None }
+    /// Adds a new indent rule with the given name
+    pub(crate) fn rule<'a>(&'a mut self, rule_name: &'static str) -> IndentRuleBuilder<'a> {
+        IndentRuleBuilder::new(self, rule_name)
+    }
+    #[cfg(test)]
+    pub(crate) fn test(&mut self, before: &'static str, after: &'static str) -> &mut IndentDsl {
+        self.tests.push((before, after));
+        self
+    }
+    #[cfg(not(test))]
+    pub(crate) fn test(&mut self, _before: &'static str, _after: &'static str) -> &mut IndentDsl {
+        self
     }
 }
 
 /// A builder to conveniently specify a single `IndentRule`.
 pub(crate) struct IndentRuleBuilder<'a> {
     dsl: &'a mut IndentDsl,
-    parent: Pattern,
-    when: Option<Pattern>,
-    when_anchor: Option<Pattern>,
+    rule_name: &'static str,
+    parent: Option<Pattern>,
+    child: Option<Pattern>,
+    child_modality: Modality,
+    anchor_pattern: Option<Pattern>,
 }
 
 impl<'a> IndentRuleBuilder<'a> {
-    /// Indent the specified `child` element.
-    pub(crate) fn indent(self, child: impl Into<Pattern>) -> &'a mut IndentDsl {
-        let mut child: Pattern = child.into();
-        if let Some(cond) = self.when {
-            child = child & cond;
+    fn new(dsl: &'a mut IndentDsl, rule_name: &'static str) -> IndentRuleBuilder<'a> {
+        IndentRuleBuilder {
+            dsl,
+            rule_name,
+            parent: None,
+            child: None,
+            child_modality: Modality::Positive,
+            anchor_pattern: None,
         }
-        let indent_rule = IndentRule {
-            pattern: child.with_parent(self.parent),
-            anchor_pattern: self.when_anchor,
-        };
-        self.dsl.rules.push(indent_rule);
-        self.dsl
     }
-    /// Only apply this rule when `cond` is true.
-    pub(crate) fn when(mut self, cond: fn(SyntaxElement<'_>) -> bool) -> IndentRuleBuilder<'a> {
-        self.when = Some(cond.into());
+
+    /// Rule applies if element's parent matches.
+    pub(crate) fn inside(mut self, parent: impl Into<Pattern>) -> Self {
+        let prev = self.parent.replace(parent.into());
+        assert!(prev.is_none());
         self
     }
+
+    /// Rule applies if element itself matches.
+    pub(crate) fn matching(self, child: impl Into<Pattern>) -> Self {
+        self.matching_modality(child.into(), Modality::Positive)
+    }
+
+    /// Rule applies if element itself does *not* match.
+    pub(crate) fn not_matching(self, child: impl Into<Pattern>) -> Self {
+        self.matching_modality(child.into(), Modality::Negative)
+    }
+
+    fn matching_modality(mut self, child: Pattern, child_modality: Modality) -> Self {
+        let prev = self.child.replace(child);
+        assert!(prev.is_none());
+        self.child_modality = child_modality;
+        self
+    }
+
+    /// Which indent does the rule applies?
+    pub(crate) fn set(self, indent_value: IndentValue) -> &'a mut IndentDsl {
+        let dsl = self.dsl;
+        let name = self.rule_name;
+        let rule = IndentRule {
+            name: RuleName::new(name),
+            parent: self.parent.unwrap_or_else(|| panic!("incomplete rule: {}", name)),
+            child: self.child,
+            child_modality: self.child_modality,
+            anchor_pattern: self.anchor_pattern,
+            indent_value,
+        };
+        dsl.rules.push(rule);
+        dsl
+    }
+
     /// Only apply this rule when `cond` is true for the anchor node, relative
     /// to which we compute indentation level.
-    pub(crate) fn when_anchor(
-        mut self,
-        cond: fn(SyntaxElement<'_>) -> bool,
-    ) -> IndentRuleBuilder<'a> {
-        self.when_anchor = Some(cond.into());
+    pub(crate) fn when_anchor(mut self, cond: fn(SyntaxElement<'_>) -> bool) -> Self {
+        self.anchor_pattern = Some(cond.into());
         self
     }
 }
diff --git a/src/engine.rs b/src/engine.rs
index 5cf6d10..52c8331 100644
--- a/src/engine.rs
+++ b/src/engine.rs
@@ -8,7 +8,7 @@ mod fixes;
 use rnix::{SmolStr, SyntaxNode, TextRange};
 
 use crate::{
-    dsl::{IndentDsl, SpacingDsl},
+    dsl::{SpacingDsl, IndentDsl},
     engine::fmt_model::{BlockPosition, FmtModel, SpaceBlock, SpaceBlockOrToken},
     pattern::PatternSet,
     tree_utils::walk_non_whitespace,
@@ -34,7 +34,6 @@ pub(crate) fn format(
     }
 
     // Next, for each node which starts the newline, adjust the indent.
-    let indent_rule_set = PatternSet::new(indent_dsl.rules.iter());
     let anchor_set = PatternSet::new(indent_dsl.anchors.iter());
     for element in walk_non_whitespace(root) {
         let block = model.block_for(element, BlockPosition::Before);
@@ -55,7 +54,7 @@ pub(crate) fn format(
             continue;
         }
 
-        let mut matching = indent_rule_set.matching(element);
+        let mut matching = indent_dsl.rules.iter().filter(|it| it.matches(element));
         if let Some(rule) = matching.next() {
             rule.apply(element, &mut model, &anchor_set);
             assert!(matching.next().is_none(), "more that one indent rule matched");
diff --git a/src/engine/indentation.rs b/src/engine/indentation.rs
index c3cbabd..6b063f1 100644
--- a/src/engine/indentation.rs
+++ b/src/engine/indentation.rs
@@ -3,7 +3,7 @@ use std::cmp::{Ord, Ordering, PartialOrd};
 use rnix::{nodes::NODE_ROOT, SyntaxElement, SyntaxNode, TextUnit};
 
 use crate::{
-    dsl::IndentRule,
+    dsl::{IndentRule, Modality},
     engine::{BlockPosition, FmtModel, SpaceBlock, SpaceBlockOrToken},
     pattern::{Pattern, PatternSet},
 };
@@ -89,13 +89,28 @@ impl IndentLevel {
 }
 
 impl IndentRule {
+    pub(super) fn matches(&self, element: SyntaxElement) -> bool {
+        let parent = match element.parent() {
+            None => return false,
+            Some(it) => it,
+        };
+        if !self.parent.matches(parent.into()) {
+            return false;
+        }
+        if let Some(child) = &self.child {
+            child.matches(element) == (self.child_modality == Modality::Positive)
+        } else {
+            true
+        }
+    }
+
     pub(super) fn apply<'a>(
         &self,
         element: SyntaxElement<'a>,
         model: &mut FmtModel<'a>,
         anchor_set: &PatternSet<&Pattern>,
     ) {
-        assert!(self.pattern.matches(element));
+        debug_assert!(self.matches(element));
         let anchor_indent = match indent_anchor(element, model, anchor_set) {
             Some((anchor, indent)) => {
                 if let Some(p) = &self.anchor_pattern {
diff --git a/src/lib.rs b/src/lib.rs
index 5214d86..d39b225 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -69,7 +69,7 @@ impl FmtDiff {
 
 pub fn reformat_node(node: &SyntaxNode) -> FmtDiff {
     let spacing = rules::spacing();
-    let indentation = rules::indentation();
+    let indentation = rules::indentation2();
     engine::format(&spacing, &indentation, node)
 }
 
diff --git a/src/pattern.rs b/src/pattern.rs
index 211b67f..11ab134 100644
--- a/src/pattern.rs
+++ b/src/pattern.rs
@@ -11,7 +11,7 @@ use rnix::{SyntaxElement, SyntaxKind};
 ///
 /// It is like `Box<dyn Fn(SyntaxElement)> -> bool`, but with additional
 /// convenience methods (for example, you can `&` two patterns). `Pattern` also
-/// knows the set of node types which *could* mach, which allows to implement
+/// knows the set of node types which *could* match, which allows to implement
 /// matching over a set of patterns efficiently.
 ///
 /// Currently, we liberally box predicates inside of `Pattern`s, as there's only
diff --git a/src/rules.rs b/src/rules.rs
index 9b6d1b7..b32fc48 100644
--- a/src/rules.rs
+++ b/src/rules.rs
@@ -1,12 +1,12 @@
 //! This module contains specific `super::dsl` rules for formatting nix language.
 use rnix::{
     parser::nodes::*,
-    types::{Apply, Operation, SetEntry, TypedNode, With},
+    types::{Operation, SetEntry, TypedNode, With},
     SyntaxElement, SyntaxKind, T,
 };
 
 use crate::{
-    dsl::{self, IndentDsl, SpacingDsl},
+    dsl::{self, IndentDsl, IndentValue::*, SpacingDsl},
     pattern::Pattern,
     tree_utils::{has_newline, prev_sibling},
 };
@@ -123,79 +123,288 @@ fn after_multiline_binop(node: SyntaxElement<'_>) -> bool {
     };
 }
 
+fn p(p: impl Into<Pattern>) -> Pattern {
+    p.into()
+}
+
 #[rustfmt::skip]
-pub(crate) fn indentation() -> IndentDsl {
+pub(crate) fn indentation2() -> IndentDsl {
     let mut dsl = IndentDsl::default();
     dsl
         .anchor(NODE_PAT_ENTRY)
         .anchor(Pattern::from(rhs_of_binop))
 
-        .inside(NODE_LIST).indent(VALUES)
-        .inside(ENTRY_OWNERS).indent([NODE_SET_ENTRY, NODE_INHERIT])
-
-        .inside(NODE_PATTERN).indent(NODE_PAT_ENTRY)
-        .inside(NODE_LAMBDA).when(lambda_body_not_on_top_level).indent(VALUES_NOT_LAMBDA)
-        .inside(NODE_APPLY).when(apply_arg).indent(VALUES)
+        .rule("Indent list content")
+            .inside(NODE_LIST)
+            .not_matching([T!["["], T!["]"]])
+            .set(Indent)
+            .test(r#"
+                [
+                92
+                ]
+            "#, r#"
+                [
+                  92
+                ]
+            "#)
+
+        .rule("Indent parenthesized expressions")
+            .inside(NODE_PAREN)
+            .not_matching([T!["("], T![")"]])
+            .set(Indent)
+            .test(r#"
+                (
+                92
+                )
+            "#, r#"
+                (
+                  92
+                )
+            "#)
+
+        .rule("Indent attribute set content")
+            .inside(NODE_SET)
+            .not_matching([T!["{"], T!["}"]])
+            .set(Indent)
+            .test(r#"
+                {
+                foo = bar;
+                }
+            "#, r#"
+                {
+                  foo = bar;
+                }
+            "#)
+
+        .rule("Indent let bindings")
+            .inside(NODE_LET_IN)
+            .not_matching([T![let], T![in]])
+            .set(Indent)
+            .test(r#"
+                let
+                x = 1;
+                inherit z;
+                  in
+                  x
+            "#, r#"
+                let
+                  x = 1;
+                  inherit z;
+                in
+                  x
+            "#)
+
+        .rule("Indent attribute value")
+            .inside(NODE_SET_ENTRY)
+            .not_matching(T![;])
+            .set(Indent)
+            .test(r#"
+                {
+                  foo =
+                  92;
+                }
+            "#, r#"
+                {
+                  foo =
+                    92;
+                }
+            "#)
 
-        .inside(NODE_SET_ENTRY).indent(VALUES)
-        .inside(NODE_SET_ENTRY).when_anchor(set_entry_with_single_line_value).indent(T![;])
-        .inside(NODE_OPERATION).when_anchor(set_entry_with_single_line_value).indent(BIN_OPS)
-        .inside(NODE_WITH)
-            .when(with_body)
+        .rule("Indent semicolon in attribute")
+            .inside(NODE_SET_ENTRY)
             .when_anchor(set_entry_with_single_line_value)
-            .indent(VALUES)
-        .inside(NODE_OR_DEFAULT).indent(VALUES)
-
-        .inside(NODE_INHERIT).indent([NODE_IDENT, NODE_INHERIT_FROM, T![;]])
-        .inside(NODE_IF_ELSE).indent(VALUES)
-        .inside(NODE_PAREN).indent(VALUES)
-
-        // FIXME: don't force indent if comment is on the first line
-        .inside(ENTRY_OWNERS).indent(TOKEN_COMMENT)
-        .inside([
-            NODE_LIST,
-            NODE_PATTERN,
-            NODE_INHERIT,
-        ]).indent(TOKEN_COMMENT)
-        ;
-    dsl
-}
+            .matching(T![;])
+            .set(Indent)
+            .test(r#"
+                {
+                  foo = 92
+                  ;
+
+                  bar = [
+                    1
+                  ]
+                  ++ [ 2 ]
+                    ;
+                }
+            "#, r#"
+                {
+                  foo = 92
+                    ;
+
+                  bar = [
+                    1
+                  ]
+                  ++ [ 2 ]
+                  ;
+                }
+            "#)
 
-fn lambda_body_not_on_top_level(body: SyntaxElement<'_>) -> bool {
-    fn find(body: SyntaxElement<'_>) -> Option<bool> {
-        let mut node = body.as_node()?;
-        loop {
-            node = node.parent()?;
-            if node.kind() == NODE_ROOT {
-                return None;
-            }
-            if node.kind() != NODE_LAMBDA {
-                return Some(true);
-            }
-        }
-    }
+        .rule("Indent concatenation to first element")
+            .inside(NODE_OPERATION)
+            .when_anchor(set_entry_with_single_line_value)
+            .matching(BIN_OPS)
+            .set(Indent)
+            .test(r#"
+                {
+                  foo = []
+                  ++ []
+                  ;
+                }
+            "#, r#"
+                {
+                  foo = []
+                    ++ []
+                    ;
+                }
+            "#)
+
+        .rule("Indent lambda parameters")
+            .inside(NODE_PATTERN)
+            .not_matching([T!["{"], T!["}"], T![,]])
+            .set(Indent)
+            .test(r#"
+                {
+                # comment
+                foo ? bar
+                , baz
+                }: foo
+            "#, r#"
+                {
+                  # comment
+                  foo ? bar
+                , baz
+                }: foo
+            "#)
+
+        .rule("Indent lambda body")
+            .inside(p(NODE_LAMBDA) & p(not_on_top_level))
+            .not_matching([NODE_LAMBDA, TOKEN_COMMENT])
+            .set(Indent)
+            .test(r#"
+                {}:
+                  {
+                foo =
+                  # describe bar
+                  bar:
+                  # describe baz
+                  baz:
+                  fnbody;
+                }
+            "#, r#"
+                {}:
+                {
+                  foo =
+                    # describe bar
+                    bar:
+                    # describe baz
+                    baz:
+                      fnbody;
+                }
+            "#)
+
+        .rule("Indent apply arg")
+            .inside(NODE_APPLY)
+            .set(Indent)
+            .test(r#"
+                foo
+                bar baz
+            "#, r#"
+                foo
+                  bar baz
+            "#)
+
+        .rule("Indent with body in attribute")
+            .inside(NODE_WITH)
+            .when_anchor(set_entry_with_single_line_value)
+            .set(Indent)
+            .test(r#"
+                with foo;
+                  {
+                  bar = with baz;
+                  body;
+                  }
+            "#, r#"
+                with foo;
+                {
+                  bar = with baz;
+                    body;
+                }
+            "#)
+
+        .rule("Indent or default")
+            .inside(NODE_OR_DEFAULT)
+            .set(Indent)
+            .test(r#"
+                {
+                  x = foo or
+                  bar;
+                }
+            "#, r#"
+                {
+                  x = foo or
+                    bar;
+                }
+            "#)
+
+        .rule("Indent if-then-else")
+            .inside(NODE_IF_ELSE)
+            .not_matching([T![if], T![then], T![else]])
+            .set(Indent)
+            .test(r#"
+                if
+                foo
+                then
+                bar
+                else
+                baz
+            "#, r#"
+                if
+                  foo
+                then
+                  bar
+                else
+                  baz
+            "#)
+
+        .rule("Indent inherit parts")
+            .inside(NODE_INHERIT)
+            .set(Indent)
+            .test(r#"
+                {
+                  inherit
+                  (builtins)
+                  # comment
+                  toString
+                  ;
+                }
+            "#, r#"
+                {
+                  inherit
+                    (builtins)
+                    # comment
+                    toString
+                    ;
+                }
+            "#)
+    ;
 
-    find(body) == Some(true)
+    dsl
 }
 
-fn with_body(body: SyntaxElement<'_>) -> bool {
-    fn find(body: SyntaxElement<'_>) -> Option<bool> {
-        let body = body.as_node()?;
-        let with = body.parent().and_then(With::cast)?;
-        Some(with.body()? == body)
-    }
-
-    find(body) == Some(true)
+fn not_on_top_level(element: SyntaxElement<'_>) -> bool {
+    !on_top_level(element)
 }
 
-fn apply_arg(arg: SyntaxElement<'_>) -> bool {
-    fn find(arg: SyntaxElement<'_>) -> Option<bool> {
-        let arg = arg.as_node()?;
-        let apply = arg.parent().and_then(Apply::cast)?;
-        Some(apply.value()? == arg)
+fn on_top_level(element: SyntaxElement<'_>) -> bool {
+    let parent = match element.parent() {
+        None => return true,
+        Some(it) => it,
+    };
+    match parent.kind() {
+        NODE_ROOT => true,
+        NODE_LAMBDA => on_top_level(parent.into()),
+        _ => false,
     }
-
-    find(arg) == Some(true)
 }
 
 fn set_entry_with_single_line_value(entry: SyntaxElement<'_>) -> bool {
@@ -221,8 +430,6 @@ fn rhs_of_binop(rhs: SyntaxElement<'_>) -> bool {
     find(rhs) == Some(true)
 }
 
-static ENTRY_OWNERS: &[SyntaxKind] = &[NODE_SET, NODE_LET_IN];
-
 static VALUES: &[SyntaxKind] = &[
     NODE_LAMBDA,
     NODE_IDENT,
@@ -238,20 +445,6 @@ static VALUES: &[SyntaxKind] = &[
     NODE_IF_ELSE,
 ];
 
-static VALUES_NOT_LAMBDA: &[SyntaxKind] = &[
-    NODE_IDENT,
-    NODE_INDEX_SET,
-    NODE_LET_IN,
-    NODE_LIST,
-    NODE_OPERATION,
-    NODE_PAREN,
-    NODE_SET,
-    NODE_STRING,
-    NODE_VALUE,
-    NODE_APPLY,
-    NODE_IF_ELSE,
-];
-
 static BIN_OPS: &[SyntaxKind] = &[
     T!["//"],
     T![++],
@@ -276,7 +469,7 @@ mod tests {
         path::{Path, PathBuf},
     };
 
-    use crate::reformat_string;
+    use crate::{reformat_string, rules::indentation2};
 
     #[test]
     fn smoke() {
@@ -303,12 +496,23 @@ foo = x:
     /// right next to the corresponding rule definition. This test looks at the
     /// text of this file, extracts test cases from comments and checks them.
     #[test]
-    fn test_inline_tests() {
+    fn test_inline_spacing_tests() {
         let this_file = include_str!("./rules.rs");
         let tests = TestCase::collect_from_comments(this_file);
         run(&tests);
     }
 
+    #[test]
+    fn test_inline_indentation_tests() {
+        let rules = indentation2();
+        let tests: Vec<TestCase> = rules
+            .tests
+            .iter()
+            .map(|&(before, after)| TestCase::from_before_after(before, after))
+            .collect();
+        run(&tests)
+    }
+
     #[test]
     fn test_bad_good_tests() {
         let test_data = {
@@ -337,6 +541,14 @@ foo = x:
     }
 
     impl TestCase {
+        fn from_before_after(before: &str, after: &str) -> TestCase {
+            TestCase {
+                name: None,
+                before: unindent::unindent(before),
+                after: unindent::unindent(after),
+            }
+        }
+
         fn try_from(line: &str) -> Option<TestCase> {
             let divisor = line.find("=>")?;
             let before = format!("{}\n", line[..divisor].trim());


Test patch:
diff --git a/test_data/curried_fn.good.nix b/test_data/curried_fn.good.nix
index cdf5c2c..5dcb464 100644
--- a/test_data/curried_fn.good.nix
+++ b/test_data/curried_fn.good.nix
@@ -4,5 +4,5 @@
       err = t: v: abort
         "oups";
     in
-    92;
+      92;
 }
diff --git a/test_data/indent_let.good.nix b/test_data/indent_let.good.nix
index 04b44d2..8141353 100644
--- a/test_data/indent_let.good.nix
+++ b/test_data/indent_let.good.nix
@@ -3,4 +3,4 @@ let
   y = 2;
   inherit z;
 in
-x + y + z
+  x + y + z
diff --git a/test_data/top_level_with.good.nix b/test_data/top_level_with.good.nix
index 235bbc4..5fe3498 100644
--- a/test_data/top_level_with.good.nix
+++ b/test_data/top_level_with.good.nix
@@ -3,4 +3,4 @@ with stdenv.lib;
 let
   foo = 92;
 in
-foo
+  foo