Today’s web linters are great.
ESLint is robust and has a huge ecosystem of plugins and configs.
Newer native speed linters such as Biome, deno lint
, and Oxlint are fast and easy to set up with their own burgeoning ecosystems.
I’ve been working on TypeScript linting for almost a decade. I started contributing to TSLint community projects in 2016 and am now a number of the typescript-eslint and ESLint teams. Based on those years of various linters, I’ve collected a large set of design ideas that I want to try.
I see a linter that is fast by default, powerful upon request, and straightforward to configure. This post is how I would craft the ways developers would interact with it.
💡 This post is the second in a series:
- Part 1: Architecture
- 👉 Part 2: Developer Experience
- Part 3: Ecosystem (coming soon)
- Part 4: Summary (coming soon)
Developer Experience is Crucial
The developer experience of a linter an incredibly important part of its usability. Especially when your tool tells developers they’re “wrong” (which is how many interpret linters, sadly). Linters have have earned that bad reputation for a few reasons:
- Any overly aggressive lint rule makes the act of linting seem unnecessarily painful
- Linters often require extra per-project configuration
- Many older lint rules used to be overly prescriptive
- …did I mention developers hate being told they’re wrong?
My goals with rethinking a linter’s developer experience are:
- Approachability: features should be as straightforward and difficult-to-mess-up as possible
- Clarity: everything should be as easy as possible to understand, even if you’re not an expert
- Minimalism: removing extraneous features and deduplicating tooling configuration
- Type safety: making as much as possible well-typed, both for plugin authors and end-users
The more we make a linter accessible and friendly, the more likely developers are to embrace it.
Only Errors
All mainstream web linters today allow configuring rules as errors or warnings. Errors are generally visualized with red squigglies and fail builds; warnings are visualized with yellow squigglies and don’t fail builds.
Warnings are intended to be transient indicators during migrations or when rules aren’t certain about issues 1. In practice, I think that delineation is not worthwhile:
- Using the same red color and terminology for lint errors and type-checking errors is confusing.
I personally use VS Code’s
eslint.rules.customizations
to visualize lint errors with yellow squigglies, so as to not conflict with TypeScript’s red squigglies. - Warnings tend to live forever in codebases, which trains developers to ignore lint reports.
- If a problem can’t be determined with certainty, it either should be suppressed using an inline config comment with an explanation, or not turned into a lint rule at all!
I think warnings are a bad fit for the migration use case. Tools like eslint-nibble and ESLint’s new Bulk Suppressions can provide a more comprehensive experience.
If I wrote a linter, I would have it so rules can only be turned off or on. All lint rule reports would be errors and visualized with yellow squigglies in errors. Gradual onboardings of new rules or rule options would be a separately managed feature.
Comprehensive Rule Reports
Rule reports are the entry point for most developers who experience a linter. The little red or yellow squiggly in an editor that shows rule text on hover is how developers learn about the report.
Linters today generally populate hover text with:
- ID/name, linked to the rule’s online documentation
- Text description of the rule report
If that message includes too little text, people won’t understand the report. Too much text, and they won’t read any of it. Many rules’ reports have too much conceptual complexity or too many important nuances to fit in a brief hover message.
If I wrote a linter, rule report bodies would include something like:
- Primary: a single sentence explaining what’s wrong
- Secondary: additional sentences explaining the problem in more detail
- Suggestions: recommendations for how to fix the problem
- Shortcut URL to the rule’s documentation page
Rich structured rule reports would enable rich displays in reporters that surface those errors to users.
Comprehensive Editor Reports (In Theory)
In theory, editor extensions could then visualize these nicely 2. In practice, VS Code is unfortunately still waiting on formatted diagnostic text support 3 4.
I’ve seen proof-of-concepts in extensions such as pretty-ts-errors — but they require unseemly workarounds.
Ugh.
Comprehensive CLI Reports
Terminals these days can do a lot more more than just print text in preset colors. They can display hex codes, format links as plain text, and even re-render quickly for interactive displays.
The Biome 5, TSSLint 6, and Vitest 7 CLIs do a great job of making rule reports approachable and clear.
Bonus shoutouts to Orta’s tsc
explorations 8 and userland Rust error reporting crates 9 10 11.
I particularly like how some CLIs display only the first report per file and/or per rule, with an opt-in option to show all.
If I wrote a linter, it would include some kind of interactive rich reporter display by default. It would at least:
- Trim out unnecessary details, such as parts of file paths above the project root
- Default to a “focused” view that only shows a few reports at a time, leaving more space for…
- Friendly primary + secondary + suggestions text for each spotlighted report, using …
- Rich colors to emphasize which parts of reports are most important
Here’s a very rough starting mockup:
The linter’s reporting would have to fall back to more primitive colors, or no colors at all, depending on what the running terminal supports. And maybe the Details and Focused view settings would persist per-package or per-system? There’s a lot of room for exploration here.
Comprehensive Rule Documentation
The next step for many developers who receive a lint report is the rule’s documentation page. The typescript-eslint site includes a pretty rich display for rules, similar to many other lint sites:
- High-level ID/name and description
- High-level metadata, such as which config(s) include the rule and whether it requires type information
- Longer description of the rule
- Examples of configuring the rule, as well as its options if they exist
- When not to use the rule
- Links to the rule’s source code, test code, and any related other documentation
Each of those sections are must-haves for any lint rule — especially one in an open source linter or popular plugin.
If I wrote a linter, I would require all those sections be filled out in every core rule’s documentation. I would additionally attempt to make sure any popular plugin website templates include the sections and fill them out too.
Standardized Rule Metadata
Users like naming and stylistic standardization. Without consistent patterns, it becomes much more difficult to remember how things work consistently. Rule names, option names, option value defaults, and other strings users need to think of when using a linter are easier to remember when they’re consistent.
Unfortunately, no mainstream linter today is consistent with its choices — even internally. Community plugins often make very different and inconsistent choices as well. This lack of standardization is painful for users.
If I wrote a linter, it would standardize how rules are named and structured. The next few sections will explain the relevant questions I would standardize answers to.
Standardized Rule Categories
How do we even categorize rules? I think developers have generally settled on three categories of lint rules:
- Formatting: Rules that don’t change the AST
- Stylistic: Rules that change the AST but don’t change code logic
- Logical: Rules that change code logic
Those categories are useful for surfacing to developers why a rule would -or wouldn’t- be useful. Many developers also treat reports differently based on which category they fall into.
If I wrote a linter, I would have all core rules categorized into one of those three categories. Plugin rules would be encouraged -but not required- to use them as well.
This would allow developers to react to the metadata in their own tooling.
For example, developers might want to use an equivalent of eslint.rules.customizations
to downgrade stylistic rules to blue info squigglies instead of yellow warnings:
"linter.rules.customizations": [
{ "category": "stylistic", "severity": "info" }
]
Standardized Rule Messages
The text of rule messages is pretty inconsistent across rules, even within the same linter or plugin. Older rules tend to have assertive and curt messages, such as:
no-console
:Unexpected console statement.
no-empty
:Empty block statement.
I’ve found that users don’t generally react well to curt messages. They can feel like the linter saying something is absolutely wrong, even though most lint rules are sometimes wrong. There’s a reason why many linters put “When Not To Use” sections in their rule documentation pages.
Newer or more recently updated rule messages tend to be more descriptive and speak to the actual problems the rules detect, such as:
no-loss-of-precision
:This number literal will lose precision at runtime.
no-useless-assignment
:This assigned value is not used in subsequent statements.
If I wrote a linter, all core rule messages would have consistent phrasing. They would never be dogmatic or overly prescriptive about what the user “should” do. They’d instead just speak to the detected potential problem in code.
Standardized Rule Names
Rule names have to make several choices:
- Abbreviation:
- Abbreviated:
no-cond-assign
,no-const-assign
/ noConstAssign - Unabbreviated:
no-constant-binary-expression
/ noConstantBinaryExpression
- Abbreviated:
- Prefixes:
- Definitive:
@typescript-eslint/ban-ts-comment
,no-await-in-loop
- Loose:
@typescript-eslint/restrict-plus-operands
,prefer-const
- No prefix:
array-callback-return
,constructor-super
- Definitive:
- Singularity:
- Singular:
default-case-last
,for-direction
- Plural:
no-magic-numbers
,require-atomic-updates
- Singular:
Inconsistent answers to each of those choices lead to user confusion:
- Abbreviation:
- Memorizing many abbreviations is annoying at best and confusing at worst
- Some words abbreviate to different meanings, such as “constant” and “const”
- Prefixes:
- Alphabetical sorting of rules places them similarly prefixed rules together, weirdly
- Many rules have dropped their prefix after adding an option to invert behavior 12
- Singularity: the least troublesome, but I still find the inconsistency painful
If I wrote a linter, core rule names would make one choice for each of those naming options:
- Abbreviation: no abbreviation, so users won’t have to memorize dozens of abbreviations
- Singularity: always plural, as that’s how one would describe what rules target
- Prefixes: no prefixes; the rules would be named for the behavior or syntax they target
For example, I would rework the following rule names roughly like:
Current Rule Name | Reworked Rule Name |
---|---|
array-callback-return | arrayCallbackReturns |
ban-ts-comment | typescriptCommentDirectives |
constructor-super | constructorSuper |
default-case-last | defaultCaseLast |
for-direction | forLoopDirections |
no-await-in-loop | loopAwaits |
no-cond-assign | conditionalAssignments |
no-const-assign | constAssignments |
no-constant-binary-expression | constantBinaryExpressions |
no-floating-promises | floatingPromises |
no-magic-numbers | magicNumbers |
prefer-const | constVariables |
require-atomic-updates | atomicUpdates |
restrict-plus-operands | plusOperands |
I would also include rule name as a required field in its metadata. That way, downstream tooling such as documentation generators and unit testers can rely on it.
Standardized Rule Options
Rule options have to make an even broader, deeper set of choices:
- Format:
- Array:
array-type
,no-restricted-component-names
- Object with keys for option names: …almost all other rules
- Array:
- Name:
- Plurality:
- Singular:
no-implicit-coercion
>boolean
- Plural:
no-floating-promises
>checkThenables
- Singular:
- Prefix:
- Turning off checks:
allow*
:getter-return
>allowImplicit
ignore*
:no-trailing-spaces
>ignoreComments
skip*
:no-trailing-spaces
>skipBlankLines
- Turning on checks:
check*
:prevent-abbreviations
>checkProperties
enforce*
:accessor-pairs
>enforceForClassMembers
include*
:no-duplicate-imports
>includeExports
require*
:arrow-parens
>requireForBlockBody
- Turning off checks:
- Plurality:
- Value defaults:
- Boolean values:
- Off-by-default:
no-floating-promises
>ignoreIIFE
- On-by-default:
no-floating-promises
>ignoreVoid
- Off-by-default:
- Multi-values:
- Empty by default:
no-floating-promises
>allowForKnownSafeCalls
- Starting set:
restrict-template-expressions
>allow
- Empty by default:
- Boolean values:
What a list!
If I wrote a linter, core rule options would make the same choice consistently:
- Format: an object with keys for option names
- Name:
- Plurality: always plural, as that’s how one would describe what rule options target
- Prefix:
allow*
for options that add in an array of valuescheck*
for options that turn on a checkignore*
for options that turn off a check
- Value defaults:
- Boolean values: off-by-default, for simpler truthiness concepts
- Multi-values: empty by default, so specifying values doesn’t remove defaults
- If a value is important to include by default, it should be hardcoded on
Here’s how the options mentioned in the choices list would look in my linter:
Current Name | Reworked Name | Current Default | Reworked Default |
---|---|---|---|
(array-type ) | style | 'array' | (same) |
(no-restricted-component-names ) | names | [] | (same) |
allow | (same) | [{ ... }] | [] |
allowForKnownSafeCalls | (same) | [] | (same) |
allowImplicit | ignoreImplicit | false | (same) |
boolean | booleans | true | false |
checkProperties | (same) | false | (same) |
checkThenables | (same) | false | (same) |
enforceForClassMembers | ignoreClassMembers | true | false |
ignoreComments | (same) | false | (same) |
ignoreIIFE | (same) | false | (same) |
ignoreVoid | (same) | true | false |
includeExports | checkExports | false | (same) |
requireForBlockBody | checkBlockBodies | false | (same) |
skipBlankLines | ignoreBlankLines | false | (same) |
Oh, and rule options would be sorted alphabetically in their docs pages. I want to be able to O(log(N)) scan a docs page for a rule option. Not O(1) search through some arbitrary order.
Standardized Specifiers
Some rules allow specifying types or values in their options.
For example, @typescript-eslint/no-restricted-types
allows specifying types in its types
option:
"@typescript-eslint/no-restricted-types": [
"error",
{
"types": {
"SomeType": "Don't use SomeType because it is unsafe",
},
},
]
The problem with using plain strings to target names is that they are ambiguous.
In the example, any type named SomeType
would be restricted, even if it is a different type than the one the user intended.
Over in typescript-eslint, we developed a TypeOrValueSpecifier format for specifying types or values in options.
It allows users to specify not just the name but also the source -global, package, etc.- of a type or value.
Here’s how that would look for specifying SomeType
from a specific file:
"@typescript-eslint/no-restricted-types": [
"error",
{
"types": [{
"message": "Don't use SomeType because it is unsafe",
"type": {
"from": "file",
"name": "SomeType",
"path": "./src/legacy-types.ts",
},
}],
},
]
If I wrote a linter, core rules would only ever use the TypeOrValueSpecifier format to specify types or values. Plugins would be strongly encouraged to use the format instead of ambiguous strings.
Typed Rules
My biggest gripe with linter configuration systems today is that rule options are not type-checked. They’re only validated at runtime. Mainstream linters today have you specify rules as properties an object, where their string key is their plugin name and rule name, and their value is their severity and any options:
{
"my-plugin/some-rule": ["error", {
setting: "...",
}],
}
Those string keys have no associated types in config files.
Linters themselves can validate rule options, such as ESLint’s options schemas, but those don’t translate to TypeScript types.
You don’t get editor intellisense while authoring; instead, you have to use @eslint/config-inspector
or run your config to know whether you’ve mistyped the name of a rule or an option.
I’d love to make a standard plugin creator function that plugin authors are encouraged -even required- to use. It could take in a set of rules and return some kind of well-typed function.
If I wrote a linter, it would allow Standard Schema descriptions for rule options, which would allow TypeScript-friendly schema validations library like Zod:
import { createRule, createPlugin } from "@joshuakgoldberg/linter";
import { z } from "zod";
const someRule = createRule({
options: {
option: z.string(),
},
});
export const myPlugin = createPlugin({
name: "My Plugin",
rules: [someRule],
});
…and in usage could look something like:
// if-i-wrote-a-linter.config.ts (partial)
({
rules: [
myPlugin.rules({
"some-rule": {
option: "...",
},
}),
],
});
Under that kind of system, users would receive intellisense as they type plugin rules, and all those settings could be type checked. Doing so would even coincidentally solve the issue of plugin namespacing and rule config duplication. Config values would be verified at runtime by the schema validation library.
Typed Plugin Settings
An even less type-safe part of many current linters’ config systems is the shared settings
object.
You can put whatever you want in there, and any plugin may read from it.
In theory, cross-plugin shared settings can be used for related plugins, while plugin-specific settings are by convention namespaced under their name. In practice, I don’t think I’ve ever used a shared setting across plugins.
If I wrote a linter, I would have plugins define their own settings and settings types. They would need to use a Standard Schema validation library too. It could look something like:
import { createPlugin } from "@joshuakgoldberg/linter";
import { z } from "zod";
export const myPlugin = createPlugin({
name: "My Plugin",
rules: [
// ...
],
settings: {
mySetting: z.string(),
},
});
…and in usage could look something like:
// if-i-wrote-a-linter.config.ts (partial)
import { myPlugin } from "@joshuakgoldberg/my-plugin";
myPlugin.settings({
mySetting: "...",
});
As with rules, allowing plugins to define their own settings types would help with the config authoring experience. It would also newly allow shared settings to be validated by both type-checking and the core linter. Doing so means plugins can be more confident in defining settings and changing them over time as needed.
Typed Configuration Files
Linters today fall into two classifications of linter configs:
- Direct JSON (
biome.json
, Deno, Oxlint) - Nuanced JS (ESLint:
.eslintrc.js
(deprecated),eslint.config.js
)
Direct JSON is a nice and straightforward “walled garden” that shines in small projects. But I don’t think it scales well. The user experience of typing JSON files isn’t great if you’re not using a custom editor extension to get JSON intellisense. Plus, because they can’t use native ESM imports, the linters have to implement their own ad hoc module system.
Nuanced JS configurations from ESLint, on the other hand, are “just JavaScript” and so utilize native module importing for plugins, global variables, and shared configurations.
That’s great for understandability and simplifying the plugin loading model.
ESLint’s flat config is a huge step forward from the confusing overrides
model of ESLint’s legacy configs.
I think ESLint’s flat config has learned the hard way:
- With “just” JavaScript objects, there’s no way to lint only the user’s own config entries 13
- Directory relativity is confusing when nested configs
import
from a higher-up config 14 - Global
ignores
needs a separate function or key from localignores
15 16 - When plugin configs set file globs and other settings, it’s unclear where settings come from
If I wrote a linter, it would have a config system that wholly leans into well-typed functions and objects. Each line of the config file would set exactly one thing and make it clear what is being set — even if you don’t understand the nuances of the config system.
Here’s a rough sketch of a starting TypeScript setup, compared to the closest ESLint equivalent:
// if-i-wrote-a-linter.config.ts
import { defineConfig, ts } from "@joshuakgoldberg/linter";
export default defineConfig({
use: [
{
glob: "**/*.ts",
rules: [ts.configs.logical, ts.configs.stylistic],
},
],
});
That sketch is different from the current ESLint flat config in a few ways:
- Most settings are moved into an explicit
use
array, each withglob
andrules
- The traditional
extends
is merged intorules
with plugins registering themselves automatically, rather than a separateplugins
object rules
is a array of type-safe references, rather than a non-type-safe key-value object
Here’s a rough sketch that configures rules, ignores some files, and adds a plugin with settings:
// if-i-wrote-a-linter.config.ts
import { defineConfig, ts } from "@joshuakgoldberg/linter";
import { example } from "@joshuakgoldberg/plugin-example";
export default defineConfig({
ignore: ["lib/"],
use: [
{
exclude: ["**/*.generated.ts"],
glob: "**/*.ts",
rules: [
ts.configs.logical,
ts.configs.stylistic,
ts.rules({
ruleA: false,
ruleB: true,
ruleC: {
someRuleOption: true,
},
}),
example.configs.recommended,
example.rules({
ruleD: true,
}),
example.settings({
mySetting: "...",
}),
],
},
],
});
That sketch shows a few more differences from the current ESLint flat config system:
defineConfig()
takes inignore
as root-level setting, rather than inline with other settingsexclude
insideuse
objects is intentionally a different key fromignore
for clarity- Note: I also want the config system to ignore files listed in your
.gitignore
by default, in addition to ESLint’s defaults of.git/
andnode_modules/
.
- Settings are specified inline in
use
through their plugin, rather than as a separate catch-allsettings
object
I previously mentioned that I would want to support common languages in core. Here’s how the config system would look for a repository that enables logical and stylistic rules on all those languages:
// if-i-wrote-a-linter.config.ts
import { defineConfig, md, json, ts, yml } from "@joshuakgoldberg/linter";
export default defineConfig({
use: [
{
glob: "**/*.ts",
rules: [ts.configs.logical, ts.configs.stylistic],
},
{
glob: "**/*.json",
rules: [json.configs.recommended],
},
{
glob: "**/*.md",
rules: [md.configs.recommended],
},
{
glob: "**/*.yml",
rules: [yml.configs.recommended],
},
],
});
I don’t love that my linter sketch has so many more lines than the ESLint equivalent. They have fewer overall characters, and I think they’re worth it for clarity, but it’s a tradeoff.
Another downside of this proposed approach is that it requires the user to know and manage their file extensions themselves.
Knowing that TypeScript files can also be *.cts
, *.mts
, *.tsx
, etc. is a bit of a pain.
Maybe plugins such as the core linter plugins could provide globs
constants?
import { defineConfig, md, json, ts, yml } from "@joshuakgoldberg/linter";
export default defineConfig({
use: [
{
glob: ts.globs.all,
rules: [ts.configs.logical, ts.configs.stylistic],
},
],
});
…and what about rules that should be different across different files?
Maybe some TypeScript rules skip or apply different logic to .d.ts
files 17?
I suspect having the rules react to the extension of the file being linted would be enough. A rule can skip linting a file based on its file extension, or really any other information it has on the file. But we’ll see.
Native Workspace Support
Larger web repositories these days are often structured as “monorepos”, often using the “workspace” feature of their package manager and other tools. Workspaces are particularly useful when different packages in the same repository use some very different frameworks.
If I wrote a linter, I would want native support for workspaces. I think a root-level lint config file should be able to define common rules and settings for all workspaces. It should also be able to define where the linter should look for workspace-specific config files. Then, each workspace could define its own config file that extends the root config and overrides any settings it wants.
Here’s a rough sketch of what the root monorepo config could like with a config system inspired by Vitest workspaces:
// if-i-wrote-a-linter.config.ts
import { defineConfig, ts } from "@joshuakgoldberg/linter";
export default defineConfig({
use: [
{
glob: "**/*.ts",
rules: [ts.configs.logical, ts.configs.stylistic],
},
],
workspaces: ["packages/*"],
});
Each packages/*
directory could have its own config that explicitly inherits from the root config:
// packages/example/if-i-wrote-a-linter.config.ts
import { defineConfig } from "@joshuakgoldberg/linter";
import { example } from "@joshuakgoldberg/plugin-example";
export default defineConfig({
from: "../..",
use: [
{
exclude: ["**/*.generated.ts"],
glob: "**/*.ts",
rules: [
example.configs.recommended,
example.rules({
ruleD: true,
}),
example.settings({
mySetting: "...",
}),
],
},
],
});
Inline Snapshot Unit Tests
I like the design of ESLint’s RuleTester
(and typescript-eslint’s RuleTester
extension).
RuleTester
’s API allows for clear, succinct descriptions of many isolated test cases at once.
I prefer it over the TSLint *.ts.lint
test files that put all test cases into one big file namespace.
Here’s an example of a RuleTester
test for a TypeScript rule:
import { RuleTester } from "eslint";
import rule from "./preferConst.js";
const ruleTester = new RuleTester();
ruleTester.run("prefer-const", rule, {
invalid: [
{
code: `let value = 123;`,
output: `const value = 123;`,
errors: [
{
column: 5,
line: 1,
data: { name: "value" },
messageId: "preferConst",
},
],
},
],
valid: [
`const value = 123;`,
`
let value = 123;
value = 456;
`,
],
});
I don’t love that we have to specify the rule name manually. That’s easily solvable by Standardized Rule Names’s note of making that a required rule property.
I also don’t love that the RuleTester
class is packaged with the linter itself.
Doing so is convenient for plugin developers, but it means the code bloats users’ package installations.
My biggest gripes with RuleTester
are around the errors
array:
- It doesn’t show the full error message, just the data used to create it
- Not managing long messages is nice, but text formatting issues can more easily sneak in
- Specifying location data as 0-4 of
column
,endColumn
,line
, and/orendLine
:- Adding new cases and updating tests for rule changes is cumbersome
- Tests tend to be inconsistent about how much location data they include
What I really want here is the best of three worlds:
- ESLint’s
RuleTester
: clear and succinct inline cases - TSLint’s lint files: quick visual display of report spans and messages
- Jest/Vitest-style inline snapshots 18: being able to auto-update those report spans
If I wrote a linter, it would provide:
- A
RuleTester
equivalent that defines rule reports in an inline string snapshot - A first-party plugin that auto-fixes report snapshots to match the current rule reports
Here’s how that could look in practice:
import { RuleTester } from "@joshuakgoldberg/rule-tester";
import rule from "./preferConst.js";
const ruleTester = new RuleTester();
ruleTester.run(rule, {
invalid: [
{
code: `let value = 123;`,
output: `const value = 123;`,
report: `
let value = 123;
^^^^^
[preferConst] Prefer using 'const' for variables that are never reassigned.
`,
},
],
valid: [
`const value = 123;`,
`
let value = 123;
value = 456;
`,
],
});
(I’d use ~
, not ^
, for squigglies, but right now they break syntax
highlighting in my MDX extension (known bug)…)
report
would only show the reported line, or the first and last lines if a multi-line report.
I’ll talk more in the next post in this series about the first-party plugin and other ecosystem tooling.
Up Next
This post ballooned in size from what I thought it would be. There are a lot of fun ideas in here and I’m not sure how many of them are practically doable. But I think it’d be great to achieve the stated goals for an approachable, clear, and friendly linting user experience.
Next week I’ll post a next entry in this series that focuses on ecosystem work. If these first two posts were of interest to you I think you’ll be interested in that one too!
💡 This post is the second in a series:
- Part 1: Architecture
- 👉 Part 2: Developer Experience
- Part 3: Ecosystem (coming soon)
- Part 4: Summary (coming soon)
Acknowledgements
Thanks in particular to Kirk Waiblinger, fellow typescript-eslint team member, for very helpful advice on the config file format.
Kirk ideated preset glob
values and helped with balancing clarity of language configurations with its verbosity.
Additional thanks to Arend van Beelen, Dimitri Mitropoulos (Michigan TypeScript), and @tonywu6.org for suggesting references of CLI prior art. Dimitri also helped ideate the rich CLI output format and what to show in the different views.
Footnotes
-
eslint/eslint#16696 docs: Add explanation of when to use ‘warn’ severity ↩
-
microsoft/vscode
vscode.d.ts
:DecorationOptions
>hoverMessage
↩ -
microsoft/vscode-eslint#1761 Support for markdown in the rule’s message report ↩
-
johnsoncodehk/tsslint#50 Include screenshot/gif of the CLI somewhere in this repo? ↩
-
Microsoft/TypeScript#49668 Compiler CLI output format update v2 ↩
-
Suggestion: standardize against prefixing rule names with “no-” ↩
-
eslint/eslint#15476 Change Request: report unnecessary config overrides ↩
-
eslint/eslint#18385 Change Request: Make it easier to inherit flat configs from the repo root #18385 ↩
-
StackOverflow: Parsing error: was not found by the project service, but I’ve ignored these files ↩
-
Discord help thread: Eslint not ignoring .js files and throwing Definition for rule … not found error ↩
-
typescript-eslint/typescript-eslint#7941 Base rule extension: no-var configuration for declarations ↩