Adding a lint rule
This page is generated from packages/eslint-plugin-reforged/AGENTS.md by docs:collect. Edit the source, not this page.
Overview
The lint layer of the Guards: type-aware ESLint rules that report Warcraft III scripting pitfalls (desync, crash, leak) in a Map project. The decisions are in spec #50 and ADR 0007; the pitfall ids (D1, C2, ...) come from the catalogue in #15.
The rules classify through the type checker, never by name alone. They read:
- the Typings of
reforged-types: a Native is a function declared in that package; - the Wrappers of
reforged-ts: classes whose chain reaches itsHandlebase; - the plugin's own data files in
data/.
configs.recommended sets no parser. A Map project places it after a configuration that provides type information.
Commands
Run from the repository root:
| Command | What it does |
|---|---|
pnpm --filter eslint-plugin-reforged test | The package's vitest project: the rule fixtures and the export tests. |
pnpm --filter eslint-plugin-reforged typecheck | tsc --noEmit on the sources and on the tests. |
pnpm --filter eslint-plugin-reforged build | Compiles src/ to dist/, the entry ESLint loads. |
pnpm exec eslint packages/eslint-plugin-reforged | The workspace lint on this package. |
pnpm check | The whole workspace; the finish condition. |
pnpm --filter eslint-plugin-reforged measure-cost | The cost over typescript-eslint's type-checked preset (see below). |
The tests run from source. They do not need build.
Cost. Spec #50 expects the plugin to add less than one fifth to a lint run with typescript-eslint's type-checked preset. scripts/measure-cost.mjs lints the docs examples on the fixture project in fresh processes, with and without the plugin, and prints the median overhead (about 10% at the first release, most of it loading the plugin; the rules themselves take about 50 ms). Run it after a change to a classification helper or a new rule that asks the checker more.
Layout
src/index.ts: the entry. Its default export iscreatePlugin().src/plugin.ts:createPluginbuildsmeta,rulesandconfigs.recommendedfrom the registry.src/rules/index.ts: the registry, one line per rule, sorted by name. The docs site'sdocs:collectreads its rules from theimport <name> from "./<rule>.js";lines, the file name being the rule's name: keep that form.src/rules/<rule>.ts: one file per rule. Its default export is aRuleEntry(src/rule-entry.ts) with the name, the severity and acreate(data)factory.src/create-rule.ts: the rule creator. It setsmeta.docs.urlfromsrc/meta.ts:https://phmilk.github.io/reforged-ts/docs/<reforged.docs>/guides/lint-rules/<rule>, wherereforged.docsinpackage.jsonis a docs version label (nextwhile the library is a prerelease, then itsmajor.minor, which the version step,release:version, stamps:docs/release.md).src/classify/: the shared classification helpers, one module per concept (package.ts,native.ts,handle.ts,wrapper.ts,creation.ts,top-level.ts, ...). Rules never inspect declarations themselves.src/data/: loading and shape checks for the data files.schema.tsholds the field readers, and there is one parser module per file.optional.tsfinds a file another package publishes in the linted project's installation.index.tsloads them all intoPluginData.data/*.json: the plugin's own data files, shipped.docs/<rule>.md: one page per rule, shipped. The docs site'sdocs:collectcopies each one under the Lint rules guide, the pagemeta.docs.urllinks.templates/rule-doc.mdis the template; it is not shipped.test/rules/<rule>.test.ts: the RuleTester fixtures of one rule.test/plugin.test.ts: the rule table, the recommended config, the metadata, the docs pages, and loading without the optional packages.test/data.test.ts: the shape errors, and a check that every Native a data file names resolves in the Typings.test/support/: the seams.rule-tester.tswires the RuleTester to vitest.lint.tslints with the recommended config as a Map project does.plugin.tsprovides the plugin created with the fixture project as its project root (its optional packages are the fixture's), andruleOf(name).typings.tsprovidesinstalledNatives().fixture-project.tsholds the paths.test/fixture-project/: the Map project the rules lint. See itstsconfig.json.- The real
reforged-typescomes from this package's devDependency. node_modules/reforged-ts/is a stub declaration package, committed; the root.gitignorere-includes it.- Every RuleTester case is linted as
file.ts, with its content replaced in memory. - The other
.tsfiles are modules a case can import.
- The real
Adding or changing a rule
-
Rule file. Create
src/rules/<rule>.tswithcreateRuleand a defaultdefineRuleEntry({ name, severity, create }). Set the severity from #16's table; a rule outside it takes the severity its issue decides:errorfor a pattern that desyncs, crashes or leaks with certainty,warnfor one wrong in most contexts (spec #50). The rule declaresmeta.type,meta.docs.description,messageswith ids,hasSuggestionsif it suggests, andschemawithdefaultOptions. Onlyno-legacy-w3ts-namesmay declarefixable. Each message says the pitfall, the consequence and the replacement. -
Match syntactically first. Then ask the checker, through
src/classify/, only for the matched node. CallESLintUtils.getParserServices(context)at the top ofcreate: without type information, the rule then fails at the first file. -
Registry. Add one import and one line to
src/rules/index.ts, in name order. The recommended config follows from it. -
Rule-table test. In
test/plugin.test.ts, add the rule todecidedTablewith its severity, and raise the rule count and the error/warning split the table asserts. ExtendeveryRuleReportsso the new rule reports it. A rule withrequiresgoes inoptionalRulestoo. -
Fixtures. Create
test/rules/<rule>.test.tswithcreateRuleTester()andruleOf("<rule>"). It needs these cases:- valid cases, including every allowlist family and every option;
- invalid cases with message ids and data;
- suggestion outputs;
- fix outputs, for rule 6.
Test escapes (
eslint-disable-next-line reforged/<rule> -- reason) and severities withlintWithRecommended. The RuleTester registers rules under its own prefix. When a case needs a Wrapper member the stub lacks, add it totest/fixture-project/node_modules/reforged-ts/index.d.ts, with the shape of the real library. -
Docs page. Copy
templates/rule-doc.mdtodocs/<rule>.md. Keep the title and the six headings. The summary paragraph is also the rule's line on the site's Lint rules index. Add a row to the rules table inREADME.md.docs:collectfails on a rule of the registry without a page and on a page without a rule. -
Data. If the rule reads a data file, write a parser in
src/data/with theschema.tsreaders. Add the file toPluginDataandDataFilesinsrc/data/index.ts. Add shape tests totest/data.test.tsand the file's row to "Data files" below. Every Native the file names must be ininstalledNatives(). -
Changeset. Write a changeset for
eslint-plugin-reforged: a minor for a new rule. The command and how to write its text are in "Adding a changeset" ofdocs/release.md.
Data files
| File | Owner | Shape |
|---|---|---|
data/unsafe-natives.json | this plugin | [{ name, reason, replacement }] |
data/local-safe.json | this plugin | [{ name, kind: "visual" | "text" | "pure", reason }]; name is a Native, print, Class#member or Class.member (static); a pure entry is a Native |
data/creation-natives.json | this plugin | [{ name, family }] |
migration/renames.json | reforged-ts | [{ old, new, kind, versions: { from, to }, oneToOne, note }] (the library's renames.schema.json); the parser skips the no-renames marker { kind: "noRenames", versions, note } |
async-natives.json | reforged-types | a sorted array of Native names; the same set the Typings tag @async (the oracle test in test/data.test.ts) |
A file another package publishes is read from the linted project's installation of that package, found from the project root (src/data/optional.ts), never from this plugin's dependencies. Declare it as an OptionalDataFile next to its parser, read it in loadPluginData with its empty value, and list the package in the rule entry's requires: when the package is missing, the plugin warns once and registers the rule disabled.
A data file grows by pull request, and every line carries its reason. A review can then challenge one entry. A file with an unexpected shape throws a DataFileError at plugin load, naming the file and the field. Changing the shape of a file the plugin reads from another package is a major of this plugin.
Definition of done
- The fixtures of the rule are green.
- Its docs page is present with the fixed sections.
- The rule-table test is green.
- A changeset is written.
pnpm checkis green.