git.iko.soy/rowtype-yoga/purslint
on main help sign in
Fix multiline formatting in UseCaseOf refactoring

The "Convert to case _ of" fix was smooshing nested case expressions onto a single line. The bug was in printLeadingComment which misinterpreted the CST Line comment type - the Int parameter is the number of newlines, not spaces. - Fix Line comment handling in UseCaseOf, LetToDo, and LetToWhere - Add printExprTokens to properly shift whitespace when reindenting - Add regression test for nested case expression formatting ๐Ÿ’˜ Generated with Crush Assisted-by: Claude Sonnet 4 via Crush <crush@charm.land>

commitc4666fd7bfd33433844347cc7a5afc6888931044 authorMark <mark.eibes@gmail.com> dateMon 26 Jan 2026, 12:37 (8 months ago) parentd246efa
4 files changed+295โˆ’63
lib/purelint/src/Purelint/Rules/LetToDo.purs+7โˆ’2 lib/purelint/src/Purelint/Rules/LetToWhere.purs+7โˆ’2 lib/purelint/src/Purelint/Rules/UseCaseOf.purs+127โˆ’59 test/Test/Main.purs+154
lib/purelint/src/Purelint/Rules/LetToDo.purs+7โˆ’2
@@ -17,7 +17,7 @@ import PureScript.CST.Print (printToken)
1717import PureScript.CST.Range (rangeOf, tokensOf)
1818import PureScript.CST.Range.TokenList as TokenList
1919import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
20โˆ’import PureScript.CST.Types (Comment(..), Declaration(..), Expr(..), Guarded(..), LetBinding, LineFeed, Module, SourceToken, Where(..))
20+import PureScript.CST.Types (Comment(..), Declaration(..), Expr(..), Guarded(..), LetBinding, LineFeed(..), Module, SourceToken, Where(..))
2121
2222-- | Rule: let x = y in z -> do let x = y; z
2323-- | Suggests replacing let...in expressions with do notation
@@ -142,7 +142,12 @@ letToDoRule = mkRule (RuleId "LetToDo") run
142142 printLeadingComment shiftAmount = case _ of
143143 Comment str -> str
144144 Space n -> power " " (max 0 (n + shiftAmount))
145โˆ’ Line _ n -> "\n" <> power " " (max 0 (n + shiftAmount))
145+ Line lf n -> foldMap printLineFeed (Array.replicate n lf)
146+
147+ printLineFeed :: LineFeed -> String
148+ printLineFeed = case _ of
149+ LF -> "\n"
150+ CRLF -> "\r\n"
146151
147152 printTrailingComment :: Comment Void -> String
148153 printTrailingComment = case _ of
lib/purelint/src/Purelint/Rules/LetToWhere.purs+7โˆ’2
@@ -16,7 +16,7 @@ import PureScript.CST.Print (printToken)
1616import PureScript.CST.Range (class RangeOf, rangeOf, tokensOf)
1717import PureScript.CST.Range.TokenList as TokenList
1818import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
19โˆ’import PureScript.CST.Types (Comment(..), Declaration(..), Expr(..), Guarded(..), LetBinding, LineFeed, Module, SourceToken, Where(..))
19+import PureScript.CST.Types (Comment(..), Declaration(..), Expr(..), Guarded(..), LetBinding, LineFeed(..), Module, SourceToken, Where(..))
2020
2121-- | Rule: let x = y in z -> z where x = y
2222-- | Suggests replacing let...in expressions with where clauses
@@ -105,7 +105,12 @@ letToWhereRule = mkRule (RuleId "LetToWhere") run
105105 printLeadingComment shiftAmount = case _ of
106106 Comment str -> str
107107 Space n -> power " " (max 0 (n + shiftAmount))
108โˆ’ Line _ n -> "\n" <> power " " (max 0 (n + shiftAmount))
108+ Line lf n -> foldMap printLineFeed (Array.replicate n lf)
109+
110+ printLineFeed :: LineFeed -> String
111+ printLineFeed = case _ of
112+ LF -> "\n"
113+ CRLF -> "\r\n"
109114
110115 -- | Print trailing comment (has Void, so Line is impossible)
111116 printTrailingComment :: Comment Void -> String
lib/purelint/src/Purelint/Rules/UseCaseOf.purs+127โˆ’59
@@ -7,17 +7,18 @@ import Data.Array.NonEmpty as NEA
77import Data.Foldable (foldMap)
88import Data.Maybe (Maybe(..))
99import Data.Monoid (power)
10โˆ’import Data.Newtype (un)
1110import Data.String as String
12โˆ’import Data.Tuple (Tuple(..), snd)
11+import Data.Tuple (Tuple(..))
1312import Data.Tuple.Nested ((/\))
14โˆ’import Data.Void (Void)
13+import Data.Void (Void, absurd)
1514import Purelint.Print (printBinder, printExpr)
1615import Purelint.Rule (Rule, RuleContext, mkRule)
1716import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
18โˆ’import PureScript.CST.Range (rangeOf)
17+import PureScript.CST.Print (printToken)
18+import PureScript.CST.Range (rangeOf, tokensOf)
19+import PureScript.CST.Range.TokenList as TokenList
1920import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
20โˆ’import PureScript.CST.Types (Binder(..), Declaration(..), Guarded(..), GuardedExpr(..), Ident(..), LetBinding(..), Module(..), ModuleBody(..), Name(..), ValueBindingFields, Where(..))
21+import PureScript.CST.Types (Binder(..), Comment(..), Declaration(..), Expr, Guarded(..), GuardedExpr(..), Ident(..), LetBinding(..), LineFeed(..), Module(..), ModuleBody(..), Name(..), SourceToken, ValueBindingFields, Where(..))
2122
2223-- | Rule: Functions with multiple clauses where the name repeats can use case _ of
2324-- |
@@ -33,12 +34,13 @@ useCaseOfRule :: Rule
3334useCaseOfRule = mkRule (RuleId "UseCaseOf") run
3435 where
3536 run :: RuleContext -> Module Void -> Array LintWarning
36โˆ’ run _ctx mod@(Module m) =
37โˆ’ let
37+ run _ctx mod@(Module m) =
38+ let
3839 ModuleBody body = m.body
3940 topLevelWarnings = groupByName body.decls # Array.concatMap (checkGroup 0)
4041 nestedWarnings = foldMapModule visitor mod
41โˆ’ in topLevelWarnings <> nestedWarnings
42+ in
43+ topLevelWarnings <> nestedWarnings
4244 where
4345 visitor = defaultMonoidalVisitor { onDecl = checkDecl }
4446
@@ -62,7 +64,7 @@ useCaseOfRule = mkRule (RuleId "UseCaseOf") run
6264
6365 checkLetBindings :: NEA.NonEmptyArray (LetBinding Void) -> Array LintWarning
6466 checkLetBindings bindings =
65โˆ’ let
67+ let
6668 vbs = NEA.toArray bindings # Array.mapMaybe case _ of
6769 LetBindingName vb -> Just vb
6870 _ -> Nothing
@@ -71,7 +73,8 @@ useCaseOfRule = mkRule (RuleId "UseCaseOf") run
7173 Just vb -> (rangeOf (LetBindingName vb)).start.column
7274 Nothing -> 0
7375 groups = groupByNameVB vbs
74โˆ’ in groups # Array.concatMap (checkGroup baseCol)
76+ in
77+ groups # Array.concatMap (checkGroup baseCol)
7578
7679 -- Group consecutive DeclValue with same name (for top-level)
7780 groupByName :: Array (Declaration Void) -> Array (Array (ValueBindingFields Void))
@@ -84,11 +87,13 @@ useCaseOfRule = mkRule (RuleId "UseCaseOf") run
8487 Just c -> Array.snoc acc c.clauses
8588 Just { head: d, tail: rest } -> case d of
8689 DeclValue vb ->
87โˆ’ let name = getName vb
88โˆ’ in case current of
89โˆ’ Nothing -> go acc (Just { name, clauses: [vb] }) rest
90โˆ’ Just c | c.name == name -> go acc (Just { name, clauses: Array.snoc c.clauses vb }) rest
91โˆ’ Just c -> go (Array.snoc acc c.clauses) (Just { name, clauses: [vb] }) rest
90+ let
91+ name = getName vb
92+ in
93+ case current of
94+ Nothing -> go acc (Just { name, clauses: [ vb ] }) rest
95+ Just c | c.name == name -> go acc (Just { name, clauses: Array.snoc c.clauses vb }) rest
96+ Just c -> go (Array.snoc acc c.clauses) (Just { name, clauses: [ vb ] }) rest
9297 _ -> case current of
9398 Nothing -> go acc Nothing rest
9499 Just c -> go (Array.snoc acc c.clauses) Nothing rest
@@ -103,11 +108,13 @@ useCaseOfRule = mkRule (RuleId "UseCaseOf") run
103108 Nothing -> acc
104109 Just c -> Array.snoc acc c.clauses
105110 Just { head: vb, tail: rest } ->
106โˆ’ let name = getName vb
107โˆ’ in case current of
108โˆ’ Nothing -> go acc (Just { name, clauses: [vb] }) rest
109โˆ’ Just c | c.name == name -> go acc (Just { name, clauses: Array.snoc c.clauses vb }) rest
110โˆ’ Just c -> go (Array.snoc acc c.clauses) (Just { name, clauses: [vb] }) rest
111+ let
112+ name = getName vb
113+ in
114+ case current of
115+ Nothing -> go acc (Just { name, clauses: [ vb ] }) rest
116+ Just c | c.name == name -> go acc (Just { name, clauses: Array.snoc c.clauses vb }) rest
117+ Just c -> go (Array.snoc acc c.clauses) (Just { name, clauses: [ vb ] }) rest
111118
112119 getName :: ValueBindingFields Void -> String
113120 getName vb = case vb.name of
@@ -117,53 +124,114 @@ useCaseOfRule = mkRule (RuleId "UseCaseOf") run
117124 checkGroup baseCol clauses
118125 | Array.length clauses < 2 = []
119126 | not (Array.all hasBindersAndUnconditional clauses) = []
120โˆ’ | otherwise = case Array.head clauses of
121โˆ’ Nothing -> []
122โˆ’ Just first ->
123โˆ’ let
124โˆ’ name = getName first
125โˆ’ numArgs = Array.length first.binders
126โˆ’ -- Check all clauses have same number of binders
127โˆ’ sameBinders = Array.all (\vb -> Array.length vb.binders == numArgs) clauses
128โˆ’ in
129โˆ’ if not sameBinders || numArgs == 0 then []
130โˆ’ else
131โˆ’ let
132โˆ’ -- Build the replacement
133โˆ’ indent = power " " baseCol
134โˆ’ wildcards = Array.replicate numArgs "_" # String.joinWith ", "
135โˆ’ caseHeader = name <> " = case " <> wildcards <> " of"
136โˆ’ caseArms = clauses # Array.mapMaybe formatClause
137โˆ’ replacement = caseHeader <> "\n" <> (caseArms # map ((indent <> " ") <> _) # String.joinWith "\n")
138โˆ’ -- Range spans from first clause to last
139โˆ’ firstRange = rangeOf (LetBindingName first)
140โˆ’ lastRange = case Array.last clauses of
141โˆ’ Just lst -> rangeOf (LetBindingName lst)
142โˆ’ Nothing -> firstRange
143โˆ’ fullRange = { start: firstRange.start, end: lastRange.end }
144โˆ’ in
145โˆ’ [ LintWarning
146โˆ’ { ruleId: RuleId "UseCaseOf"
147โˆ’ , message: WarningMessage $ "Function '" <> name <> "' has " <> show (Array.length clauses) <> " clauses - consider using case _ of"
148โˆ’ , range: fullRange
149โˆ’ , severity: Refactor
150โˆ’ , suggestion: Just $ Suggestion
151โˆ’ { replacement: ReplacementText replacement
152โˆ’ , description: SuggestionDescription "Convert to case _ of"
153โˆ’ }
154โˆ’ }
155โˆ’ ]
127+ | otherwise = foldMap
128+ ( \first ->
129+ let
130+ name = getName first
131+ numArgs = Array.length first.binders
132+ -- Check all clauses have same number of binders
133+ sameBinders = Array.all (\vb -> Array.length vb.binders == numArgs) clauses
134+ in
135+ if not sameBinders || numArgs == 0 then []
136+ else
137+ let
138+ -- Build the replacement
139+ indent = power " " baseCol
140+ wildcards = Array.replicate numArgs "_" # String.joinWith ", "
141+ caseHeader = name <> " = case " <> wildcards <> " of"
142+ caseArms = clauses # Array.mapMaybe (formatClause (baseCol + 2))
143+ replacement = caseHeader <> "\n" <> (caseArms # map ((indent <> " ") <> _) # String.joinWith "\n")
144+ -- Range spans from first clause to last
145+ firstRange = rangeOf (LetBindingName first)
146+ lastRange = case Array.last clauses of
147+ Just lst -> rangeOf (LetBindingName lst)
148+ Nothing -> firstRange
149+ fullRange = { start: firstRange.start, end: lastRange.end }
150+ in
151+ [ LintWarning
152+ { ruleId: RuleId "UseCaseOf"
153+ , message: WarningMessage $ "Function '" <> name <> "' has " <> show (Array.length clauses) <> " clauses - consider using case _ of"
154+ , range: fullRange
155+ , severity: Refactor
156+ , suggestion: Just $ Suggestion
157+ { replacement: ReplacementText replacement
158+ , description: SuggestionDescription "Convert to case _ of"
159+ }
160+ }
161+ ]
162+ )
163+ (Array.head clauses)
156164
157165 hasBindersAndUnconditional :: ValueBindingFields Void -> Boolean
158166 hasBindersAndUnconditional vb = case vb.guarded of
159167 Unconditional _ (Where { bindings: Nothing }) -> Array.length vb.binders > 0
168+ Unconditional _ (Where { bindings: Just _ }) -> Array.length vb.binders > 0
160169 _ -> false
161170
162โˆ’ formatClause :: ValueBindingFields Void -> Maybe String
163โˆ’ formatClause vb = case vb.guarded of
171+ formatClause :: Int -> ValueBindingFields Void -> Maybe String
172+ formatClause baseCol vb = case vb.guarded of
164173 Unconditional _ (Where { expr, bindings: Nothing }) ->
174+ let
175+ binders = vb.binders # map printBinder # String.joinWith ", "
176+ exprRange = rangeOf expr
177+ isMultiline = exprRange.start.line /= exprRange.end.line
178+ in
179+ if isMultiline
180+ then
181+ let
182+ -- Target indentation for the expression body
183+ targetCol = baseCol + 2
184+ body = printExprTokens targetCol expr
185+ in
186+ Just $ binders <> " ->\n" <> power " " targetCol <> body
187+ else Just $ binders <> " -> " <> printExpr expr
188+ Unconditional _ (Where { expr, bindings: Just (_ /\ letBindings) }) ->
165189 let
166190 binders = vb.binders # map printBinder # String.joinWith ", "
167191 body = printExpr expr
168โˆ’ in Just $ binders <> " -> " <> body
192+ indent = power " " baseCol
193+ -- Format the where bindings with proper indentation
194+ whereBindings = NEA.toArray letBindings # map (printBindingTokens (baseCol + 4))
195+ whereText = String.joinWith ("\n" <> indent <> " ") whereBindings
196+ in
197+ Just $ binders <> " ->\n" <> indent <> " " <> body <> "\n" <> indent <> " where\n" <> indent <> " " <> whereText
169198 _ -> Nothing
199+
200+ printExprTokens :: Int -> Expr Void -> String
201+ printExprTokens targetCol expr =
202+ let
203+ exprCol = (rangeOf expr).start.column
204+ shiftAmount = targetCol - exprCol
205+ in
206+ String.trim $ foldMap (printSourceToken shiftAmount) (TokenList.toArray (tokensOf expr))
207+
208+ printBindingTokens :: Int -> LetBinding Void -> String
209+ printBindingTokens targetCol b =
210+ let
211+ bindingCol = (rangeOf b).start.column
212+ shiftAmount = targetCol - bindingCol
213+ in
214+ String.trim $ foldMap (printSourceToken shiftAmount) (TokenList.toArray (tokensOf b))
215+
216+ printSourceToken :: Int -> SourceToken -> String
217+ printSourceToken shiftAmount tok =
218+ foldMap (printLeadingComment shiftAmount) tok.leadingComments
219+ <> printToken tok.value
220+ <> foldMap printTrailingComment tok.trailingComments
221+
222+ printLeadingComment :: Int -> Comment LineFeed -> String
223+ printLeadingComment shiftAmount = case _ of
224+ Comment str -> str
225+ Space n -> power " " (max 0 (n + shiftAmount))
226+ Line lf n -> foldMap printLineFeed (Array.replicate n lf)
227+
228+ printLineFeed :: LineFeed -> String
229+ printLineFeed = case _ of
230+ LF -> "\n"
231+ CRLF -> "\r\n"
232+
233+ printTrailingComment :: Comment Void -> String
234+ printTrailingComment = case _ of
235+ Comment str -> str
236+ Space n -> power " " n
237+ Line v _ -> absurd v
test/Test/Main.purs+154
@@ -90,6 +90,7 @@ import Purelint.Rules.EvaluateEither (evaluateEitherRule)
9090import Purelint.Rules.UseCaseOf (useCaseOfRule)
9191import Purelint.Rules.FlattenCase (flattenCaseRule)
9292import Purelint.Rules.UsePatternGuards (usePatternGuardsRule)
93+import Purelint.Rules.RedundantParens (redundantParensRule)
9394import Purelint.Rule (Rule)
9495import Purelint.Runner (runRules, getImportsFromSource, getModuleNames)
9596import Purelint.Imports (hasValue, isPreludeModule)
@@ -241,6 +242,7 @@ main = do
241242 <> useCaseOfTests
242243 <> flattenCaseTests
243244 <> usePatternGuardsTests
245+ <> redundantParensTests
244246
245247 -- Run effectful golden tests (with purs-tidy check)
246248 goldenResults <- goldenTests
@@ -1248,6 +1250,16 @@ useFoldMapTests =
12481250 (withPrelude "x mx = case mx of\n Nothing -> \"\"\n Just s -> s <> \"!\"") "foldMap (\\s -> s <> \"!\") mx"
12491251 , assertWarningCount "UseFoldMap: reversed order Just/Nothing" [useFoldMapRule]
12501252 (withPrelude "x mx = case mx of\n Just s -> f s\n Nothing -> \"\"") 1
1253+
1254+ -- Complex scrutinee should be wrapped in parens
1255+ , assertSuggestion "UseFoldMap: complex scrutinee gets parens" [useFoldMapRule]
1256+ (withPrelude "x f mx = case f mx of\n Nothing -> \"\"\n Just s -> g s") "foldMap g (f mx)"
1257+
1258+ -- Negative cases: should NOT detect
1259+ , assertWarningCount "UseFoldMap: no fire on guarded Just branch" [useFoldMapRule]
1260+ (withPrelude "x mx = case mx of\n Just s | s > \"\" -> f s\n _ -> \"\"") 0
1261+ , assertWarningCount "UseFoldMap: no fire on where binding in Just branch" [useFoldMapRule]
1262+ (withPrelude "x mx = case mx of\n Just s -> g s where g = f\n Nothing -> \"\"") 0
12511263 ]
12521264
12531265-- ============================================================================
@@ -1478,6 +1490,18 @@ useCaseOfTests =
14781490 , assertSuggestion "UseCaseOf: two args suggestion" [useCaseOfRule]
14791491 (withPrelude "add True True = 2\nadd True False = 1\nadd False True = 1\nadd False False = 0") "add = case _, _ of\n True, True -> 2\n True, False -> 1\n False, True -> 1\n False, False -> 0"
14801492
1493+ -- Clauses with where bindings
1494+ , assertWarningCount "UseCaseOf: clause with where binding" [useCaseOfRule]
1495+ (withPrelude "foo x = go x\n where\n isJoin True = x\n where x = 1\n isJoin False = 0") 1
1496+ , assertSuggestion "UseCaseOf: clause with where binding suggestion" [useCaseOfRule]
1497+ (withPrelude "foo x = go x\n where\n isJoin True = x\n where x = 1\n isJoin False = 0") "isJoin = case _ of\n True ->\n x\n where\n x = 1\n False -> 0"
1498+
1499+ -- Regression test: nested case expression should preserve multiline formatting
1500+ , assertWarningCount "UseCaseOf: nested case expression" [useCaseOfRule]
1501+ (withPrelude "extractFn (Just x) =\n case x of\n 1 -> \"one\"\n _ -> \"other\"\nextractFn Nothing = \"nothing\"") 1
1502+ , assertSuggestion "UseCaseOf: nested case should preserve newlines" [useCaseOfRule]
1503+ (withPrelude "extractFn (Just x) =\n case x of\n 1 -> \"one\"\n _ -> \"other\"\nextractFn Nothing = \"nothing\"") "extractFn = case _ of\n (Just x) ->\n case x of\n 1 -> \"one\"\n _ -> \"other\"\n Nothing -> \"nothing\""
1504+
14811505 -- Negative cases: should NOT detect
14821506 , assertWarningCount "UseCaseOf: single clause" [useCaseOfRule]
14831507 (withPrelude "foo x = x + 1") 0
@@ -1534,6 +1558,131 @@ usePatternGuardsTests =
15341558 (withPrelude "foo x y = case x of\n Just a\n | Just b <- y -> a + b\n _ -> 0")
15351559 ]
15361560
1561+-- ============================================================================
1562+-- RedundantParens Tests
1563+-- ============================================================================
1564+
1565+redundantParensTests :: Array TestResult
1566+redundantParensTests =
1567+ -- Positive cases: should detect redundant parens
1568+ [ assertWarningCount "RedundantParens: nested parens ((x))" [redundantParensRule]
1569+ (withPrelude "x = ((1))") 1
1570+ , assertRuleId "RedundantParens: correct rule id" [redundantParensRule]
1571+ (withPrelude "x = ((1))") "RedundantParens"
1572+ , assertSuggestion "RedundantParens: nested parens suggestion" [redundantParensRule]
1573+ (withPrelude "x = ((1))") "(1)"
1574+
1575+ -- Function arguments with atomic expressions
1576+ , assertWarningCount "RedundantParens: literal in arg" [redundantParensRule]
1577+ (withPrelude "x = show (42)") 1
1578+ , assertSuggestion "RedundantParens: literal in arg suggestion" [redundantParensRule]
1579+ (withPrelude "x = show (42)") "42"
1580+ , assertWarningCount "RedundantParens: identifier in arg" [redundantParensRule]
1581+ (withPrelude "x y = show (y)") 1
1582+ , assertWarningCount "RedundantParens: string in arg" [redundantParensRule]
1583+ (withPrelude "x = show (\"hi\")") 1
1584+ , assertWarningCount "RedundantParens: array in arg" [redundantParensRule]
1585+ (withPrelude "x = length ([1, 2])") 1
1586+ , assertWarningCount "RedundantParens: record in arg" [redundantParensRule]
1587+ (withPrelude "x = show ({ a: 1 })") 1
1588+
1589+ -- Multiple redundant args
1590+ , assertWarningCount "RedundantParens: two redundant args" [redundantParensRule]
1591+ (withPrelude "x = add (1) (2)") 2
1592+
1593+ -- Array elements
1594+ , assertWarningCount "RedundantParens: array element literal" [redundantParensRule]
1595+ (withPrelude "x = [(1), (2)]") 2
1596+ , assertWarningCount "RedundantParens: array mixed" [redundantParensRule]
1597+ (withPrelude "x = [(1), 2, (3)]") 2
1598+ , assertWarningCount "RedundantParens: no fire on complex array element" [redundantParensRule]
1599+ (withPrelude "x = [(1 + 2)]") 0
1600+
1601+ -- Negative cases: should NOT detect
1602+ , assertWarningCount "RedundantParens: no fire on precedence (1+2)*3" [redundantParensRule]
1603+ (withPrelude "x = (1 + 2) * 3") 0
1604+ , assertWarningCount "RedundantParens: no fire on complex arg" [redundantParensRule]
1605+ (withPrelude "x = show (1 + 2)") 0
1606+ , assertWarningCount "RedundantParens: no fire on lambda arg" [redundantParensRule]
1607+ (withPrelude "x = map (\\y -> y + 1) [1]") 0
1608+ , assertWarningCount "RedundantParens: no fire on negate arg" [redundantParensRule]
1609+ (withPrelude "x f = f (-1)") 0
1610+ , assertWarningCount "RedundantParens: no fire on section" [redundantParensRule]
1611+ (withPrelude "x = (_ + 1)") 0
1612+ , assertWarningCount "RedundantParens: no fire on if" [redundantParensRule]
1613+ (withPrelude "x = (if true then 1 else 2)") 0
1614+ , assertWarningCount "RedundantParens: no fire on let" [redundantParensRule]
1615+ (withPrelude "x = (let y = 1 in y)") 0
1616+
1617+ -- Outer parens on RHS (simple cases)
1618+ , assertWarningCount "RedundantParens: outer parens on literal RHS" [redundantParensRule]
1619+ (withPrelude "x = (42)") 1
1620+ , assertWarningCount "RedundantParens: outer parens on app RHS" [redundantParensRule]
1621+ (withPrelude "x = (show 1)") 1
1622+ , assertWarningCount "RedundantParens: outer parens on op RHS" [redundantParensRule]
1623+ (withPrelude "x = (1 + 2)") 1
1624+
1625+ -- If branches
1626+ , assertWarningCount "RedundantParens: if condition" [redundantParensRule]
1627+ (withPrelude "x = if (true) then 1 else 2") 1
1628+ , assertWarningCount "RedundantParens: if then branch" [redundantParensRule]
1629+ (withPrelude "x = if true then (1) else 2") 1
1630+
1631+ -- Case branches
1632+ , assertWarningCount "RedundantParens: case scrutinee" [redundantParensRule]
1633+ (withPrelude "x = case (1) of y -> y") 1
1634+ , assertWarningCount "RedundantParens: case body" [redundantParensRule]
1635+ (withPrelude "x = case 1 of y -> (y)") 1
1636+
1637+ -- Let expression
1638+ , assertWarningCount "RedundantParens: let binding" [redundantParensRule]
1639+ (withPrelude "x = let y = (1) in y") 1
1640+ , assertWarningCount "RedundantParens: let body" [redundantParensRule]
1641+ (withPrelude "x = let y = 1 in (y)") 1
1642+
1643+ -- Record fields
1644+ , assertWarningCount "RedundantParens: record field" [redundantParensRule]
1645+ (withPrelude "x = { a: (1) }") 1
1646+ , assertWarningCount "RedundantParens: multiple record fields" [redundantParensRule]
1647+ (withPrelude "x = { a: (1), b: (2) }") 2
1648+
1649+ -- Pattern (binder) cases
1650+ , assertWarningCount "RedundantParens: nested parens in pattern" [redundantParensRule]
1651+ (withPrelude "f ((x)) = x") 1
1652+ , assertSuggestion "RedundantParens: nested parens in pattern suggestion" [redundantParensRule]
1653+ (withPrelude "f ((x)) = x") "(x)"
1654+ , assertWarningCount "RedundantParens: atomic pattern in constructor arg" [redundantParensRule]
1655+ (withPrelude "f (Just (x)) = x\nf Nothing = 0") 1
1656+ , assertSuggestion "RedundantParens: constructor arg suggestion" [redundantParensRule]
1657+ (withPrelude "f (Just (x)) = x\nf Nothing = 0") "x"
1658+ , assertWarningCount "RedundantParens: wildcard in constructor" [redundantParensRule]
1659+ (withPrelude "f (Just (_)) = 1\nf Nothing = 0") 1
1660+ , assertWarningCount "RedundantParens: literal in constructor" [redundantParensRule]
1661+ (withPrelude "f (Just (1)) = true\nf _ = false") 1
1662+ , assertWarningCount "RedundantParens: array pattern element" [redundantParensRule]
1663+ (withPrelude "f [(x), (y)] = x + y\nf _ = 0") 2
1664+
1665+ -- Negative pattern cases
1666+ , assertWarningCount "RedundantParens: no fire on constructor with args" [redundantParensRule]
1667+ (withPrelude "f (Just (Right x)) = x\nf _ = 0") 0
1668+ , assertWarningCount "RedundantParens: no fire on typed pattern" [redundantParensRule]
1669+ (withPrelude "f (x :: Int) = x") 0
1670+ , assertWarningCount "RedundantParens: no fire on negated number pattern" [redundantParensRule]
1671+ (withPrelude "f (Just (-1)) = true\nf _ = false") 0
1672+
1673+ -- Top-level case pattern parens
1674+ , assertWarningCount "RedundantParens: top-level case pattern" [redundantParensRule]
1675+ (withPrelude "f = case _ of\n (Just x) -> x\n Nothing -> 0") 1
1676+ , assertSuggestion "RedundantParens: top-level case pattern suggestion" [redundantParensRule]
1677+ (withPrelude "f = case _ of\n (Just x) -> x\n Nothing -> 0") "Just x"
1678+ , assertWarningCount "RedundantParens: top-level constructor pattern" [redundantParensRule]
1679+ (withPrelude "f = case _ of\n (Foo a b) -> a\n _ -> 0") 1
1680+ , assertWarningCount "RedundantParens: no fire on cons pattern" [redundantParensRule]
1681+ (withPrelude "f = case _ of\n (x : xs) -> x\n _ -> 0") 0
1682+ , assertWarningCount "RedundantParens: no fire on typed case pattern" [redundantParensRule]
1683+ (withPrelude "f = case _ of\n (x :: Int) -> x") 0
1684+ ]
1685+
15371686-- | Helper for Alternative imports
15381687withAlt :: String -> String
15391688withAlt code = """
@@ -1793,6 +1942,11 @@ goldenTests = traverse identity
17931942 , pure $ assertFix "Golden: UsePatternGuards preserves do-let" [usePatternGuardsRule]
17941943 (withPrelude "checkExpr expr =\n case expr of\n Just mapArgs ->\n case toArray mapArgs of\n [ x, y ] -> do\n let\n f = show x\n g = show y\n [ f, g ]\n _ -> []\n _ -> []")
17951944 (withPrelude "checkExpr expr =\n case expr of\n Just mapArgs\n | [ x, y ] <- toArray mapArgs -> do\n let\n f = show x\n g = show y\n [ f, g ]\n _ -> [ ]")
1945+
1946+ -- UseFoldMap: should preserve multiline formatting in complex body
1947+ , pure $ assertFix "Golden: UseFoldMap preserves multiline body" [useFoldMapRule]
1948+ (withPrelude "checkExpr mx =\n case mx of\n Just p ->\n let\n pText = show p\n in\n [ pText ]\n Nothing -> []")
1949+ (withPrelude "checkExpr mx =\n foldMap\n ( \\p ->\n let\n pText = show p\n in\n [ pText ]\n )\n mx")
17961950 ]
17971951
17981952-- ============================================================================