Open-source project
ndmitchell/hlint avatar
ndmitchell/hlint

HLint for Haskell code suggestions and review

Haskell source code suggestions

1,613 stars212 forksHaskellNOASSERTION

At a glance

What is it?
HLint is a Haskell source checker that reports suggested replacements, simplifications, and redundancies. It is useful for maintaining Haskell code, but its module-local analysis and parser assumptions require reviewers to judge every change.
Who is it for?
HLint is a reasonable fit for Haskell maintainers who want repeatable style and simplification findings in CI or local review. It is less suitable for projects that require type-aware, scope-aware transformations or fully automatic rewriting.
Can I use it commercially?
Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
Is it still maintained?
Yes. The repository last received commits 27 days ago.
What is it written in?
Mainly Haskell, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

HLint's module-local suggestion model

HLint addresses a specific problem in Haskell maintenance: finding expressions that can often be written more clearly, more directly, or with fewer intermediate operations. It suggests alternatives such as replacing `concat $ map escapeC s` with `concatMap escapeC s`, removing unnecessary brackets, and choosing a more efficient monadic variant such as `mapM_` when the returned values are discarded. These are source-level suggestions rather than type-directed compilation diagnostics.

The central design decision is that HLint analyzes one module at a time without knowing types or which names are in scope. The README says this is deliberate, because module isolation allows parallel processing and supports incremental use on code that may not yet type-check. That boundary explains both its speed-oriented architecture and its main limitation. HLint can recognize a familiar syntactic pattern without proving that the replacement is valid for the surrounding program.

The output is organized for code review. Each hint identifies a file and line, assigns a severity such as Warning or Suggestion, describes the issue, shows the original expression, and presents a possible replacement. The README says warnings are generally desirable, while suggestions may depend on imports, newer `base` versions, or differences of opinion. That distinction makes HLint useful as a report generator rather than a mandate for mechanical edits.

Installing HLint and scanning a Haskell tree

The documented installation path uses the standard Cabal workflow. First update the local Hackage index, then install the `hlint` package. The command below follows the README exactly and is the appropriate first step when Cabal is already available:

bash
cabal update
cabal install hlint

After installation, pass either a Haskell file or a directory to `hlint`. Directory traversal is recursive and includes files ending in `.hs` or `.lhs`. This command checks the current project tree and prints findings to standard output:

bash
hlint .

For a first review, the README recommends generating an HTML report rather than editing every diagnostic immediately:

bash
hlint . --report

That command writes `report.html`, according to the documentation. A reviewer can use the report to decide which findings merit changes. HLint should not be treated as an automatic refactoring engine, because the project explicitly says that the suggested replacement should be equivalent and asks users to report incorrect suggestions that are not listed as known limitations.

A narrower directory can be selected when only one part of a repository should be checked. The README gives `hlint src` as the CI form for a source directory, so the path remains under the reviewer's control:

bash
hlint src

Generating and editing `.hlint.yaml`

HLint's configuration uses `.hlint.yaml` by default. The README documents a staged workflow: scan the project, fix findings selectively, then generate a settings file that ignores the hints currently outstanding. The generation command is:

bash
hlint . --default > .hlint.yaml

The resulting file is not described as a universal correctness policy. It records the current project baseline, which means a later edit to the configuration can intentionally remove, add, or suppress individual findings. The README also notes that files with other names, such as `.hlint.yml`, are not discovered automatically and must be passed explicitly with `--hint=FILE`.

The documentation identifies `.hlint.yaml` as the normal configuration file and links to custom hints and rules. That means the workflow can start with generated defaults while still leaving room for project-specific rules in a larger codebase. The repository also contains an `.hlint.yaml` entry at its root, based on the repository layout, but its contents are not reproduced here and should be inspected before copying its settings into another project.

The configuration approach has a practical advantage for CI: the team can choose which findings fail a build by making policy explicit in a file. It also creates a review burden. A generated ignore list can preserve code that the team has not examined yet, while a broad ignore policy can hide new findings. The README does not document a migration command or automatic merge procedure for changing this file.

Parser extensions and preprocessing limits

HLint turns on many language extensions to parse more Haskell documents, but that policy can conflict with legal source syntax. The README gives `{-#INLINE foo#-}` as an example of code that does not work with `MagicHash`, and it explains that `foo $bar` can have a different meaning under `TemplateHaskell`. The documented remedy is to disable the relevant extension with flags such as `-XNoMagicHash` or `-XNoTemplateHaskell`.

This is a real compatibility boundary, not a cosmetic warning. A project that relies on syntax interpreted differently by an enabled extension may receive misleading suggestions or fail to parse. Projects using `Strict` face another documented risk: certain hints, including eta reduction, can be incorrect. HLint also says that `RebindableSyntax` can produce incorrect suggestions.

Custom preprocessors are outside the tool's execution path. The README specifically lists `markdown-unlit` and `record-dot-preprocessor` as examples of preprocessors HLint does not run. Code that depends on those transformations will usually fail to parse. For C preprocessor input, HLint can be configured with CPP flags, but it sees only one conditional set of code at a time. A repository with multiple target-specific branches therefore needs separate review arrangements.

Known correctness and semantic risks

The most important limitation is stated plainly by the project: some transformations can change program behavior. The README specifically identifies `seq`, where eta reduction may alter semantics. It also warns that transformed programs can require additional type signatures, particularly when the monomorphism restriction is triggered or rank-2 types are involved. In rare cases, there may be nowhere to add the required signature.

Defaulting behavior is another documented risk. HLint may produce a rewrite that causes values to default to different types, which can change runtime behavior. It also assumes that duplicate identical expressions within one expression are used at the same type. Those assumptions are plausible for a syntax-oriented checker, but they are not equivalent to full type checking.

The limitations affect adoption strategy. Running HLint during early development on code that does not compile can be useful because the tool is intentionally incremental. The same design means that a clean report is not evidence that every suggested rewrite is safe. Reviewers should preserve compiler feedback, tests, and type information from the project's normal build rather than relying on an absent HLint finding as proof of correctness.

For unlifted or unboxed types, the README notes that hints such as `Use const` do not work. That is a narrower gap, but it illustrates that a rule can be unavailable where a Haskell developer might otherwise expect it.

CI use, licensing, and release maintenance

HLint can run in continuous integration with `hlint .` or a narrower path such as `hlint src`. To avoid compilation cost, the README links to the latest HLint binary release and documents a platform-independent shell command for Travis, Appveyor, and Azure Pipelines. The command downloads a script and passes the final argument through to `hlint`:

sh
curl -sSL https://raw.github.com/ndmitchell/hlint/master/misc/run.sh | sh -s .

The final `.` can be replaced with another path or arguments, according to the README. This convenience has a deployment consequence for CI: the command executes a script fetched from the repository's `master` branch. The documentation provides that command, but it does not describe pinning the fetched script or verifying a checksum.

The repository is not archived, and its last push was on 2026-09-26. Recent releases listed by the repository are v3.10 on 2025-02-02, v3.8 on 2024-01-15, and v3.6.1 on 2023-07-03. The release history indicates versioned distribution, but the README does not provide a compatibility matrix for GHC, Cabal, or Haskell language versions.

The license is identified as custom rather than assigned a standard SPDX-style category by the repository metadata. The `LICENSE` file must be read before redistribution or integration. This review does not interpret that license or give legal advice; it only records that the project does not present a conventional license classification in the stated metadata.

Choosing HLint against a type-aware workflow

HLint is a different kind of tool from a compiler-based Haskell analysis system. Its advantage is deliberately narrow analysis: module-local pattern recognition can run in parallel and can inspect code before it type-checks. A type-aware workflow can reason about declarations, imported names, inferred types, and program-wide constraints, while HLint cannot rely on those facts. The trade-off is clear. HLint offers inexpensive, reviewable suggestions without requiring the entire project to compile, but it cannot establish equivalence for every transformation.

For a maintainer seeking broad style cleanup, that trade-off is useful. The report and `.hlint.yaml` workflow allow a team to review findings, preserve selected exceptions, and run the same command in CI. For a library whose transformations interact with advanced types, unlifted values, `seq`, strictness, or preprocessors, a compiler and tests remain necessary evidence. HLint should occupy the review and policy layer, not replace those checks.

Editors are another available surface. The README says that many editors have HLint plugins, sometimes more than one. It does not document a single canonical plugin or configuration workflow, so editor adoption should be checked separately from installing the command-line tool.

Editorial conclusion

HLint is a reasonable fit for Haskell maintainers who want repeatable style and simplification findings in CI or local review. It is less suitable for projects that require type-aware, scope-aware transformations or fully automatic rewriting. Before adopting its generated `.hlint.yaml`, inspect the custom `LICENSE` file, review the enabled language extensions, and test suggested changes against code that uses `seq`, `Strict`, `RebindableSyntax`, CPP branches, or custom preprocessors. The first concrete adoption check is whether `hlint . --report` produces findings your team can review without accepting them blindly.

Frequently asked questions

How do I install HLint?

The README documents `cabal update` followed by `cabal install hlint`. Once installed, run `hlint source`, where `source` can be a Haskell file or a directory containing `.hs` and `.lhs` files.

Does HLint replace the Haskell compiler?

No. HLint analyzes each module in isolation without knowing types or which names are in scope. The README presents this as a deliberate feature that permits parallel and incremental analysis, but suggestions still need compiler checks and tests.

How can I suppress findings that do not apply to my project?

The README documents `hlint . --default > .hlint.yaml` as a way to generate a settings file ignoring currently outstanding hints. You can then edit that file, and other file names can be supplied explicitly with `--hint=FILE`.

Official sources

  1. Issues
  2. ndmitchell/hlint on GitHub
  3. README
  4. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/ndmitchell-hlint.svg)](https://hysenlabs.com/projects/ndmitchell-hlint)

Was this useful?