Oxlint – JavaScript linter written in Rust
oxc-project.github.io
oxc-project.github.io
If you just use linting for checking a bit of stylistic policy, any replacement might be fine. However, linting is much more than that and if you are depending on third party rules or writing your own ([link redacted]), there is no way around ESLint.
Adding logic into linters blurs separation of concerns, adding unnecessary complexity akin to an extra programming language.
Linting in essence should be orthogonal to development — a layer that enhances code quality without being fundamental to the code’s functionality. By overextending linting, we risk creating a maintenance burden and an additional learning curve for developers.
Linting is a great tool and but as with any great hammer it’s easy for lots of things to start to look like nails.
It might be better to have some other tool to do pluggable static analysis, but the fact is that there isn't one. And eschewing project-specific static analysis entirely would be giving up far too much.
I guess that's what I disagree with. Yes, it adds complexity of its own, just like types do. And I still favor solutions that are based on types for most things, but more and more I try to go this route. They are surprisingly easy to write.
Two examples:
- we have an API like dbModel.getValue() that subscribes the current view to any change in an entire database row. We noticed this lead to UI performance issues from components over-rendering. To deprecate, I wrote a rule to transform dbModel.getValue().specificProp to dbModel.getSpecificProp(). We can’t remove the getValue method since there’s times you really do need it, but we can automatically switch new code written to a more performant specific call for many cases.
- We use a lint rule to enforce that API endpoints and queue worker jobs have ownership and monitoring rules specified. We could use the type system to strictly enforce this, but we want to support gradual migration as well as suggest some inferred values based on the identity of the author. Using a lint with ratcheting means newly added cases are enforced but easy to add, and old cases can/will adopt over time.
I want to find the time to write a blog post about this, I think it’s a pretty handy pattern.
https://github.com/oxc-project/oxc/tree/main/crates/oxc_quer...
I'm the Trustfall maintainer, happy to answer questions about the query engine or how oxlint or cargo-semver-checks use it.
I also recently gave a talk at P99 CONF on how cargo-semver-checks used Trustfall's optimizations API to get a 2000x speedup: https://www.youtube.com/watch?v=Fqo8r4bInsk
There are a couple of alternative formatters:
* https://github.com/andrewbaxter/genemichaels * https://github.com/jinxdash/prettier-plugin-rust
Still, all of them are better than clang-format!
[1] https://dprint.dev/plugins/typescript/config/
> Our previous linting setup took 75 minutes to run, so we were fanning it out across 40+ workers in CI. By comparison, oxlint takes around 10 seconds to lint the same codebase on a single worker[…]
So it's in fact 18000 times faster on this embarrassingly parallel problem (but doing less for now).
I'm not sure if Eslint does this either, but indices or some incremental static analysis sounds like it could help the linter minimize rechecks or use previous state.
I'm not sure how well this could scale across (600- 1000?) different lints though. I should look into static analysis a bit more.
If a file has been linted, is unchanged since it was linted, there's literally no need to lint it again. Much like if you only need to process one record, you don't query the whole table to get the record.
But I'm generally working in a (human & financially) resource-constrained environment.
I'm not aware of eslint rules which would complain about some other untouched file if types have changed in ways such that the program still compiles
https://typescript-eslint.io/rules/await-thenable/
https://typescript-eslint.io/rules/no-for-in-array
https://typescript-eslint.io/rules/no-duplicate-type-constit...
ESLint could also cache things fairly trivially:
hash = hash_file_contents()
if previously_seen_hashes.contains(hash)
report_previous_results()
else
run_lint_and_cache_results()
end
maybe that already exists. But that has the same problem.When you've got enough hardware to throw at it, then "just run it on the full code" is the safest.
Probably not, but it's a trick question: if you try to look for exceptions to the rule, you have already wasted so much time that running a linter on all files would be faster.
Every file not touched in any given diff
What if the linter uses more context than a single file, a type-checker for example or even just checking the correct number of arguments (regardless of type) are passed to an imported function - or that that symbol is indeed even callable? Should we only run the linter on the caller's file, or the callee's, when they haven't both changed?
Also, ESLint doesn't do type checking. That's typescripts job, and apparently typescripts runtime isn't an issue.
In particular, the call site itself hasn't changed, as this thread assumes the linter is only run on changed files
What a colossal waste of compute resources. (1)
IME if you’re using TypeScript then ESlint’s real value mostly approaches zero. For pure JS projects it’s useful for finding nullref type bugs.
(1) > Our previous linting setup took 75 minutes to run, so we were fanning it out across 40+ workers in CI
This. Is. Insane.
ESLint primary job is linting. It should be finding 'foot guns' and code style issues. Things that are absolutely valid in the language, but could lead to potential issues. Because of that, it's totally valid that you're not finding as much value in it. It depends on the rules you enable in it, etc. And yeah, it can feel super nitpicky when it's yelling at you for not having a radix in parseInt().
Typescript's 'compile' step or whatever, it doing type checking and making sure your code is valid. If you're using bare JS, your IDE should be doing this job, not eslint.
(but yes, anything more than a few minutes to lint even a large code base is insane.)
Linting has some value, it’s just that in my professional experience its costs outweigh its benefits
Honestly if not exhaustively putting some closures in your useEffect deps list is your idea of a “maintenance nightmare” then maybe you should stay away from real production code bases? There are plenty of hairier mistakes and patterns out there than that.
For example, the no-floating-promise[0] rule catches some easily-made mistakes involving promises in a way that TypeScript doesn't on its own.
Other rules can be used to increase type safety further. There are various rules relating to `any`, like no-unsafe-argument[1], which can be helpful to prevent such types sneaking into your code without realising it; TS has `noImplicitAny`, but it'll still let you run something like `JSON.parse()` and pass the resulting any-typed value around without checking it.
[0] https://typescript-eslint.io/rules/no-floating-promises [1] https://typescript-eslint.io/rules/no-unsafe-argument
Is there a fast linter that checks for this? I find this error easy to make as well, and it usually causes weird runtime behaviour that's hard to track down.
We use eslint for formatting and other legal-but-likely-a-mistake behavior and it does catch bugs.
Requiring the TS AST adds a massive overhead.
- setting up eslint isn't actually that simple
- if you're using typescript you need eslint-typescript too
- there are sets of rules in both eslint and eslint-typescript that conflict with each other, so I have countless rules in my config like this:
'comma-dangle': 'off',
'@typescript-eslint/comma-dangle': ['error', 'always-multiline'],
- then if you're doing React there's another set of JS and TS rules to apply, I still never figured out how to correctly apply airbnb rules- this is a pretty garbage developer experience
- you can quite literally spend hours or days getting a "good" linting/formatting configuration setup, and you often can only use pieces of the configs you wrote for other repos because over time the rules and settings seem to change
- I hope this will eventually support things such as .astro files which is actually a combination of TypeScript and TSX blocks
> At this stage, oxlint is not intended to fully replace ESLint; it serves as an enhancement when ESLint's slowness becomes a bottleneck in your workflow.
I also hope that eventually it does become a full replacement. I like eslint, but holy shit, I cannot bring myself to create a new config from scratch that wrestles all the required extras and the frequently changing dependencies.
Also, wanted to give a sort of shout out to Deno here. Deno comes with a linter/formatter built in that is barely configurable (just double vs single quote, 2 or 4 space indentation, minor things) and it too is very fast and simply "just works".
---
Update: I just gave it a quick try and I am immediately impressed by it. Not only was it incredibly fast like it claims, it appears to already have all of the rules I was complaining about built in.
eslint-plugin-react(jsx-no-useless-fragment): Fragments should contain more than one child.
╭─[src/design/site/preact/MobileNavigationMenu.tsx:18:1]
18 │ return (
19 │ <>
· ──
20 │ <MenuButton isOpen={isOpen} onChange={setIsOpen} />
Finished in 17ms on 90 files with 70 rules using 16 threads.
Found 13 warnings and 0 errors.> Fragments should contain more than one child.
What an annoying rule
I write C# and do "linting" via editorconfig + ReSharper file layout formatting at dev time and via precommit hook with their CLI tool
I'm surely missing something crucial in that ecosystem that editorconfig can't handle...
These linters do checks on the abstract syntax tree, and so they can statically analyze that e.g. you don’t use certain unsafe APIs or do things that might introduce performance issues or bugs.
Pick a config that roughly matches your ideals and just use it. On older projects you’ll have to customize it a bit, on new ones you’ll probably just adapt to it.
I’ve been using eslint-config-xo-typescript for several years, plus some plugins with their “recommended” presets.
But yeah, I'm also looking forward to the day when I only need a single speedy tool for python linting, type-checking and formatting.
Comparison to mypy: https://github.com/microsoft/pyright/blob/main/docs/mypy-com...
1) it works better as an lsp/vscode plugin, so I don't need to save to get errors popping up. 2) it respects pyproject.toml and doesn't need to liter my root dir with another dot file. 3) as an intangible, its errors just feel better.
I am a junior developer writing a side project in Golang which probably no one on the internet sees, and there are 3 ways to install it already.
1. Compiled executable
2. Language package manager (go install)
3. Docker image (super trivial to create a distroless docker image).
Same can be said of many python tools (pip, pipx, docker image, homebrew or whatever)
It's not that we are doing more work. It's just that we have more tools these days. :)
But it's giving the impression that these projects perhaps could be better off collaborating instead of each of them aiming to eat the world on their own?
EDIT: I'm not saying it's wrong to write competing tools, it's open source anyway, so please do whatever you like with your time and have fun. But it looks like out of these 3 projects, 1 has a startup behind it, and 1 receives funding from bigger company. I assume that money will stop coming in if these tools don't gain adoption fast enough, and nobody would want to see that happen, especially with so much potential here.
[0] https://bun.sh/
Just a couple days ago, the curl maintainer published a blog post about why he wouldn't rewrite curl in Rust and a big part of the reason was that he and the other maintainers weren't good at it and weren't the right people to lead a project that used it--he said that he encouraged other people to start their own project in Rust. But then when people follow that advise, they're chided for not contributing to the more established project! To be clear, I'm not a "just rewrite it in Rust" guy, but I think people underestimate the difficulty and frustration involved in petitioning an established project to make the reforms necessary for significant improvements.
Maybe this is a better design than the other projects. Maybe people cannot get along and so they are forced to fork. There are many other good reasons to not contribute to an existing project. However we should always look at skepticism on such claims: it is easy to start you own project and you are in control so the amount of work you get done is higher. However working together, while it makes everyone slower normally results in many more features and higher quality code over the long term.
So please when you have an itch technology can solve look to see if you can contribute to someone else's project first. It won't be as fun, but the world and you will be better for it.
Now instead of a new JS framework daily, it's going to be new reimplementation of an existing tool daily. For a while.
So when you say “compiled app” you might be referring instead to C or C++ apps, which don’t generally have as simple and common a distribution model. Rust is entirely different, and incorporated a lot of design decisions about how to package software from npm and other languages.
Second, this was in response to an npm is simpler comment; npm and cargo are absolutely the same category of tool.
1) what’s the easiest way to give people access to a tool I just wrote, `cargo publish`
2) what’s the easiest way for someone to use it, as few steps as possible, right now it’s `install rust` && `cargo install`.
Once I get to three or more steps on 2 I tend to turn to just or make depending on the context.
Use github actions or other setup for other backends.
(this is language agnostic and a reasonable thing to learn as a dev).
Or if you must live in the cargo command, go nuts with cargo-release.
local compilation may work for you and other individuals, but "just cargo install" can immediately run into issues if you're trying to deploy something to things that aren't dev workstations
> npm and cargo are absolutely the same category of tool
as a dev tool? absolutely. as a production distribution solution? definitely not
If you’re talking about distributing Rust projects, sure it’s fine. Generally though, if you’re orchestrating a bunch of other things outside the rust software itself, I’d turn to just.
npm is still mainly used in JavaScript and Typescript scenarios, so I think you’re kinda splitting hairs if you’re suggesting it’s a general purpose tool.
OOTH, Precompiled Rust binaries have the libc version issue only if you're distributing binaries to unknown/all distribtuions, but that's pretty trivially solved by just compiling using an old glibc (or MUSL). Whereas `cargo install' (and targetting specific distributions) does the actual compiling and uses the current glibc so it's not an issue.
Why it matters: in GP’s case it sounds like compiling is the problem, so migrating to using SWC as the compiler but keeping TSC as the checker (noEmit flag) in a lint step may ease that pain a bit. Though it might be nicer to migrate both in parallel.
Compilation or not isn't a feature of languages but of language implementations, but, yes, the primary TypeScript implementation is compiled.
> It is a "transpiled" language.
Transpilation is a subset of compilation.
It's not compiled to native machine code for the target system, but that doesn't make it not-compiled.
Just like Typescript compiles the source to Javascript which is then loaded by the V8/Node etc.
And thus programming languages can be only of one type - Compiled.
When I think why Rust was successful at establishing community-accepted standard tooling (clippy, rust-lsp), 2 things come to mind:
- Project developers were always promoting each other's tools, pointing them out in docs or blog posts
- Good tools were being pulled into rust-lang GH org (for visibility) and rustup CLI distribution (for ease of system-wide installation)
Both of these things are not technical challenges, they are rather more "political" (require agreements between parties). In JS ecosystem, what would it take for Oxc to say on their website "we are not writing a formatter, please install Biome" and for Biome homepage to say "we are not writing a linter, please install Oxlint"?
If I remember correctly, OXC was born out of its author's desire to learn Rust and his feeling that Rome Tools/Biome had made complex technological decisions (mainly the use of CST instead of an AST). Rome Tools/Biome chose a CST to bring first-class IDE support: you can format and lint malformed code as you are writing it.
I hope more collaboration between Biome and OXC in the future. However, the inherent difference comes from technological choices.
I'm its author and focus solely on the collaboration picture. I don't generate much press because I only build internal APIs for tooling and language authors, where the projects you've shared all opted to prioritize fulfilling specific real use cases over generalizing their core technology.
Cruel as it is, I think all of them have planted the seeds of their own failure by failing to protect their organization's mission and day-to-day work from being jailed by a set of specific opinions about code style, which cannot possibly be "right" or "wrong" but must instead by argued about forever.
I see the core challenge as shifting all editors and tools to share a common DOM representation and be interoperable in a per-node way, where the current solution is to use siloed and reimplemented tools which interoperate mostly in a per-file way, with each tool parsing the text, doing some work, then emitting text for some other tool to parse...
"The Oxc AST differs slightly from the estree AST by removing ambiguous nodes and introducing distinct types. For example, instead of using a generic estree Identifier, the Oxc AST provides specific types such as BindingIdentifier, IdentifierReference, and IdentifierName."
Already this is getting into matters of style! It is one style, yes, but Javascript's shorthand syntax `({ foo })` already breaks the mental model: the identifier `foo` is technically doing the work of both an IdentifierName and an IdentifierReference. OXC chooses IdentifierReference so any system built on top of it would need to contain additional logic in order to be able to identify all sites in code that are used as identifier names.
1. The need for a 50-100x perf bump is indicative of average projects reaching a level of complexity and abstraction that's statistically likely to be tech debt. This community needs complexity analysis tools (and performant alternative libraries) more than it needs accelerated parsers that sweep the complexity problem under a rug.
2. (more oft cited) The most commonly and deeply understood language in any language community is that language. By extension, any tools written in that language are going to be considerably more accessible for a broader range of would be contributors. Learning new languages is cool but diverging on language choices for core language tooling is a recipe for maintainer burnout.
Metaprogramming and compilers/language analysis tooling is a jump above your run of the mill frontend code or CRUD backends.
Sort of elitist, but IMO devs capable of tackling that complexity level won't be hindered by a different language much.
And Rust is really tame compared say C/C++. Borrow checker is a PITA, but it's also really good at providing guardrails in the manual memory management land, and the build tooling is really good. Don't know enough about Zig but I get the impression that rust guardrails would help developers without C/C++ background contribute safe code.
You could argue Go is an alternative for this use case (and similar languages) but it brings it's own runtime/GC, which complicates things significantly when you're dealing with multi language projects. There's real value in having simple C FFI and minimal dependencies.
Not elitism, just an honest appraisal, though I think flawed as competency isn't linear it's heterogeneous - you'll find the most surprising limitations accompanying the most monumental talent. Language fixation is a common enough one, but even beyond that, the beginner-expert curve on each language shouldn't be underestimated regardless of talent or experience.
In particular when it comes to Javascript there's a tendency to believe the above by virtue of the community being very large & accessible - bringing in a lot of in-expert contributors, especially from the web design field. This isn't fully representative of the whole though: there are significant solid minorities of hard JS experts in most areas.
I've done this a few times for Go. One of the nice things about Go is that this is actually pretty easy. I've written some pretty useful things with this and gotten good mileage out of it. Any competent Go programmer could do this in an afternoon.
I don't really know what the state of JS tooling on this is, but my impression is that it's a lot harder, partly because JS is just so much more complex of a language, even just on the syntax/AST level. And TypeScript is even more complex.
There is also abundant complexity analysis tooling for JS too. When I worked as an architect at a large telco we had this tooling in CI. It revealed some code smells and areas needing refactoring but didn’t really signal anything especially terrible.
Software tooling is more productive than ever and product requirements have grown to use that capacity. It’s definitely not a load of tech debt.
> There is also abundant complexity analysis tooling for JS too.
I would highly appreciate recommendations here; I wonder does your review indicate the projects being analysed had little wrong, or that the tools were not very good at identifying problems.
Javscript is just inherently suboptimal for this.
* The JIT needs to warm up
* AST data structures can be implemented much more efficiently with better control over memory layout
E.g. in ESTree evaluating such a common expression as `node.type` is actually really expensive -- it incurs the costs of a hashmap lookup (more or less) where you'd expect it to be able to be implemented using pointer arithmetic.
You're optimising your execution but there's trade-offs: you need to think about optimising your software development model holistically. There's little point in having the most efficient abandonware.
A JS tool may be technically suboptimal but that's not a problem unless AST size is a bottleneck.
> AST data structures can be implemented much more efficiently with better control over memory layout
I assume you're right but I'm not sure I fully understand why this is the case - can you give examples of how a data structure can be implemented in ways that aren't possible in JS?
I don’t think this is the right way to look at it. The issue is that JavaScript developers have been writing servers, build tools, dev ops tools, etc, in JavaScript because that’s the language they are expert in, but JavaScript was never the right choice of language for those types of programs. The whole industry is caught in a giant case of “If all you have is a hammer…”.
I do web development in JavaScript because JavaScript is the language of the browser. But I write all of my own build and devops tools in Java, including SaSS compiling, bundling, whatever you want. There’s no contest between the Java runtime vs the JavaScript runtime for that kind of work.
I think it’s backwards to see this as a 50-100x performance boost because Rust was used. That same performance increase could be had in a number of languages. The real issue is a 50-100x performance hit was taken at the outset simply by using JavaScript to write tooling.
Edit: just to put it in perspective, a 50-100x speed up in build time means that what would currently take a minute and a half using JS tooling could be accomplished in a second using a fast runtime. A minute and a half of webpack in the blink of an eye.
I don't want to be too hard on such projects; nobody writes perfect code the first time, and stuff happens. But this does in my mind tend to tune down my amazement level for such announcements.
And your last edit is really the important point. That level of performance improvement means that you are virtually certain to move up in the UI latency numbers: https://slhenty.medium.com/ui-response-times-acec744f3157 Unless everything you were doing is already in the highest tier, this kind of move is significant.
I don't mean to be facetious here, but... citation needed.
There are a lot of assumptions about language performance being made throughout comments threads on this page that seem more based on age-old mythology rather than being grounded in reality.
https://youtu.be/sRCgu1ng6Bo?si=SV_Mcinuqh_c-nuX
JavaScript is ~8x slower and Python ~30x slower on average vs Java / Go / C++ that are all quite close.
A funny aside: I always believed that Java is slow because I heard it repeated so many times. I internalized that bit of age-old mythology. But lately as I’ve gotten more focused on performance, I’ve come across a lot of hints in various talks and articles that Java has become one of the go-to languages for high-performance programming (e.g. high frequency trading). So, I hear you about the mythology point.
Well, when I need to batch fix errors in files, yes it can take a while to run eslint. But that almost never happens. I have the plugin and fix errors as I go (which I believe is what most people do), and I never feel performance is an issue in this workflow. I really doubt how (actually) useful this is.
And once in a while that we have to run lint for entire repo (ESLint upgrade for example) we can afford to wait 1 hour ONCE
It's not that you can't wait 1 hour, it's that you don't have to wait. Think of all the wasted cycles that could be put to better use...
Quite hard, especially since type-aware rules from e.g. https://typescript-eslint.io/ mean that changing the type of a variable in file A can break your code in file B, even if file B hasn't changed.
If you lint the entire repo, fix every issue in one try on the first go, and then lint the entire repo to double-check, that's two hours.
But my workflow is usually: lint the repo -> fix one thing -> repeat
IMO oxlint currently only fits the niche “I’m starting a new project and I don’t want to install 100 dependencies and configure eslint”.
Without ts-eslint and unicorn rules this is DOA for me otherwise (but I’m hopeful)
To be that says Rust is more easily accessible as a language than C/C++ and that's great for our environment (speed is green) and our joy in computing (speed is happiness).
Why would a team of talented engineers focus on solving ESLint performance issue? Where is the value in this? If your project is small, ESLint is fast enough. If it's super large like ours (3 millions LOC) then you spend a little time making local and CI linters smarter to run only on changed files. Rewriting in Rust seems cool and novel but now you lost the entire wealth of ESLint plugin ecosystem, you have to keep up with maintaining this new linter which has to be updated very frequently for at least new syntaxes and so on...
We could put this effort into looking into why ESLint is not fast enough and fix the bottlenecks IF we had extra time in our hand...
If it was my team, I would not let them spend time on this. I don't see the value to be honest.
Imagine that your pushes to production can happen an hour faster. At Shopify’s scale.
Do you see the pure economic value?
However, I think trying to find the actual numbers is dumb because there's also the intangibles such as marketing and brand recognition bump by doing this both for the company and individuals involved.
That's not to say all greenfield endeavors should be actioned, but ones with substantial gains like this seem fine given the company is big enough to absorb the initial up front cost of development.
However, people tend to underestimate this sort of thing in general. Even since before programming... we have adages about the importance of sharpening the axe precisely because people have been hacking away with metaphorical and literal dull axes hoping to avoid needing to sharpen them for a long time. Sometimes you just won't be able to convince a business person of the importance of stopping work for a moment to sharpen the axe, because all they see is the work stopping. I don't have a solution to that level of lack of wisdom in a leader. These are the people who save $200 per programmer on computer hardware at the cost of 5 hours of productivity lost... per week. Some battles just come pre-lost.