How to debug custom lint rules
Opened this issue · 1 comments
This isn't a bug report, more like a documentation attempt. I've been repeatedly hitting speed-bumps when trying to introduce custom project-specific HLint rules; at least to me, rule writing feels harder than it should be. Perhaps some hints could help future me, or others.
I'm assuming a flow like the following.
- Spot a "popular" but flawed code pattern in a codebase.
- Write an improved replacement.
- Write a hlint rule, for the double-duty:
- statically find the pattern in existing code for mass-change;
- prevent popping in again in the future.
- Apply, build, test, commit... continue as usual.
Alas, something goes wrong at step 3. The new rule doesn't trigger, No hints
. What do?
Conventional first steps
-
Skim the output of
--help
, discover and try--verbose
&--show
. -
Consult the README, several times if impatient.
-
Minimize a self-contained small source snippet where your rule should trigger. There's no need for it to typecheck, it only has to parse as well-formed Haskell syntax. Test your rule on it. Experiment.
(That's generically-applicable troubleshooting advice; as such, I'd assume it's well-known anyway. However, I'd been wrong.)
HLint-specific gotchas
-
hlint considers only single-letter identifiers as rules' variables. Anything longer will match literally.
-
hlint has special interpretations for
()
,$
,(.)
and possibly other haskell syntax. -
know thy yaml syntax, and its symbols. Try Dhall if unsure.
Renamed as
imports
The AST that HLint works on, comes after GHC renamer (which is the chunk of compiler logic that resolves names). This means that your rules might need to refer to identifiers by fully-qualified original names.
Example
-- sample source file
import Control.Exception (handle)
import qualified AcmeProject.Logger as Log
main = handle onException $ do
Log.debug "example"
Log.info "OK"
where
onException (err :: SomeException)
= Log.error "not ok" >> Log.exception err
# .hlint.yaml rules file
#-- won't trigger:
warn:
lhs: Log.error a >> Log.exception b
rhs: Log.exceptionCtx a b
#-- won't trigger either:
warn:
lhs: error a >> exception b
rhs: exceptionCtx a b
#-- this one does work!
warn:
lhs: AcmeProject.Logger.error a >> AcmeProject.Logger.exception b
rhs: AcmeProject.Logger.exceptionCtx a b
I agree better documentation is needed especially for scoping. Regarding your example, I think the second hint (with unqualified names) should trigger - it doesn't make much sense that it doesn't.