# lintr: static analysis for R, with linters named after the thing they catch

> lintr is the R package that checks code for style and possible semantic problems, and its output format is the interesting part, because every message carries the name of the linter that produced it. It suits an R team that wants a style enforced in CI without giving up the flexibility of per-project configuration, and it complements styler rather than replacing it.

**r-lib/lintr** — Static Code Analysis for R

- Repository: https://github.com/r-lib/lintr
- Website: https://lintr.r-lib.org
- Stars: 1,300 · Forks: 203
- Language: R
- License: NOASSERTION
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/r-lib-lintr

## The output names the linter, and that is the design

lintr provides static code analysis for R. It checks for adherence to a given style, identifying syntax errors and possible semantic issues, then reports them to you so you can take action.

The example in the documentation is a deliberately bad file, and the output is what makes lintr worth reading about. Given a file that assigns with `=`, puts the brace on the wrong side, indents with four spaces and returns `T` explicitly, running `lintr::lint()` on it produces a list of messages, each one naming the linter that fired:

```
bad.R:1:9: style: [assignment_linter] Use one of <-, <<- for assignment, not =.
my_func = function(x){
        ^
bad.R:1:22: style: [brace_linter] There should be a space before an opening curly brace.
my_func = function(x){
                     ^
```

The format is file, line, column, category, linter name, message, then the offending source line with a caret or tilde under the exact position. And the tilde is not decoration, because in the indentation messages the marker is a range rather than a single character, so `~~~^` under eight spaces points at the block rather than at one space.

Putting the linter name in the message is the design decision that makes the tool configurable rather than merely noisy. Once every message names its rule, a project can turn individual rules off, and a user reading a message knows exactly which mechanism produced it and can look it up. A linter that reported only a message would give you a list of complaints; a linter that reports the rule gives you a list of options.

The rules visible in the example cover a good spread of R specifics. `assignment_linter` catches `=` used for assignment. `indentation_linter` knows the expected spacing. `spaces_left_parentheses_linter` requires a space before a parenthesis except in a function call. `paren_body_linter` wants a space between a right parenthesis and a body expression. `brace_linter` covers both the space before an opening curly brace and the `else` on the same line as the previous closing brace. `return_linter` wants implicit return. `T_and_F_symbol_linter` catches `T` and `F` used as values, which is a genuine R trap because they are assignable variables rather than constants. And `trailing_blank_lines_linter` catches the blank lines at the end of a file.

## What counts as a possible semantic issue in R

The description says lintr identifies syntax errors and possible semantic issues, and the second category is the more interesting one for an R codebase.

Two of the linters in the example are pure style, and two are closer to catching a mistake. `T_and_F_symbol_linter` reports that you should use TRUE instead of the symbol T. That reads like a style rule and is not, because in R `T` and `F` are ordinary assignable variables rather than reserved words, which means `T <- FALSE` somewhere in your code or in a loaded package changes what `T` means everywhere downstream. The linter is pointing at a genuine class of bug rather than a formatting preference.

`return_linter` reports that implicit return behaviour should be used and that explicit `return()` is not needed. That is a style rule with a design opinion behind it, and the opinion is that an R function's last expression is its result, so the explicit return is noise. It is the kind of rule where a team either adopts it or turns it off, and the fact that it is named makes that conversation specific.

`paren_body_linter` and `brace_linter` are the same category of purely visual rules, and the interesting thing about them is how narrow they are. `brace_linter` has at least two distinct behaviours in the example output, requiring a space before an opening curly brace and requiring `else` to come on the same line as the previous closing brace. A linter that can express more than one rule about the same construct, and reports which one it is through the message, is more useful than one that reports a generic brace complaint.

The message format also tells you the severity category, which is `style` in every case in the example. A static analyser that can say style, and that has other categories available, is telling you that what it found is not a bug. That calibration matters, because the alternative, a tool that cries wolf, gets disabled.

And the `endOfLine` style of marker, the tilde spanning a range of characters, is what makes an indentation complaint actionable. A message pointing at one space tells you there is a problem. A message with a tilde under the whole block tells you which block, which is the difference between fixing it and hunting for it.

## Lint and style are separate jobs

One line in the documentation settles a question that comes up constantly, and it is worth repeating here because it is the reason lintr and styler are not rivals.

lintr is complementary to the styler package, which automatically restyles code, eliminating some of the problems that lintr can detect.

So lintr reports, styler fixes. That division matters because the two have different failure modes. A formatter has to make a decision about every construct it encounters, and it will make one whether or not you wanted it to. A linter only has to point at the constructs it was asked to look for, and it can be selective.

The practical consequence is that a project can adopt lintr first, with the default rule set, and see what it says about the existing codebase without changing a single character. Then you decide which rules you actually want, and only then bring in a formatter for the mechanical part. Doing it the other way round, running a formatter over an unfamiliar codebase, produces a diff nobody can review.

And the linters that overlap are the ones styler can eliminate. If styler has already normalised the indentation and the spacing, then `indentation_linter` and `spaces_left_parentheses_linter` have nothing left to say, and the two tools coexist without fighting.

The other thing the linter names give you is a way to talk about the rules. When a team disagrees about a rule, the argument can be about `brace_linter` rather than about whitespace in general, and the configuration file becomes the record of the decision.

## Configurable rule sets, from tidyverse to everything

The configuration surface is where a linter becomes usable in a real project.

The first step is creating a configuration file, and the entry point takes a type:

```R
lintr::use_lintr(type = "tidyverse")
```

The tidyverse set is the default, and the documentation shows how to list what is in each set. `names(lintr::linters_with_defaults())` gives the default linters, and `names(lintr::all_linters())` gives the full set. The difference between those two lists is the difference between what a project gets by default and what it could opt into.

The linters themselves are named, so a configuration file reads as a list of rules, and the diagnostic output tells you which rule fired. The two together mean you can enable a rule because you saw it catch a real problem in your own code, which is a much better way to build a rule set than adopting a list wholesale.

Once the configuration exists, running it is two functions, and the distinction between them is the distinction between a project and a package:

```R
# in a project:
lintr::lint_dir()

# in a package:
lintr::lint_package()
```

`lint_dir` walks a directory of scripts, which is what you want for an analysis project or a Shiny app. `lint_package` knows the conventions of an R package, where the R directory, the tests directory and the vignettes each have their own expectations, and where a stray file in the package root is itself a finding.

Without further configuration, this will run the default linters, and the documentation points at the `lintr` vignette for how to modify those defaults. The `lintr` package site at lintr.r-lib.org is the reference for the full default set.

The configuration file itself lives in the project, usually as `.lintr.R`, and the repository has one at the top level for its own code, which is a nice piece of dogfooding for a linter.

## Continuous integration and editors

The route into CI is short, because the project deliberately does not require you to hand-write a workflow.

The usethis package provides helper functions to generate lint workflows for GitHub Actions:

```R
# in a project:
usethis::use_github_action("lint-project")

# in a package:
usethis::use_github_action("lint")
```

So the setup is one call and a commit of the generated workflow, and the two variants exist for the same reason as the two lint functions, project and package are linted differently.

The documentation is also clear that the action is not the only option. You can also run lintr during continuous integration or within your IDE or text editor, with the `continuous-integration` and `editors` vignettes for details.

That last point is the one that determines whether a linter gets used. A linter that only runs in CI is a linter that fails the build after the work is done, and the fix cycle is push, fail, fix, push. A linter wired into the editor gives you the same message at the moment you write the line, and then CI becomes a safety net rather than the place you find out.

The project metadata shows how the package itself is set up. There is a `DESCRIPTION` and a `NAMESPACE` for the package, a `man/` directory for the documentation, `vignettes/` for the long form guides, `inst/` for installed files, `tests/` for the test suite, `revdep/` for reverse dependency checks, `pkgdown/` for the site build and a `paper/` directory for the JOSS paper, which the README links to with its DOI.

The badges tell the rest. There is an R build status badge for R-CMD-check, which is the standard R package check, a codecov badge, a CRAN status badge, a lifecycle badge reading stable, and the JOSS DOI badge. The stable lifecycle badge is worth noting, because it is a claim about the maintenance policy of the project rather than about the code, and a linter that is going to sit in your CI for years is one whose lifecycle you want to know about.

## Conclusion

lintr fits an R team that has agreed on a style and wants it checked automatically, because the linters are individually selectable and the configuration is a file in the repository, so the rules travel with the code. Skip it if you want your code reformatted rather than reported, because lintr points at problems and the styler package is the one that fixes them. Before you enforce it in CI, decide which linter set you want, because the default set is tidyverse and the full set includes linters that many projects will not want, and note the licence, which the repository classifies as a custom one that GitHub cannot categorise rather than one of the standard licences. The last push was on 2026-10-15 and the current release is v3.4.0 from 2026-07-16.

## FAQ

### What does lintr do for R code?

It provides static code analysis, checking for adherence to a given style and identifying syntax errors and possible semantic issues, then reporting them so you can take action.

### How is lintr different from the styler package?

They are complementary rather than competing. lintr points at problems, while styler automatically restyles code and eliminates some of the problems lintr can detect, so the two coexist without fighting.

### How do I configure which linters lintr runs?

Create a configuration file with `lintr::use_lintr(type = "tidyverse")`, which sets the default tidyverse set, and list what is included with `names(lintr::linters_with_defaults())` for the defaults or `names(lintr::all_linters())` for the full set. Every linter is named in the diagnostic output.

### How do I run lintr on a project versus a package?

Use `lintr::lint_dir()` in a project and `lintr::lint_package()` in a package, because the package form knows the conventions of the R, tests and vignettes directories. For CI, `usethis::use_github_action("lint-project")` or `usethis::use_github_action("lint")` generates the workflow.

### What is the current version of lintr?

v3.4.0, released on 2026-07-16, with the last push on 2026-10-15. The package is marked with a stable lifecycle badge and is installed from CRAN with `install.packages("lintr")`.

## Sources

- [Issues](https://github.com/r-lib/lintr/issues)
- [Project website](https://lintr.r-lib.org)
- [README](https://github.com/r-lib/lintr/blob/main/README.md)
- [Releases](https://github.com/r-lib/lintr/releases)
- [r-lib/lintr on GitHub](https://github.com/r-lib/lintr)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/r-lib-lintr
