diff --git a/.changeset/unknown-rule-name-diagnostic.md b/.changeset/unknown-rule-name-diagnostic.md
new file mode 100644
index 00000000..6943718c
--- /dev/null
+++ b/.changeset/unknown-rule-name-diagnostic.md
@@ -0,0 +1,25 @@
+---
+"@effect/tsgo": minor
+---
+
+Report a diagnostic when `diagnosticSeverity` names a rule this build does not provide. A name that does not resolve was previously accepted in silence, so a project pinning a rule at `error` believed it was enforced while nothing ran. The check covers the top-level `diagnosticSeverity` map and the map inside each `overrides` entry, is anchored on the offending key in the config file that declares it, and carries a spelling suggestion when one is close enough.
+
+```jsonc
+{
+ "compilerOptions": {
+ "plugins": [
+ {
+ "name": "@effect/language-service",
+ "diagnosticSeverity": {
+ "floatingEfect": "error"
+ // warning TS377135: Unknown Effect diagnostic rule `floatingEfect` in
+ // `diagnosticSeverity`. Did you mean `floatingEffect`?
+ // effect(unknownRuleName)
+ }
+ }
+ ]
+ }
+}
+```
+
+The diagnostic is a `warning` by default and is configured through `diagnosticSeverity.unknownRuleName` like any other, so `"unknownRuleName": "off"` silences it. Note that with the default `ignoreEffectWarningsInTscExitCode: false` a warning already fails `tsc`.
diff --git a/_patches/typescript/030-tsoptions-effect-plugin-validation.patch b/_patches/typescript/030-tsoptions-effect-plugin-validation.patch
new file mode 100644
index 00000000..d9d25ef6
--- /dev/null
+++ b/_patches/typescript/030-tsoptions-effect-plugin-validation.patch
@@ -0,0 +1,59 @@
+diff --git a/tsc/internal/tsoptions/parsinghelpers.go b/tsc/internal/tsoptions/parsinghelpers.go
+--- a/tsc/internal/tsoptions/parsinghelpers.go
++++ b/tsc/internal/tsoptions/parsinghelpers.go
+@@ -22,6 +22,32 @@
+ // standard compiler option merging.
+ func RegisterMergeCompilerOptionsCallback(cb func(targetOptions, sourceOptions *core.CompilerOptions, rawSource any, sourceConfigPath string, basePath string)) {
+ MergeCompilerOptionsCallback = cb
++}
++
++// ValidateEffectPluginOptionsCallback lets the Effect integration report
++// configuration diagnostics for its own plugin block, anchored on nodes inside the
++// config file that declares it. It is invoked while compilerOptions.plugins is
++// parsed, which is the only point at which those nodes exist, and therefore runs
++// before any extends hop is merged: it must not decide whether a diagnostic is
++// enabled, because the setting that governs that may be inherited. That decision
++// belongs to FinalizeEffectPluginDiagnosticsCallback.
++var ValidateEffectPluginOptionsCallback func(sourceFile *ast.SourceFile, pluginsNode *ast.Node) []*ast.Diagnostic
++
++// RegisterValidateEffectPluginOptionsCallback registers a callback invoked when
++// compilerOptions.plugins is parsed from a tsconfig file.
++func RegisterValidateEffectPluginOptionsCallback(cb func(sourceFile *ast.SourceFile, pluginsNode *ast.Node) []*ast.Diagnostic) {
++ ValidateEffectPluginOptionsCallback = cb
++}
++
++// FinalizeEffectPluginDiagnosticsCallback lets the Effect integration drop or
++// recategorize the diagnostics it contributed earlier, now that the whole extends
++// chain has been merged and the config's final compiler options are known.
++var FinalizeEffectPluginDiagnosticsCallback func(diagnostics []*ast.Diagnostic, options *core.CompilerOptions) []*ast.Diagnostic
++
++// RegisterFinalizeEffectPluginDiagnosticsCallback registers a callback invoked
++// after the whole extends chain has been merged.
++func RegisterFinalizeEffectPluginDiagnosticsCallback(cb func(diagnostics []*ast.Diagnostic, options *core.CompilerOptions) []*ast.Diagnostic) {
++ FinalizeEffectPluginDiagnosticsCallback = cb
+ }
+
+ func ParseTristate(value any) core.Tristate {
+diff --git a/tsc/internal/tsoptions/tsconfigparsing.go b/tsc/internal/tsoptions/tsconfigparsing.go
+--- a/tsc/internal/tsoptions/tsconfigparsing.go
++++ b/tsc/internal/tsoptions/tsconfigparsing.go
+@@ -204,6 +204,9 @@
+ switch parentOption.Name {
+ case "compilerOptions":
+ parseDiagnostics = ParseCompilerOptions(option.Name, value, compilerOptions)
++ if option.Name == "plugins" && ValidateEffectPluginOptionsCallback != nil {
++ parseDiagnostics = append(parseDiagnostics, ValidateEffectPluginOptionsCallback(sourceFile, propertyAssignment.Initializer)...)
++ }
+ case "typeAcquisition":
+ parseDiagnostics = ParseTypeAcquisition(option.Name, value, typeAcquisition)
+ }
+@@ -1261,6 +1264,9 @@
+ parsedConfig, errors := parseConfig(json, sourceFile, host, basePath, configFileName, resolutionStack, extendedConfigCache)
+ mergeCompilerOptions(parsedConfig.options, existingOptions, existingOptionsRaw, configFileName, basePath)
+ handleOptionConfigDirTemplateSubstitution(parsedConfig.options, basePathForFileNames)
++ if FinalizeEffectPluginDiagnosticsCallback != nil {
++ errors = FinalizeEffectPluginDiagnosticsCallback(errors, parsedConfig.options)
++ }
+ rawConfig := parseJsonToStringKey(parsedConfig.raw)
+ if configFileName != "" && parsedConfig.options != nil {
+ parsedConfig.options.ConfigFilePath = tspath.NormalizeSlashes(configFileName)
diff --git a/etscheckerhooks/init.go b/etscheckerhooks/init.go
index 9201af62..d3572ac1 100644
--- a/etscheckerhooks/init.go
+++ b/etscheckerhooks/init.go
@@ -7,6 +7,7 @@ import (
"context"
"github.com/effect-ts/tsgo/etscore"
+ "github.com/effect-ts/tsgo/internal/effectconfigcheck"
"github.com/effect-ts/tsgo/internal/effectconfigraw"
"github.com/effect-ts/tsgo/internal/rulerunner"
"github.com/microsoft/TypeScript/tsc/shim/ast"
@@ -19,6 +20,7 @@ func init() {
// Set the version suffix so that core.Version() includes the Effect version
core.SetVersionSuffix("+effect-tsgo." + etscore.EffectVersion)
effectconfigraw.Register()
+ effectconfigcheck.Register()
// Register the after check source file callback
checker.RegisterAfterCheckSourceFileCallback(afterCheckSourceFile)
}
diff --git a/internal/diagnostics/effectDiagnosticMessages.json b/internal/diagnostics/effectDiagnosticMessages.json
index 1db9e0fa..6b5f1614 100644
--- a/internal/diagnostics/effectDiagnosticMessages.json
+++ b/internal/diagnostics/effectDiagnosticMessages.json
@@ -526,5 +526,13 @@
"`Effect.andThen` expresses this sequencing more directly than `Effect.flatMap` with a zero-parameter callback. effect(flatMapIgnoredParamToAndThen)": {
"category": "Suggestion",
"code": 377132
+ },
+ "Unknown Effect diagnostic rule `{0}` in `diagnosticSeverity`. This version of @effect/tsgo does not provide it, so this entry has no effect. effect(unknownRuleName)": {
+ "category": "Warning",
+ "code": 377134
+ },
+ "Unknown Effect diagnostic rule `{0}` in `diagnosticSeverity`. Did you mean `{1}`? effect(unknownRuleName)": {
+ "category": "Warning",
+ "code": 377135
}
}
diff --git a/internal/effectconfigcheck/effectconfigcheck.go b/internal/effectconfigcheck/effectconfigcheck.go
new file mode 100644
index 00000000..9a5180af
--- /dev/null
+++ b/internal/effectconfigcheck/effectconfigcheck.go
@@ -0,0 +1,215 @@
+// Package effectconfigcheck validates the @effect/language-service plugin block of
+// a tsconfig file and reports configuration diagnostics anchored on the offending
+// node inside that file.
+//
+// Validation runs in two phases because the two things it needs become available at
+// different points. The offending node exists only while the config file that
+// declares it is being parsed, before any extends hop is merged; whether the
+// diagnostic is enabled, and at which severity, is a property of the fully merged
+// configuration. ValidatePluginsNode therefore reports every unresolved name it
+// finds, and FinalizeDiagnostics drops or recategorizes them once the merge is done.
+package effectconfigcheck
+
+import (
+ "slices"
+ "sync"
+
+ "github.com/effect-ts/tsgo/etscore"
+ "github.com/effect-ts/tsgo/internal/directives"
+ "github.com/effect-ts/tsgo/internal/rule"
+ "github.com/effect-ts/tsgo/internal/rules"
+ "github.com/microsoft/TypeScript/tsc/shim/ast"
+ "github.com/microsoft/TypeScript/tsc/shim/core"
+ tsdiag "github.com/microsoft/TypeScript/tsc/shim/diagnostics"
+ "github.com/microsoft/TypeScript/tsc/shim/tsoptions"
+)
+
+var (
+ unknownRuleNameMessage = tsdiag.Unknown_Effect_diagnostic_rule_0_in_diagnosticSeverity_This_version_of_effect_Slashtsgo_does_not_provide_it_so_this_entry_has_no_effect_effect_unknownRuleName
+ didYouMeanMessage = tsdiag.Unknown_Effect_diagnostic_rule_0_in_diagnosticSeverity_Did_you_mean_1_effect_unknownRuleName
+)
+
+// Register wires tsconfig plugin-option validation into TypeScript-Go.
+func Register() {
+ tsoptions.RegisterValidateEffectPluginOptionsCallback(ValidatePluginsNode)
+ tsoptions.RegisterFinalizeEffectPluginDiagnosticsCallback(FinalizeDiagnostics)
+}
+
+// ConfigurableNames returns every name diagnosticSeverity accepts: the rules in the
+// registry plus the diagnostics that are configurable without being rules.
+var ConfigurableNames = sync.OnceValue(func() []string {
+ names := make([]string, 0, len(rules.All)+len(rule.NonRuleConfigurableNames))
+ for i := range rules.All {
+ names = append(names, rules.All[i].Name)
+ }
+ names = append(names, rule.NonRuleConfigurableNames...)
+ slices.Sort(names)
+ return names
+})
+
+// ValidatePluginsNode reports a diagnostic for every diagnosticSeverity key in the
+// @effect/language-service plugin entry that this build does not provide, covering
+// both the top-level map and the map inside each overrides entry. It reports
+// unconditionally: the config file being parsed does not yet know what it inherits,
+// so FinalizeDiagnostics owns the decision to keep, drop or recategorize.
+func ValidatePluginsNode(sourceFile *ast.SourceFile, pluginsNode *ast.Node) []*ast.Diagnostic {
+ if sourceFile == nil || pluginsNode == nil {
+ return nil
+ }
+
+ pluginEntry := findEffectPluginEntry(pluginsNode)
+ if pluginEntry == nil {
+ return nil
+ }
+
+ known := ConfigurableNames()
+ var diags []*ast.Diagnostic
+ diags = appendUnknownNames(diags, sourceFile, known, propertyValue(pluginEntry, "diagnosticSeverity"))
+ for _, override := range arrayElements(propertyValue(pluginEntry, "overrides")) {
+ overrideOptions := propertyValue(override, "options")
+ diags = appendUnknownNames(diags, sourceFile, known, propertyValue(overrideOptions, "diagnosticSeverity"))
+ }
+ return diags
+}
+
+// FinalizeDiagnostics applies the merged configuration to the diagnostics
+// ValidatePluginsNode contributed. Diagnostics from any other source pass through
+// untouched. This is what makes `diagnostics: false` and
+// `diagnosticSeverity.unknownRuleName` work when they are inherited through
+// extends rather than declared in the file that carries the offending name.
+func FinalizeDiagnostics(diags []*ast.Diagnostic, options *core.CompilerOptions) []*ast.Diagnostic {
+ if !slices.ContainsFunc(diags, isUnknownRuleNameDiagnostic) {
+ return diags
+ }
+
+ severity, enabled := resolveSeverity(options)
+ category := directives.ToCategory(severity)
+
+ result := make([]*ast.Diagnostic, 0, len(diags))
+ for _, diag := range diags {
+ if !isUnknownRuleNameDiagnostic(diag) {
+ result = append(result, diag)
+ continue
+ }
+ if !enabled {
+ continue
+ }
+ result = append(result, withCategory(diag, category))
+ }
+ return result
+}
+
+// resolveSeverity reads the severity of the unknown-rule-name diagnostic from the
+// merged configuration, and reports whether it should be surfaced at all.
+func resolveSeverity(options *core.CompilerOptions) (etscore.Severity, bool) {
+ if options == nil || options.Effect == nil || !options.Effect.Diagnostics {
+ return etscore.SeverityOff, false
+ }
+ severity, configured := options.Effect.DiagnosticSeverity[rule.UnknownRuleNameName]
+ if !configured {
+ severity = etscore.SeverityWarning
+ }
+ return severity, !severity.IsOff()
+}
+
+func isUnknownRuleNameDiagnostic(diag *ast.Diagnostic) bool {
+ if diag == nil {
+ return false
+ }
+ code := diag.Code()
+ return code == unknownRuleNameMessage.Code() || code == didYouMeanMessage.Code()
+}
+
+func withCategory(diag *ast.Diagnostic, category tsdiag.Category) *ast.Diagnostic {
+ if diag.Category() == category {
+ return diag
+ }
+ return ast.NewDiagnosticFromSerialized(
+ diag.File(),
+ core.NewTextRange(diag.Pos(), diag.End()),
+ diag.Code(),
+ category,
+ diag.MessageKey(),
+ diag.MessageArgs(),
+ diag.MessageChain(),
+ diag.RelatedInformation(),
+ diag.ReportsUnnecessary(),
+ diag.ReportsDeprecated(),
+ diag.SkippedOnNoEmit(),
+ )
+}
+
+func appendUnknownNames(
+ diags []*ast.Diagnostic,
+ sourceFile *ast.SourceFile,
+ known []string,
+ diagnosticSeverity *ast.Node,
+) []*ast.Diagnostic {
+ if diagnosticSeverity == nil || !ast.IsObjectLiteralExpression(diagnosticSeverity) {
+ return diags
+ }
+ for _, property := range diagnosticSeverity.Properties() {
+ if !ast.IsPropertyAssignment(property) {
+ continue
+ }
+ name := property.Name()
+ if name == nil || !ast.IsStringLiteralLike(name) {
+ continue
+ }
+ text := ast.GetTextOfPropertyName(name)
+ if text == "" || slices.Contains(known, text) {
+ continue
+ }
+ diags = append(diags, unknownNameDiagnostic(sourceFile, name, text, known))
+ }
+ return diags
+}
+
+func unknownNameDiagnostic(sourceFile *ast.SourceFile, name *ast.Node, text string, known []string) *ast.Diagnostic {
+ if suggestion := core.GetSpellingSuggestionForStrings(text, slices.Values(known)); suggestion != "" {
+ return tsoptions.CreateDiagnosticForNodeInSourceFile(sourceFile, name, didYouMeanMessage, text, suggestion)
+ }
+ return tsoptions.CreateDiagnosticForNodeInSourceFile(sourceFile, name, unknownRuleNameMessage, text)
+}
+
+// findEffectPluginEntry returns the object literal in the plugins array whose name
+// is the Effect language service plugin, or nil when the block is absent or is not
+// written as a literal.
+func findEffectPluginEntry(pluginsNode *ast.Node) *ast.Node {
+ for _, entry := range arrayElements(pluginsNode) {
+ name := propertyValue(entry, "name")
+ if name == nil || !ast.IsStringLiteralLike(name) {
+ continue
+ }
+ if name.Text() == etscore.EffectPluginName {
+ return entry
+ }
+ }
+ return nil
+}
+
+func arrayElements(node *ast.Node) []*ast.Node {
+ if node == nil || !ast.IsArrayLiteralExpression(node) {
+ return nil
+ }
+ return node.Elements()
+}
+
+func propertyValue(objectLiteral *ast.Node, key string) *ast.Node {
+ if objectLiteral == nil || !ast.IsObjectLiteralExpression(objectLiteral) {
+ return nil
+ }
+ for _, property := range objectLiteral.Properties() {
+ if !ast.IsPropertyAssignment(property) {
+ continue
+ }
+ name := property.Name()
+ if name == nil || !ast.IsStringLiteralLike(name) {
+ continue
+ }
+ if ast.GetTextOfPropertyName(name) == key {
+ return property.Initializer()
+ }
+ }
+ return nil
+}
diff --git a/internal/effectconfigcheck/effectconfigcheck_test.go b/internal/effectconfigcheck/effectconfigcheck_test.go
new file mode 100644
index 00000000..5ffa3fa9
--- /dev/null
+++ b/internal/effectconfigcheck/effectconfigcheck_test.go
@@ -0,0 +1,348 @@
+package effectconfigcheck_test
+
+import (
+ "testing"
+ "testing/fstest"
+
+ "github.com/effect-ts/tsgo/internal/rule"
+ "github.com/microsoft/TypeScript/tsc/shim/ast"
+ "github.com/microsoft/TypeScript/tsc/shim/core"
+ tsdiag "github.com/microsoft/TypeScript/tsc/shim/diagnostics"
+ "github.com/microsoft/TypeScript/tsc/shim/parser"
+ "github.com/microsoft/TypeScript/tsc/shim/tsoptions"
+ "github.com/microsoft/TypeScript/tsc/shim/tspath"
+ "github.com/microsoft/TypeScript/tsc/shim/vfs"
+ "github.com/microsoft/TypeScript/tsc/shim/vfs/vfstest"
+
+ // etscheckerhooks registers the config-validation callbacks in its init, which
+ // is the same path the compiler uses. Registering from the tests themselves
+ // would race, because every test here runs in parallel.
+ _ "github.com/effect-ts/tsgo/etscheckerhooks"
+)
+
+const currentDirectory = "/.src"
+
+const unknownRuleNameCode = 377134
+
+const didYouMeanCode = 377135
+
+type parseConfigHost struct {
+ fs vfs.FS
+}
+
+func (h *parseConfigHost) FS() vfs.FS { return h.fs }
+
+func (h *parseConfigHost) GetCurrentDirectory() string { return currentDirectory }
+
+// allConfigDiagnostics parses the named tsconfig out of files and returns every
+// diagnostic the config parse produced, unfiltered.
+func allConfigDiagnostics(t *testing.T, files map[string]string, configName string) []*ast.Diagnostic {
+ t.Helper()
+
+ testfs := make(map[string]any, len(files))
+ for name, content := range files {
+ testfs[tspath.GetNormalizedAbsolutePath(name, currentDirectory)] = &fstest.MapFile{Data: []byte(content)}
+ }
+ fs := vfstest.FromMap(testfs, true /*useCaseSensitiveFileNames*/)
+
+ configFileName := tspath.GetNormalizedAbsolutePath(configName, currentDirectory)
+ sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{
+ FileName: configFileName,
+ Path: tspath.ToPath(configFileName, currentDirectory, true),
+ }, files[configName], core.ScriptKindJSON)
+
+ parsed := tsoptions.ParseJsonSourceFileConfigFileContent(
+ &tsoptions.TsConfigSourceFile{SourceFile: sourceFile},
+ &parseConfigHost{fs: fs},
+ tspath.GetDirectoryPath(configFileName),
+ nil,
+ nil,
+ configFileName,
+ nil,
+ nil,
+ nil,
+ )
+ return parsed.Errors
+}
+
+// configDiagnostics returns only the Effect diagnostics. TS18003 (no inputs found)
+// is inherent to a config fixture that ships no source files and is not what most
+// of these tests are about. Anything asserting on what FinalizeDiagnostics must
+// leave alone has to use allConfigDiagnostics instead.
+func configDiagnostics(t *testing.T, files map[string]string, configName string) []*ast.Diagnostic {
+ t.Helper()
+ var effectDiags []*ast.Diagnostic
+ for _, diag := range allConfigDiagnostics(t, files, configName) {
+ if rule.IsEffectCode(diag.Code()) {
+ effectDiags = append(effectDiags, diag)
+ }
+ }
+ return effectDiags
+}
+
+func pluginConfig(body string) string {
+ return `{
+ "compilerOptions": {
+ "plugins": [
+ {
+ "name": "@effect/language-service",
+ ` + body + `
+ }
+ ]
+ }
+}`
+}
+
+func codesOf(diags []*ast.Diagnostic) []int32 {
+ codes := make([]int32, 0, len(diags))
+ for _, diag := range diags {
+ codes = append(codes, diag.Code())
+ }
+ return codes
+}
+
+func TestUnknownRuleNameIsReported(t *testing.T) {
+ t.Parallel()
+
+ diags := configDiagnostics(t, map[string]string{
+ "tsconfig.json": pluginConfig(`"diagnosticSeverity": { "importFromBarrel": "error" }`),
+ }, "tsconfig.json")
+
+ if len(diags) != 1 {
+ t.Fatalf("expected 1 diagnostic, got %d: %v", len(diags), codesOf(diags))
+ }
+ if got := diags[0].Code(); got != unknownRuleNameCode {
+ t.Errorf("code = %d, want %d", got, unknownRuleNameCode)
+ }
+ if got := diags[0].Category(); got != tsdiag.CategoryWarning {
+ t.Errorf("category = %v, want warning", got)
+ }
+ if got := diags[0].File(); got == nil {
+ t.Error("diagnostic is not anchored on the config file")
+ }
+ if args := diags[0].MessageArgs(); len(args) != 1 || args[0] != "importFromBarrel" {
+ t.Errorf("message args = %v, want [importFromBarrel]", args)
+ }
+}
+
+func TestKnownNamesAreSilent(t *testing.T) {
+ t.Parallel()
+
+ diags := configDiagnostics(t, map[string]string{
+ "tsconfig.json": pluginConfig(`"diagnosticSeverity": { "floatingEffect": "error", "unusedDirective": "warning", "unknownRuleName": "warning" }`),
+ }, "tsconfig.json")
+
+ if len(diags) != 0 {
+ t.Fatalf("expected no diagnostics, got %v", codesOf(diags))
+ }
+}
+
+func TestOverridesAreChecked(t *testing.T) {
+ t.Parallel()
+
+ diags := configDiagnostics(t, map[string]string{
+ "tsconfig.json": pluginConfig(`"overrides": [
+ { "include": ["src/**"], "options": { "diagnosticSeverity": { "notARule": "error" } } }
+ ]`),
+ }, "tsconfig.json")
+
+ if len(diags) != 1 {
+ t.Fatalf("expected 1 diagnostic, got %d: %v", len(diags), codesOf(diags))
+ }
+ if args := diags[0].MessageArgs(); len(args) != 1 || args[0] != "notARule" {
+ t.Errorf("message args = %v, want [notARule]", args)
+ }
+}
+
+func TestNearMissSuggestsTheIntendedRule(t *testing.T) {
+ t.Parallel()
+
+ diags := configDiagnostics(t, map[string]string{
+ "tsconfig.json": pluginConfig(`"diagnosticSeverity": { "floatingEfect": "error" }`),
+ }, "tsconfig.json")
+
+ if len(diags) != 1 {
+ t.Fatalf("expected 1 diagnostic, got %d: %v", len(diags), codesOf(diags))
+ }
+ if got := diags[0].Code(); got != didYouMeanCode {
+ t.Fatalf("code = %d, want %d", got, didYouMeanCode)
+ }
+ if args := diags[0].MessageArgs(); len(args) != 2 || args[1] != "floatingEffect" {
+ t.Errorf("message args = %v, want suggestion floatingEffect", args)
+ }
+}
+
+func TestSeverityIsConfigurable(t *testing.T) {
+ t.Parallel()
+
+ t.Run("off suppresses the diagnostic", func(t *testing.T) {
+ t.Parallel()
+ diags := configDiagnostics(t, map[string]string{
+ "tsconfig.json": pluginConfig(`"diagnosticSeverity": { "importFromBarrel": "error", "unknownRuleName": "off" }`),
+ }, "tsconfig.json")
+ if len(diags) != 0 {
+ t.Fatalf("expected no diagnostics, got %v", codesOf(diags))
+ }
+ })
+
+ t.Run("error raises the category", func(t *testing.T) {
+ t.Parallel()
+ diags := configDiagnostics(t, map[string]string{
+ "tsconfig.json": pluginConfig(`"diagnosticSeverity": { "importFromBarrel": "error", "unknownRuleName": "error" }`),
+ }, "tsconfig.json")
+ if len(diags) != 1 {
+ t.Fatalf("expected 1 diagnostic, got %v", codesOf(diags))
+ }
+ if got := diags[0].Category(); got != tsdiag.CategoryError {
+ t.Errorf("category = %v, want error", got)
+ }
+ })
+}
+
+func TestUnrelatedPluginIsIgnored(t *testing.T) {
+ t.Parallel()
+
+ diags := configDiagnostics(t, map[string]string{
+ "tsconfig.json": `{
+ "compilerOptions": {
+ "plugins": [
+ { "name": "some-other-plugin", "diagnosticSeverity": { "importFromBarrel": "error" } }
+ ]
+ }
+}`,
+ }, "tsconfig.json")
+
+ if len(diags) != 0 {
+ t.Fatalf("expected no diagnostics, got %v", codesOf(diags))
+ }
+}
+
+func TestExtendedConfigIsChecked(t *testing.T) {
+ t.Parallel()
+
+ diags := configDiagnostics(t, map[string]string{
+ "tsconfig.base.json": pluginConfig(`"diagnosticSeverity": { "importFromBarrel": "error" }`),
+ "tsconfig.json": `{ "extends": "./tsconfig.base.json" }`,
+ }, "tsconfig.json")
+
+ if len(diags) != 1 {
+ t.Fatalf("expected 1 diagnostic, got %d: %v", len(diags), codesOf(diags))
+ }
+ if file := diags[0].File(); file == nil || tspath.GetBaseFileName(file.FileName()) != "tsconfig.base.json" {
+ t.Errorf("diagnostic is not anchored on the config that declares the plugin block")
+ }
+}
+
+func TestDiagnosticsDisabledSuppressesTheCheck(t *testing.T) {
+ t.Parallel()
+
+ diags := configDiagnostics(t, map[string]string{
+ "tsconfig.json": pluginConfig(`"diagnostics": false, "diagnosticSeverity": { "importFromBarrel": "error" }`),
+ }, "tsconfig.json")
+
+ if len(diags) != 0 {
+ t.Fatalf("expected no diagnostics, got %v", codesOf(diags))
+ }
+}
+
+func TestInheritedSuppressionAcrossExtends(t *testing.T) {
+ t.Parallel()
+
+ t.Run("unknownRuleName off in a base silences a name declared in a child", func(t *testing.T) {
+ t.Parallel()
+ diags := configDiagnostics(t, map[string]string{
+ "tsconfig.base.json": pluginConfig(`"diagnosticSeverity": { "unknownRuleName": "off" }`),
+ "tsconfig.json": `{ "extends": "./tsconfig.base.json", "compilerOptions": { "plugins": [
+ { "name": "@effect/language-service", "diagnosticSeverity": { "importFromBarrel": "error" } }
+ ] } }`,
+ }, "tsconfig.json")
+ if len(diags) != 0 {
+ t.Fatalf("expected no diagnostics, got %v", codesOf(diags))
+ }
+ })
+
+ t.Run("diagnostics false in a base silences a name declared in a child", func(t *testing.T) {
+ t.Parallel()
+ diags := configDiagnostics(t, map[string]string{
+ "tsconfig.base.json": pluginConfig(`"diagnostics": false`),
+ "tsconfig.json": `{ "extends": "./tsconfig.base.json", "compilerOptions": { "plugins": [
+ { "name": "@effect/language-service", "diagnosticSeverity": { "importFromBarrel": "error" } }
+ ] } }`,
+ }, "tsconfig.json")
+ if len(diags) != 0 {
+ t.Fatalf("expected no diagnostics, got %v", codesOf(diags))
+ }
+ })
+
+ t.Run("a child silences a name declared in a base it does not own", func(t *testing.T) {
+ t.Parallel()
+ diags := configDiagnostics(t, map[string]string{
+ "tsconfig.base.json": pluginConfig(`"diagnosticSeverity": { "importFromBarrel": "error" }`),
+ "tsconfig.json": `{ "extends": "./tsconfig.base.json", "compilerOptions": { "plugins": [
+ { "name": "@effect/language-service", "diagnosticSeverity": { "unknownRuleName": "off" } }
+ ] } }`,
+ }, "tsconfig.json")
+ if len(diags) != 0 {
+ t.Fatalf("expected no diagnostics, got %v", codesOf(diags))
+ }
+ })
+
+ t.Run("a child raises the severity of a name declared in a base", func(t *testing.T) {
+ t.Parallel()
+ diags := configDiagnostics(t, map[string]string{
+ "tsconfig.base.json": pluginConfig(`"diagnosticSeverity": { "importFromBarrel": "error" }`),
+ "tsconfig.json": `{ "extends": "./tsconfig.base.json", "compilerOptions": { "plugins": [
+ { "name": "@effect/language-service", "diagnosticSeverity": { "unknownRuleName": "error" } }
+ ] } }`,
+ }, "tsconfig.json")
+ if len(diags) != 1 {
+ t.Fatalf("expected 1 diagnostic, got %v", codesOf(diags))
+ }
+ if got := diags[0].Category(); got != tsdiag.CategoryError {
+ t.Errorf("category = %v, want error", got)
+ }
+ if file := diags[0].File(); file == nil || tspath.GetBaseFileName(file.FileName()) != "tsconfig.base.json" {
+ t.Error("diagnostic should stay anchored on the config that declares the name")
+ }
+ })
+}
+
+// FinalizeDiagnostics is handed the whole config-error slice and returns a
+// replacement, so it holds drop authority over diagnostics it does not own. This
+// asserts on the unfiltered slice, because the Effect-only filter used elsewhere
+// would hide exactly the regression this guards: a lost ownership predicate
+// silently dropping every other config error in the file.
+func TestFinalizeLeavesForeignDiagnosticsAlone(t *testing.T) {
+ t.Parallel()
+
+ files := map[string]string{
+ "tsconfig.json": `{
+ "compilerOptions": {
+ "bogusOption": true,
+ "plugins": [
+ {
+ "name": "@effect/language-service",
+ "diagnosticSeverity": { "importFromBarrel": "error", "unknownRuleName": "off" }
+ }
+ ]
+ }
+}`,
+ }
+
+ // "off" takes FinalizeDiagnostics down its drop arm, which is the arm that
+ // rebuilds the slice and so the only one that can lose a foreign diagnostic.
+ diags := allConfigDiagnostics(t, files, "tsconfig.json")
+
+ var foundUnknownOption bool
+ for _, diag := range diags {
+ if rule.IsEffectCode(diag.Code()) {
+ t.Errorf("expected every Effect diagnostic to be dropped, got %d", diag.Code())
+ }
+ if diag.Code() == 5023 {
+ foundUnknownOption = true
+ }
+ }
+ if !foundUnknownOption {
+ t.Fatalf("TS5023 was dropped by FinalizeDiagnostics; got %v", codesOf(diags))
+ }
+}
diff --git a/internal/effecttest/runner.go b/internal/effecttest/runner.go
index 56830105..0bf8180f 100644
--- a/internal/effecttest/runner.go
+++ b/internal/effecttest/runner.go
@@ -311,6 +311,11 @@ func RunEffectTest(t *testing.T, version bundledeffect.EffectVersion, testFile s
// Get diagnostics
ctx := context.Background()
var diagnostics []*ast.Diagnostic
+ if parsedConfig != nil {
+ // Config-file diagnostics are reported against the tsconfig rather than a
+ // source file, so they never reach the per-file collections below.
+ diagnostics = append(diagnostics, parsedConfig.Errors...)
+ }
diagnostics = append(diagnostics, program.GetProgramDiagnostics()...)
diagnostics = append(diagnostics, program.GetSyntacticDiagnostics(ctx, nil)...)
diagnostics = append(diagnostics, program.GetSemanticDiagnostics(ctx, nil)...)
diff --git a/internal/rule/rule.go b/internal/rule/rule.go
index a135bb5a..2ee8c83b 100644
--- a/internal/rule/rule.go
+++ b/internal/rule/rule.go
@@ -37,6 +37,21 @@ type Rule struct {
Run func(ctx *Context) []*ast.Diagnostic
}
+// UnusedDirectiveName configures the diagnostic reported for an
+// @effect-diagnostics directive that suppresses nothing. It is a diagnosticSeverity
+// key but not a per-file rule, so it is absent from the rule registry.
+const UnusedDirectiveName = "unusedDirective"
+
+// UnknownRuleNameName configures the diagnostic reported for a diagnosticSeverity
+// entry naming a rule this build does not provide. Like UnusedDirectiveName it is a
+// diagnosticSeverity key but not a per-file rule.
+const UnknownRuleNameName = "unknownRuleName"
+
+// NonRuleConfigurableNames lists every diagnosticSeverity key that configures a
+// diagnostic which is not a rule in the registry. Validation of configured names
+// must accept these in addition to the registry.
+var NonRuleConfigurableNames = []string{UnusedDirectiveName, UnknownRuleNameName}
+
// ByName finds a rule by name in a slice. Returns nil if not found.
func ByName(rules []Rule, name string) *Rule {
for i := range rules {
diff --git a/internal/rulerunner/diagnostics.go b/internal/rulerunner/diagnostics.go
index caa39c86..461887a9 100644
--- a/internal/rulerunner/diagnostics.go
+++ b/internal/rulerunner/diagnostics.go
@@ -229,7 +229,7 @@ func createTransformedDiagnostic(original *ast.Diagnostic, newCategory tsdiag.Ca
}
func unusedDirectiveDiagnostics(sf *ast.SourceFile, allDirectives []directives.Directive, directiveSet *directives.DirectiveSet, resolvedSeverity map[string]etscore.Severity) []*ast.Diagnostic {
- severity, ok := severityFromMap(resolvedSeverity, "unusedDirective")
+ severity, ok := severityFromMap(resolvedSeverity, rule.UnusedDirectiveName)
if !ok {
severity = etscore.SeverityWarning
}
diff --git a/shim/diagnostics/shim.go b/shim/diagnostics/shim.go
index b9d03044..948479ed 100644
--- a/shim/diagnostics/shim.go
+++ b/shim/diagnostics/shim.go
@@ -2061,6 +2061,8 @@ var Unexpected_token_expected = diagnostics.Unexpected_token_expected
var Unicode_escape_sequence_cannot_appear_here = diagnostics.Unicode_escape_sequence_cannot_appear_here
var Unicode_escape_sequences_are_only_available_when_the_Unicode_u_flag_or_the_Unicode_Sets_v_flag_is_set = diagnostics.Unicode_escape_sequences_are_only_available_when_the_Unicode_u_flag_or_the_Unicode_Sets_v_flag_is_set
var Unicode_property_value_expressions_are_only_available_when_the_Unicode_u_flag_or_the_Unicode_Sets_v_flag_is_set = diagnostics.Unicode_property_value_expressions_are_only_available_when_the_Unicode_u_flag_or_the_Unicode_Sets_v_flag_is_set
+var Unknown_Effect_diagnostic_rule_0_in_diagnosticSeverity_Did_you_mean_1_effect_unknownRuleName = diagnostics.Unknown_Effect_diagnostic_rule_0_in_diagnosticSeverity_Did_you_mean_1_effect_unknownRuleName
+var Unknown_Effect_diagnostic_rule_0_in_diagnosticSeverity_This_version_of_effect_Slashtsgo_does_not_provide_it_so_this_entry_has_no_effect_effect_unknownRuleName = diagnostics.Unknown_Effect_diagnostic_rule_0_in_diagnosticSeverity_This_version_of_effect_Slashtsgo_does_not_provide_it_so_this_entry_has_no_effect_effect_unknownRuleName
var Unknown_Unicode_property_name = diagnostics.Unknown_Unicode_property_name
var Unknown_Unicode_property_name_or_value = diagnostics.Unknown_Unicode_property_name_or_value
var Unknown_Unicode_property_value = diagnostics.Unknown_Unicode_property_value
diff --git a/shim/tsoptions/shim.go b/shim/tsoptions/shim.go
index f8405393..1ca4c69a 100644
--- a/shim/tsoptions/shim.go
+++ b/shim/tsoptions/shim.go
@@ -49,6 +49,7 @@ func CreateDiagnosticForNodeInSourceFileOrCompilerDiagnostic(sourceFile *ast.Sou
type DidYouMeanOptionsDiagnostics = tsoptions.DidYouMeanOptionsDiagnostics
type ExtendedConfigCache = tsoptions.ExtendedConfigCache
type ExtendedConfigCacheEntry = tsoptions.ExtendedConfigCacheEntry
+var FinalizeEffectPluginDiagnosticsCallback = tsoptions.FinalizeEffectPluginDiagnosticsCallback
//go:linkname ForEachCompilerOptionValue github.com/microsoft/TypeScript/tsc/internal/tsoptions.ForEachCompilerOptionValue
func ForEachCompilerOptionValue(options *core.CompilerOptions, declFilter func(*tsoptions.CommandLineOption) bool, fn func(option *tsoptions.CommandLineOption, value reflect.Value, i int) bool) bool
//go:linkname GetCallbackForFindingPropertyAssignmentByValue github.com/microsoft/TypeScript/tsc/internal/tsoptions.GetCallbackForFindingPropertyAssignmentByValue
@@ -116,12 +117,17 @@ func ParseWatchOptions(key string, value any, allOptions *core.WatchOptions) []*
type ParsedBuildCommandLine = tsoptions.ParsedBuildCommandLine
type ParsedCommandLine = tsoptions.ParsedCommandLine
type ParsedOptions = tsoptions.ParsedOptions
+//go:linkname RegisterFinalizeEffectPluginDiagnosticsCallback github.com/microsoft/TypeScript/tsc/internal/tsoptions.RegisterFinalizeEffectPluginDiagnosticsCallback
+func RegisterFinalizeEffectPluginDiagnosticsCallback(cb func(diagnostics []*ast.Diagnostic, options *core.CompilerOptions) []*ast.Diagnostic)
//go:linkname RegisterMergeCompilerOptionsCallback github.com/microsoft/TypeScript/tsc/internal/tsoptions.RegisterMergeCompilerOptionsCallback
func RegisterMergeCompilerOptionsCallback(cb func(targetOptions *core.CompilerOptions, sourceOptions *core.CompilerOptions, rawSource any, sourceConfigPath string, basePath string))
+//go:linkname RegisterValidateEffectPluginOptionsCallback github.com/microsoft/TypeScript/tsc/internal/tsoptions.RegisterValidateEffectPluginOptionsCallback
+func RegisterValidateEffectPluginOptionsCallback(cb func(sourceFile *ast.SourceFile, pluginsNode *ast.Node) []*ast.Diagnostic)
type SourceOutputAndProjectReference = tsoptions.SourceOutputAndProjectReference
type TSConfig = tsoptions.TSConfig
//go:linkname TargetToLibMap github.com/microsoft/TypeScript/tsc/internal/tsoptions.TargetToLibMap
func TargetToLibMap() map[core.ScriptTarget]string
type TsConfigSourceFile = tsoptions.TsConfigSourceFile
var TscBuildOption = tsoptions.TscBuildOption
+var ValidateEffectPluginOptionsCallback = tsoptions.ValidateEffectPluginOptionsCallback
var WatchNameMap = tsoptions.WatchNameMap
diff --git a/testdata/baselines/reference/effect-v4/unknownRuleName.errors.txt b/testdata/baselines/reference/effect-v4/unknownRuleName.errors.txt
new file mode 100644
index 00000000..8f5e4d63
--- /dev/null
+++ b/testdata/baselines/reference/effect-v4/unknownRuleName.errors.txt
@@ -0,0 +1,35 @@
+=== Metadata ===
+Effect version: 4.0.0
+
+/.src/tsconfig.json(8,11): warning TS377134: Unknown Effect diagnostic rule `importFromBarrel` in `diagnosticSeverity`. This version of @effect/tsgo does not provide it, so this entry has no effect. effect(unknownRuleName)
+/.src/tsconfig.json(9,11): warning TS377134: Unknown Effect diagnostic rule `outdatedEffectCodegen` in `diagnosticSeverity`. This version of @effect/tsgo does not provide it, so this entry has no effect. effect(unknownRuleName)
+
+
+==== /.src/tsconfig.json (2 errors) ====
+ {
+ "compilerOptions": {
+ "plugins": [
+ {
+ "name": "@effect/language-service",
+ "diagnosticSeverity": {
+ "floatingEffect": "error",
+ "importFromBarrel": "error",
+ ~~~~~~~~~~~~~~~~~~
+!!! warning TS377134: Unknown Effect diagnostic rule `importFromBarrel` in `diagnosticSeverity`. This version of @effect/tsgo does not provide it, so this entry has no effect. effect(unknownRuleName)
+ "outdatedEffectCodegen": "error"
+ ~~~~~~~~~~~~~~~~~~~~~~~
+!!! warning TS377134: Unknown Effect diagnostic rule `outdatedEffectCodegen` in `diagnosticSeverity`. This version of @effect/tsgo does not provide it, so this entry has no effect. effect(unknownRuleName)
+ }
+ }
+ ]
+ }
+ }
+
+
+==== /.src/test.ts (0 errors) ====
+ import { Effect } from "effect"
+
+ // The rule names above that this build does not provide are reported on the
+ // tsconfig; floatingEffect is provided, so it is not.
+ export const program = Effect.succeed(1)
+
diff --git a/testdata/baselines/reference/effect-v4/unknownRuleName.flows.test.mermaid b/testdata/baselines/reference/effect-v4/unknownRuleName.flows.test.mermaid
new file mode 100644
index 00000000..e5c22a8b
--- /dev/null
+++ b/testdata/baselines/reference/effect-v4/unknownRuleName.flows.test.mermaid
@@ -0,0 +1,6 @@
+flowchart TB
+ 0[/"type: 1
node: 1"/]
+ 1["type: Effect#lt;number, never, never#gt;
callee: Effect.succeed
args: #91;#93;"]
+ 2[/"type: #lt;A#gt;#40;value: A#41; =#gt; Effect#lt;A, never, never#gt;
node: Effect.succeed"/]
+ 0 -->|"kind: pipe"| 1
+ 2 -->|"kind: transformCallee"| 1
\ No newline at end of file
diff --git a/testdata/baselines/reference/effect-v4/unknownRuleName.flows.txt b/testdata/baselines/reference/effect-v4/unknownRuleName.flows.txt
new file mode 100644
index 00000000..14e30c5d
--- /dev/null
+++ b/testdata/baselines/reference/effect-v4/unknownRuleName.flows.txt
@@ -0,0 +1 @@
+/.src/test.ts -> unknownRuleName.flows.test.mermaid
diff --git a/testdata/baselines/reference/effect-v4/unknownRuleName.layers.txt b/testdata/baselines/reference/effect-v4/unknownRuleName.layers.txt
new file mode 100644
index 00000000..39e24226
--- /dev/null
+++ b/testdata/baselines/reference/effect-v4/unknownRuleName.layers.txt
@@ -0,0 +1 @@
+==== /.src/test.ts (0 layer exports) ====
diff --git a/testdata/baselines/reference/effect-v4/unknownRuleName.pipings.txt b/testdata/baselines/reference/effect-v4/unknownRuleName.pipings.txt
new file mode 100644
index 00000000..35c1234e
--- /dev/null
+++ b/testdata/baselines/reference/effect-v4/unknownRuleName.pipings.txt
@@ -0,0 +1,15 @@
+==== /.src/test.ts (1 flows) ====
+
+=== Piping Flow ===
+Location: 5:23 - 5:41
+Node: Effect.succeed(1)
+Node Kind: KindCallExpression
+
+Subject: 1
+Subject Type: 1
+
+Transformations (1):
+ [0] kind: call
+ callee: Effect.succeed
+ args: (constant)
+ outType: Effect
diff --git a/testdata/baselines/reference/effect-v4/unknownRuleName.quickfixes.txt b/testdata/baselines/reference/effect-v4/unknownRuleName.quickfixes.txt
new file mode 100644
index 00000000..dad05bfc
--- /dev/null
+++ b/testdata/baselines/reference/effect-v4/unknownRuleName.quickfixes.txt
@@ -0,0 +1,5 @@
+=== Quick Fix Inventory ===
+(no diagnostics)
+
+=== Quick Fix Application Results ===
+(no quick fixes to apply)
diff --git a/testdata/baselines/reference/effect-v4/unknownRuleName_overrides.errors.txt b/testdata/baselines/reference/effect-v4/unknownRuleName_overrides.errors.txt
new file mode 100644
index 00000000..77a25131
--- /dev/null
+++ b/testdata/baselines/reference/effect-v4/unknownRuleName_overrides.errors.txt
@@ -0,0 +1,37 @@
+=== Metadata ===
+Effect version: 4.0.0
+
+/.src/tsconfig.json(11,17): warning TS377135: Unknown Effect diagnostic rule `floatingEfect` in `diagnosticSeverity`. Did you mean `floatingEffect`? effect(unknownRuleName)
+
+
+==== /.src/tsconfig.json (1 errors) ====
+ {
+ "compilerOptions": {
+ "plugins": [
+ {
+ "name": "@effect/language-service",
+ "overrides": [
+ {
+ "include": ["**/*.ts"],
+ "options": {
+ "diagnosticSeverity": {
+ "floatingEfect": "error"
+ ~~~~~~~~~~~~~~~
+!!! warning TS377135: Unknown Effect diagnostic rule `floatingEfect` in `diagnosticSeverity`. Did you mean `floatingEffect`? effect(unknownRuleName)
+ }
+ }
+ }
+ ]
+ }
+ ]
+ }
+ }
+
+
+==== /.src/test.ts (0 errors) ====
+ import { Effect } from "effect"
+
+ // diagnosticSeverity inside an overrides entry is checked the same way, and a
+ // near miss carries the intended name.
+ export const program = Effect.succeed(1)
+
diff --git a/testdata/baselines/reference/effect-v4/unknownRuleName_overrides.flows.test.mermaid b/testdata/baselines/reference/effect-v4/unknownRuleName_overrides.flows.test.mermaid
new file mode 100644
index 00000000..e5c22a8b
--- /dev/null
+++ b/testdata/baselines/reference/effect-v4/unknownRuleName_overrides.flows.test.mermaid
@@ -0,0 +1,6 @@
+flowchart TB
+ 0[/"type: 1
node: 1"/]
+ 1["type: Effect#lt;number, never, never#gt;
callee: Effect.succeed
args: #91;#93;"]
+ 2[/"type: #lt;A#gt;#40;value: A#41; =#gt; Effect#lt;A, never, never#gt;
node: Effect.succeed"/]
+ 0 -->|"kind: pipe"| 1
+ 2 -->|"kind: transformCallee"| 1
\ No newline at end of file
diff --git a/testdata/baselines/reference/effect-v4/unknownRuleName_overrides.flows.txt b/testdata/baselines/reference/effect-v4/unknownRuleName_overrides.flows.txt
new file mode 100644
index 00000000..e745e92f
--- /dev/null
+++ b/testdata/baselines/reference/effect-v4/unknownRuleName_overrides.flows.txt
@@ -0,0 +1 @@
+/.src/test.ts -> unknownRuleName_overrides.flows.test.mermaid
diff --git a/testdata/baselines/reference/effect-v4/unknownRuleName_overrides.layers.txt b/testdata/baselines/reference/effect-v4/unknownRuleName_overrides.layers.txt
new file mode 100644
index 00000000..39e24226
--- /dev/null
+++ b/testdata/baselines/reference/effect-v4/unknownRuleName_overrides.layers.txt
@@ -0,0 +1 @@
+==== /.src/test.ts (0 layer exports) ====
diff --git a/testdata/baselines/reference/effect-v4/unknownRuleName_overrides.pipings.txt b/testdata/baselines/reference/effect-v4/unknownRuleName_overrides.pipings.txt
new file mode 100644
index 00000000..35c1234e
--- /dev/null
+++ b/testdata/baselines/reference/effect-v4/unknownRuleName_overrides.pipings.txt
@@ -0,0 +1,15 @@
+==== /.src/test.ts (1 flows) ====
+
+=== Piping Flow ===
+Location: 5:23 - 5:41
+Node: Effect.succeed(1)
+Node Kind: KindCallExpression
+
+Subject: 1
+Subject Type: 1
+
+Transformations (1):
+ [0] kind: call
+ callee: Effect.succeed
+ args: (constant)
+ outType: Effect
diff --git a/testdata/baselines/reference/effect-v4/unknownRuleName_overrides.quickfixes.txt b/testdata/baselines/reference/effect-v4/unknownRuleName_overrides.quickfixes.txt
new file mode 100644
index 00000000..dad05bfc
--- /dev/null
+++ b/testdata/baselines/reference/effect-v4/unknownRuleName_overrides.quickfixes.txt
@@ -0,0 +1,5 @@
+=== Quick Fix Inventory ===
+(no diagnostics)
+
+=== Quick Fix Application Results ===
+(no quick fixes to apply)
diff --git a/testdata/baselines/reference/effect-v4/unknownRuleName_valid.errors.txt b/testdata/baselines/reference/effect-v4/unknownRuleName_valid.errors.txt
new file mode 100644
index 00000000..255f86d0
--- /dev/null
+++ b/testdata/baselines/reference/effect-v4/unknownRuleName_valid.errors.txt
@@ -0,0 +1,28 @@
+=== Metadata ===
+Effect version: 4.0.0
+
+
+
+==== /.src/tsconfig.json (0 errors) ====
+ {
+ "compilerOptions": {
+ "plugins": [
+ {
+ "name": "@effect/language-service",
+ "diagnosticSeverity": {
+ "floatingEffect": "error",
+ "unusedDirective": "warning",
+ "unknownRuleName": "warning"
+ }
+ }
+ ]
+ }
+ }
+
+
+==== /.src/test.ts (0 errors) ====
+ import { Effect } from "effect"
+
+ // Every configured name resolves, so the configuration is reported clean.
+ export const program = Effect.succeed(1)
+
diff --git a/testdata/baselines/reference/effect-v4/unknownRuleName_valid.flows.test.mermaid b/testdata/baselines/reference/effect-v4/unknownRuleName_valid.flows.test.mermaid
new file mode 100644
index 00000000..e5c22a8b
--- /dev/null
+++ b/testdata/baselines/reference/effect-v4/unknownRuleName_valid.flows.test.mermaid
@@ -0,0 +1,6 @@
+flowchart TB
+ 0[/"type: 1
node: 1"/]
+ 1["type: Effect#lt;number, never, never#gt;
callee: Effect.succeed
args: #91;#93;"]
+ 2[/"type: #lt;A#gt;#40;value: A#41; =#gt; Effect#lt;A, never, never#gt;
node: Effect.succeed"/]
+ 0 -->|"kind: pipe"| 1
+ 2 -->|"kind: transformCallee"| 1
\ No newline at end of file
diff --git a/testdata/baselines/reference/effect-v4/unknownRuleName_valid.flows.txt b/testdata/baselines/reference/effect-v4/unknownRuleName_valid.flows.txt
new file mode 100644
index 00000000..3c33dd85
--- /dev/null
+++ b/testdata/baselines/reference/effect-v4/unknownRuleName_valid.flows.txt
@@ -0,0 +1 @@
+/.src/test.ts -> unknownRuleName_valid.flows.test.mermaid
diff --git a/testdata/baselines/reference/effect-v4/unknownRuleName_valid.layers.txt b/testdata/baselines/reference/effect-v4/unknownRuleName_valid.layers.txt
new file mode 100644
index 00000000..39e24226
--- /dev/null
+++ b/testdata/baselines/reference/effect-v4/unknownRuleName_valid.layers.txt
@@ -0,0 +1 @@
+==== /.src/test.ts (0 layer exports) ====
diff --git a/testdata/baselines/reference/effect-v4/unknownRuleName_valid.pipings.txt b/testdata/baselines/reference/effect-v4/unknownRuleName_valid.pipings.txt
new file mode 100644
index 00000000..f6f2a37e
--- /dev/null
+++ b/testdata/baselines/reference/effect-v4/unknownRuleName_valid.pipings.txt
@@ -0,0 +1,15 @@
+==== /.src/test.ts (1 flows) ====
+
+=== Piping Flow ===
+Location: 4:23 - 4:41
+Node: Effect.succeed(1)
+Node Kind: KindCallExpression
+
+Subject: 1
+Subject Type: 1
+
+Transformations (1):
+ [0] kind: call
+ callee: Effect.succeed
+ args: (constant)
+ outType: Effect
diff --git a/testdata/baselines/reference/effect-v4/unknownRuleName_valid.quickfixes.txt b/testdata/baselines/reference/effect-v4/unknownRuleName_valid.quickfixes.txt
new file mode 100644
index 00000000..dad05bfc
--- /dev/null
+++ b/testdata/baselines/reference/effect-v4/unknownRuleName_valid.quickfixes.txt
@@ -0,0 +1,5 @@
+=== Quick Fix Inventory ===
+(no diagnostics)
+
+=== Quick Fix Application Results ===
+(no quick fixes to apply)
diff --git a/testdata/tests/effect-v4/unknownRuleName.ts b/testdata/tests/effect-v4/unknownRuleName.ts
new file mode 100644
index 00000000..ca26e21a
--- /dev/null
+++ b/testdata/tests/effect-v4/unknownRuleName.ts
@@ -0,0 +1,22 @@
+// @filename: tsconfig.json
+{
+ "compilerOptions": {
+ "plugins": [
+ {
+ "name": "@effect/language-service",
+ "diagnosticSeverity": {
+ "floatingEffect": "error",
+ "importFromBarrel": "error",
+ "outdatedEffectCodegen": "error"
+ }
+ }
+ ]
+ }
+}
+
+// @filename: test.ts
+import { Effect } from "effect"
+
+// The rule names above that this build does not provide are reported on the
+// tsconfig; floatingEffect is provided, so it is not.
+export const program = Effect.succeed(1)
diff --git a/testdata/tests/effect-v4/unknownRuleName_overrides.ts b/testdata/tests/effect-v4/unknownRuleName_overrides.ts
new file mode 100644
index 00000000..8e7ec8ac
--- /dev/null
+++ b/testdata/tests/effect-v4/unknownRuleName_overrides.ts
@@ -0,0 +1,27 @@
+// @filename: tsconfig.json
+{
+ "compilerOptions": {
+ "plugins": [
+ {
+ "name": "@effect/language-service",
+ "overrides": [
+ {
+ "include": ["**/*.ts"],
+ "options": {
+ "diagnosticSeverity": {
+ "floatingEfect": "error"
+ }
+ }
+ }
+ ]
+ }
+ ]
+ }
+}
+
+// @filename: test.ts
+import { Effect } from "effect"
+
+// diagnosticSeverity inside an overrides entry is checked the same way, and a
+// near miss carries the intended name.
+export const program = Effect.succeed(1)
diff --git a/testdata/tests/effect-v4/unknownRuleName_valid.ts b/testdata/tests/effect-v4/unknownRuleName_valid.ts
new file mode 100644
index 00000000..af56d754
--- /dev/null
+++ b/testdata/tests/effect-v4/unknownRuleName_valid.ts
@@ -0,0 +1,21 @@
+// @filename: tsconfig.json
+{
+ "compilerOptions": {
+ "plugins": [
+ {
+ "name": "@effect/language-service",
+ "diagnosticSeverity": {
+ "floatingEffect": "error",
+ "unusedDirective": "warning",
+ "unknownRuleName": "warning"
+ }
+ }
+ ]
+ }
+}
+
+// @filename: test.ts
+import { Effect } from "effect"
+
+// Every configured name resolves, so the configuration is reported clean.
+export const program = Effect.succeed(1)