Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/unknown-diagnostic-rule.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@effect/tsgo": minor
---

Warn when `diagnosticSeverity` contains an unknown Effect rule name, including in overrides and inherited configurations. For example, `"floatingEfect": "error"` now reports `effect(unknownRuleName)` instead of being silently ignored.

The check uses the fully merged configuration. Set `"unknownRuleName": "off"` to disable it or `"unknownRuleName": "error"` to raise its severity. Local keys are underlined in tsconfig; inherited keys without local syntax produce a diagnostic without a source location.
29 changes: 29 additions & 0 deletions _patches/typescript-go/030-tsoptions-validation.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
diff --git a/internal/tsoptions/tsconfigparsing.go b/internal/tsoptions/tsconfigparsing.go
--- a/internal/tsoptions/tsconfigparsing.go
+++ b/internal/tsoptions/tsconfigparsing.go
@@ -19,6 +19,14 @@
"github.com/microsoft/typescript-go/internal/vfs"
"github.com/microsoft/typescript-go/internal/vfs/vfsmatch"
)
+
+// ValidateCompilerOptionsCallback validates the final options after configuration
+// inheritance and existing options have been merged.
+var ValidateCompilerOptionsCallback func(*core.CompilerOptions, *ast.SourceFile) []*ast.Diagnostic
+
+func RegisterValidateCompilerOptionsCallback(cb func(*core.CompilerOptions, *ast.SourceFile) []*ast.Diagnostic) {
+ ValidateCompilerOptionsCallback = cb
+}

type extendsResult struct {
options *core.CompilerOptions
@@ -1367,6 +1375,10 @@
return projectReferences
}

+ if ValidateCompilerOptionsCallback != nil {
+ errors = append(errors, ValidateCompilerOptionsCallback(parsedConfig.options, tsconfigToSourceFile(sourceFile))...)
+ }
+
fileNames, literalFileNamesLen := getFileNames(basePathForFileNames)
return &ParsedCommandLine{
ParsedConfig: &core.ParsedOptions{
29 changes: 29 additions & 0 deletions _patches/typescript/030-tsoptions-validation.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
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
@@ -22,6 +22,14 @@
"github.com/microsoft/TypeScript/tsc/internal/vfs"
"github.com/microsoft/TypeScript/tsc/internal/vfs/vfsmatch"
)
+
+// ValidateCompilerOptionsCallback validates the final options after configuration
+// inheritance and existing options have been merged.
+var ValidateCompilerOptionsCallback func(*core.CompilerOptions, *ast.SourceFile) []*ast.Diagnostic
+
+func RegisterValidateCompilerOptionsCallback(cb func(*core.CompilerOptions, *ast.SourceFile) []*ast.Diagnostic) {
+ ValidateCompilerOptionsCallback = cb
+}

type extendsResult struct {
options *core.CompilerOptions
@@ -1508,6 +1516,10 @@
return projectReferences
}

+ if ValidateCompilerOptionsCallback != nil {
+ errors = append(errors, ValidateCompilerOptionsCallback(parsedConfig.options, tsconfigToSourceFile(sourceFile))...)
+ }
+
fileNames, literalFileNamesLen := getFileNames(basePathForFileNames)
compileOnSave := new(false)
if raw, ok := parsedConfig.raw.(*collections.OrderedMap[string, any]); ok {
12 changes: 12 additions & 0 deletions _tools/gen_shims/config/tsoptions/foreach.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package tsoptions

import (
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/tsoptions"
)

// ForEachTsConfigPropArray forwards through a regular call because go:linkname
// does not support generic functions.
func ForEachTsConfigPropArray[T any](sourceFile *ast.SourceFile, propKey string, callback func(*ast.PropertyAssignment) *T) *T {
return tsoptions.ForEachTsConfigPropArray(sourceFile, propKey, callback)
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,32 @@ import (
"github.com/microsoft/typescript-go/internal/tspath"
)

func ParseJsonConfigFileContent(
json any,
host tsoptions.ParseConfigHost,
basePath string,
existingOptions *core.CompilerOptions,
configFileName string,
resolutionStack []tspath.Path,
extraFileExtensions any,
extendedConfigCache tsoptions.ExtendedConfigCache,
) *tsoptions.ParsedCommandLine {
var extensions []tsoptions.FileExtensionInfo
if extraFileExtensions != nil {
extensions = extraFileExtensions.([]tsoptions.FileExtensionInfo)
}
return tsoptions.ParseJsonConfigFileContent(
json,
host,
basePath,
existingOptions,
configFileName,
resolutionStack,
extensions,
extendedConfigCache,
)
}

func ParseJsonSourceFileConfigFileContent(
sourceFile *tsoptions.TsConfigSourceFile,
host tsoptions.ParseConfigHost,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"IgnoreFunctions": ["ParseJsonSourceFileConfigFileContent"]
"IgnoreFunctions": ["ParseJsonSourceFileConfigFileContent", "ParseJsonConfigFileContent"]
}
21 changes: 21 additions & 0 deletions _tools/gen_shims/providers/typescript/tsoptions/compatibility.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,27 @@ import (
"github.com/microsoft/typescript-go/internal/tspath"
)

func ParseJsonConfigFileContent(
json any,
host tsoptions.ParseConfigHost,
basePath string,
existingOptions *core.CompilerOptions,
configFileName string,
resolutionStack []tspath.Path,
_ any,
extendedConfigCache tsoptions.ExtendedConfigCache,
) *tsoptions.ParsedCommandLine {
return tsoptions.ParseJsonConfigFileContent(
json,
host,
basePath,
existingOptions,
configFileName,
resolutionStack,
extendedConfigCache,
)
}

func ParseJsonSourceFileConfigFileContent(
sourceFile *tsoptions.TsConfigSourceFile,
host tsoptions.ParseConfigHost,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"IgnoreFunctions": ["ParseJsonSourceFileConfigFileContent"]
"IgnoreFunctions": ["ParseJsonSourceFileConfigFileContent", "ParseJsonConfigFileContent"]
}
2 changes: 2 additions & 0 deletions etscheckerhooks/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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)
}
Expand Down
4 changes: 4 additions & 0 deletions internal/diagnostics/effectDiagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -526,5 +526,9 @@
"`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`. effect(unknownRuleName)": {
"category": "Warning",
"code": 377134
}
}
123 changes: 123 additions & 0 deletions internal/effectconfigcheck/validation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Package effectconfigcheck validates resolved Effect compiler options.
package effectconfigcheck

import (
"slices"

"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"
"github.com/microsoft/TypeScript/tsc/shim/diagnostics"
"github.com/microsoft/TypeScript/tsc/shim/tsoptions"
)

func Register() {
tsoptions.RegisterValidateCompilerOptionsCallback(validate)
}

func validate(options *core.CompilerOptions, sourceFile *ast.SourceFile) []*ast.Diagnostic {
if options == nil || !etscore.DiagnosticsEnabled(options.Effect) {
return nil
}
config := options.Effect
severity, configured := config.DiagnosticSeverity[rule.UnknownRuleNameName]
if !configured {
severity = etscore.SeverityWarning
}
if severity.IsOff() {
return nil
}

// Syntax is used only to locate diagnostics; validation uses the merged options.
plugin := effectPluginSyntax(sourceFile)
var result []*ast.Diagnostic
check := func(severities map[string]etscore.Severity, syntax *ast.Node) {
var unknown []string
for name := range severities {
if name != rule.UnusedDirectiveName && name != rule.UnknownRuleNameName && rule.ByName(rules.All, name) == nil {
unknown = append(unknown, name)
}
}
// Maps have no iteration order; keep CLI output and baselines deterministic.
slices.Sort(unknown)
for _, name := range unknown {
var node *ast.Node
if property := findProperty(syntax, name); property != nil {
node = property.Name()
}
diagnostic := tsoptions.CreateDiagnosticForNodeInSourceFileOrCompilerDiagnostic(
sourceFile, node,
diagnostics.Unknown_Effect_diagnostic_rule_0_in_diagnosticSeverity_effect_unknownRuleName,
name,
)
diagnostic.SetCategory(directives.ToCategory(severity))
result = append(result, diagnostic)
}
}
check(config.DiagnosticSeverity, propertyValue(plugin, "diagnosticSeverity"))

// Inherited overrides precede local overrides. Only local object entries have
// syntax in this file, and the parser skips non-object entries.
var localOverrides []*ast.Node
for _, node := range arrayElements(propertyValue(plugin, "overrides")) {
if ast.IsObjectLiteralExpression(node) {
localOverrides = append(localOverrides, node)
}
}
localStart := len(config.Overrides) - len(localOverrides)
for i, override := range config.Overrides {
var syntax *ast.Node
if localStart >= 0 && i >= localStart {
syntax = propertyValue(propertyValue(localOverrides[i-localStart], "options"), "diagnosticSeverity")
}
check(override.Options.DiagnosticSeverity, syntax)
}
return result
}

func effectPluginSyntax(sourceFile *ast.SourceFile) *ast.Node {
compilerOptions := tsoptions.ForEachTsConfigPropArray(sourceFile, "compilerOptions", func(property *ast.PropertyAssignment) *ast.PropertyAssignment {
return property
})
if compilerOptions == nil {
return nil
}
for _, plugin := range arrayElements(propertyValue(compilerOptions.Initializer, "plugins")) {
name := propertyValue(plugin, "name")
if name != nil && ast.IsStringLiteralLike(name) && name.Text() == etscore.EffectPluginName {
return plugin
}
}
return nil
}

func findProperty(node *ast.Node, name string) *ast.PropertyAssignment {
if node == nil || !ast.IsObjectLiteralExpression(node) {
return nil
}
var result *ast.PropertyAssignment
for _, property := range node.Properties() {
if ast.IsPropertyAssignment(property) && ast.GetTextOfPropertyName(property.Name()) == name {
// JSON parsing retains the last value for duplicate keys.
result = property.AsPropertyAssignment()
}
}
return result
}

func propertyValue(node *ast.Node, name string) *ast.Node {
if property := findProperty(node, name); property != nil {
return property.Initializer
}
return nil
}

func arrayElements(node *ast.Node) []*ast.Node {
if node != nil && ast.IsArrayLiteralExpression(node) {
return node.Elements()
}
return nil
}
Loading