Repository navigation
Description
Summary
A diagnosticSeverity key naming a rule the running @effect/tsgo does not provide is accepted without any diagnostic, so a project that pins a rule at error believes it is enforced while nothing runs.
parseDiagnosticSeverityMap stores every string-valued key it sees, with no membership test against the rule registry (etscore/options_parser.go#L427-L444), reached from the top-level map at options_parser.go#L66 and from each overrides entry at options_parser.go#L347. The resolver merges the maps unchanged (internal/pluginoptions/resolver.go#L40-L65), and the rule runner then looks up a severity per registered rule rather than iterating the configured names (internal/rulerunner/diagnostics.go#L135), so an entry naming nothing is never visited and never reported.
The shipped JSON schema does not close this either: effectLanguageServicePluginDiagnosticSeverityDefinition carries "additionalProperties": {"enum": ["off","error","warning","message","suggestion"]}, so an editor validating against it accepts any key with a valid severity value.
This is the configuration-side sibling of #681, which asked for the resolved Effect major to be reported so a version-gated rule's silence would be explainable. A pinned name that can never fire is the same class of unexplained silence, one layer earlier: there the rule exists and does not run, here the rule does not exist at all.
Environment
- bun 1.4.2
- pnpm 10.34.5
@effect/tsgo0.45.0- TypeScript 7.0.2
- macOS arm64 (Darwin 25.4.0)
Reproduction
DIR=$(mktemp -d)
cd "$DIR"
mkdir -p src
echo 'export const x: number = 1' > src/a.ts
cat > package.json <<'JSON'
{ "name": "tsgo-unknown-rule-repro", "private": true, "version": "0.0.0" }
JSON
cat > tsconfig.json <<'JSON'
{
"compilerOptions": {
"strict": true,
"noEmit": true,
"module": "preserve",
"moduleResolution": "bundler",
"target": "esnext",
"plugins": [
{
"name": "@effect/language-service",
"diagnosticSeverity": {
"floatingEffect": "error",
"importFromBarrel": "error"
}
}
]
},
"include": ["src"]
}
JSON
bun install --ignore-scripts @effect/[email protected] [email protected]
./node_modules/.bin/effect-tsgo patch --typescript
./node_modules/.bin/tsc --version
./node_modules/.bin/tsc -p tsconfig.json --noEmit --pretty false; echo "exit=$?"
rm -rf "$DIR"Measured output:
Version 7.0.2+effect-tsgo.0.45.0
exit=0
importFromBarrel is not a rule in 0.45.0. Its schema.json records 113 rule names and does not contain it. The compiler reports nothing, exits 0, and the entry has no effect.
Expected behavior
A diagnostic naming the key that does not resolve, anchored on it in the config file that declares it, so the mistake is visible where it was made. A spelling suggestion when one is close would make the common case self-correcting. Default warning rather than error seems right, because the same plugin block is read by the JS language service and a hard failure on upgrade would be disruptive; note that with the default ignoreEffectWarningsInTscExitCode: false a warning already fails tsc.
Where this came up
A TypeScript 7.0.2 monorepo (@effect/tsgo 0.45.0) pinned three names at error in the diagnosticSeverity map of its shared tsconfig.base.json, out of 107 entries:
importFromBarreloutdatedEffectCodegenunsupportedServiceAccessors
None of the three is a rule in @effect/tsgo. All three are rules in @effect/language-service 0.87.2, which was installed alongside it in the same tree. Measured over the two schema.json files in that repository's node_modules: the JS package declares 77 rule names, @effect/tsgo 0.45.0 declares 113, and the set difference js - tsgo is exactly those 3. Both tools read the same plugin block, because etscore/consts.go#L5 sets EffectPluginName = "@effect/language-service", which is how a shared config drifts into naming rules one of the two has never had.
The repository believed three rules were enforced at error. They had never run.
Measured against that repository at commit 6d23bd7f (working tree dirty; nothing in it was modified, staged, or built for these runs), with its plugin block applied to two of its packages, using a compiler built from main at ae1ed026:
| run | compiler | config | exit | Effect diagnostics |
|---|---|---|---|---|
| E0 | main @ ae1ed026 |
with the three pins | 0 | 0 |
| E1 | ae1ed026 + the change below |
with the three pins | 2 | 6 (3 per package, each name once) |
| E2 | ae1ed026 + the change below |
pins removed | 0 | 0 |
E0 is the silence being reported. E1 names each of the three once per config and nothing else. E2 shows the check is silent on a config whose names all resolve.
Deliberately not covered
An invalid severity value is a separate defect and is not part of this report. ParseSeverity returns SeverityError for any unrecognized string (etscore/severity.go#L22-L40), so "errror" silently promotes a rule to error rather than disabling it. That is loud and wrong rather than silent, so it wants its own decision about whether to report or to reject, and folding it in here would conflate the two.
Two further gaps are adjacent but separable, and are noted here rather than folded in:
- The generated
diagnosticSeverityproperties inschema.jsonlist rules only, so neitherunusedDirectivenor a key for this diagnostic would get editor completion or a documented default. Carrying non-rule keys in a map described as "Maps rule names to severity levels" is a change to a published artifact and a call for the maintainers. - The raw-JSON config path (
parseOwnConfigOfJson, used by the API server's raw-options surfaces and by LSP inferred projects) has no source file, so there is no node to anchor a diagnostic on. A location-less warning there would be worse than none.
A third, on tsc --build: these diagnostics reach the config-file parsing diagnostics, whose presence sets errors in the build info, so an offending config keeps its project out of date and re-typechecks on every --build. Measured: there is no cascade to dependent projects, and a composite project whose only diagnostic is a pre-existing warning-severity rule behaves the same way, so this is existing policy rather than something this diagnostic introduces. Also measured, and worth recording because it is counterintuitive: excluding warnings from that flag would not silence the diagnostic on later builds. With the errors key absent the project reads up to date, does not rebuild, and still reports the diagnostic, because an up-to-date project's config diagnostics are re-walked and reported anyway. The open question there is the exit status, which moves from 2 to 1 for an up-to-date project, not silence.
Prior art referenced: #681 (report the resolved Effect major so a version-gated rule's silence is explainable) and #346 (ship schema.json with @effect/tsgo), which is what makes the additionalProperties gap above reachable from an editor.
Disclosure: researched and drafted with AI assistance. Every number above comes from a command run against the sources cited.
Activity