Files
kjol/tools/tsgo/internal/ls/lsutil/userpreferences_test.go
2026-07-09 16:50:43 -04:00

645 lines
18 KiB
Go

package lsutil
import (
"reflect"
"testing"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/json"
"github.com/microsoft/typescript-go/internal/modulespecifiers"
"gotest.tools/v3/assert"
)
func fillNonZeroValues(v reflect.Value) {
t := v.Type()
for i := range t.NumField() {
field := v.Field(i)
if !field.CanSet() {
continue
}
switch field.Kind() {
case reflect.Bool:
field.SetBool(true)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
field.SetInt(1)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
field.SetUint(1)
case reflect.String:
val := getValidStringValue(field.Type())
field.SetString(val)
case reflect.Slice:
if field.Type().Elem().Kind() == reflect.String {
field.Set(reflect.ValueOf([]string{"test"}))
}
case reflect.Struct:
fillNonZeroValues(field)
}
}
}
func getValidStringValue(t reflect.Type) string {
typeName := t.String()
switch typeName {
case "lsutil.QuotePreference":
return string(QuotePreferenceSingle)
case "lsutil.JsxAttributeCompletionStyle":
return string(JsxAttributeCompletionStyleBraces)
case "lsutil.IncludeInlayParameterNameHints":
return string(IncludeInlayParameterNameHintsAll)
case "lsutil.SemicolonPreference":
return string(SemicolonPreferenceInsert)
case "modulespecifiers.ImportModuleSpecifierPreference":
return string(modulespecifiers.ImportModuleSpecifierPreferenceRelative)
case "modulespecifiers.ImportModuleSpecifierEndingPreference":
return string(modulespecifiers.ImportModuleSpecifierEndingPreferenceJs)
default:
return "test"
}
}
func TestUserPreferencesRoundtrip(t *testing.T) {
t.Parallel()
var original UserPreferences
fillNonZeroValues(reflect.ValueOf(&original).Elem())
jsonBytes, err := json.Marshal(&original)
assert.NilError(t, err)
t.Run("UnmarshalJSONFrom", func(t *testing.T) {
t.Parallel()
var parsed UserPreferences
err2 := json.Unmarshal(jsonBytes, &parsed)
assert.NilError(t, err2)
assert.DeepEqual(t, original, parsed)
})
t.Run("withConfig", func(t *testing.T) {
t.Parallel()
var config map[string]any
err2 := json.Unmarshal(jsonBytes, &config)
assert.NilError(t, err2)
parsed := UserPreferences{}.withConfig(config)
assert.DeepEqual(t, original, parsed)
})
}
func TestUserPreferencesSerialize(t *testing.T) {
t.Parallel()
t.Run("config path field serializes to nested path", func(t *testing.T) {
t.Parallel()
prefs := &UserPreferences{
QuotePreference: QuotePreferenceSingle,
}
jsonBytes, err := json.Marshal(prefs)
assert.NilError(t, err)
var actual map[string]any
err = json.Unmarshal(jsonBytes, &actual)
assert.NilError(t, err)
preferences := actual["preferences"].(map[string]any)
assert.Equal(t, "single", preferences["quoteStyle"])
})
t.Run("raw-only field serializes to unstable section", func(t *testing.T) {
t.Parallel()
prefs := &UserPreferences{
DisableSuggestions: core.TSTrue,
}
jsonBytes, err := json.Marshal(prefs)
assert.NilError(t, err)
var actual map[string]any
err = json.Unmarshal(jsonBytes, &actual)
assert.NilError(t, err)
unstable := actual["unstable"].(map[string]any)
assert.Equal(t, true, unstable["disableSuggestions"])
})
t.Run("inlay hint inversion on serialize", func(t *testing.T) {
t.Parallel()
prefs := &UserPreferences{
InlayHints: InlayHintsPreferences{
IncludeInlayParameterNameHints: IncludeInlayParameterNameHintsAll,
IncludeInlayParameterNameHintsWhenArgumentMatchesName: core.TSTrue,
},
}
jsonBytes, err := json.Marshal(prefs)
assert.NilError(t, err)
var actual map[string]any
err = json.Unmarshal(jsonBytes, &actual)
assert.NilError(t, err)
inlayHints := actual["inlayHints"].(map[string]any)
parameterNames := inlayHints["parameterNames"].(map[string]any)
assert.Equal(t, "all", parameterNames["enabled"])
assert.Equal(t, false, parameterNames["suppressWhenArgumentMatchesName"]) // inverted
})
t.Run("mixed config and unstable fields", func(t *testing.T) {
t.Parallel()
prefs := &UserPreferences{
QuotePreference: QuotePreferenceSingle,
DisableSuggestions: core.TSTrue,
DisplayPartsForJSDoc: core.TSTrue,
}
jsonBytes, err := json.Marshal(prefs)
assert.NilError(t, err)
var actual map[string]any
err = json.Unmarshal(jsonBytes, &actual)
assert.NilError(t, err)
preferences := actual["preferences"].(map[string]any)
assert.Equal(t, "single", preferences["quoteStyle"])
unstable := actual["unstable"].(map[string]any)
assert.Equal(t, true, unstable["disableSuggestions"])
assert.Equal(t, true, unstable["displayPartsForJSDoc"])
})
}
func TestUserPreferencesParseUnstable(t *testing.T) {
t.Parallel()
tests := []struct {
name string
json string
expected UserPreferences
}{
{
name: "unstable fields with correct casing",
json: `{
"unstable": {
"disableSuggestions": true,
"maximumHoverLength": 100,
"allowRenameOfImportPath": true
}
}`,
expected: UserPreferences{
DisableSuggestions: core.TSTrue,
MaximumHoverLength: 100,
AllowRenameOfImportPath: core.TSTrue,
},
},
{
name: "nested preferences path",
json: `{
"preferences": {
"quoteStyle": "single",
"useAliasesForRenames": true
}
}`,
expected: UserPreferences{
QuotePreference: QuotePreferenceSingle,
UseAliasesForRename: core.TSTrue,
},
},
{
name: "suggest section",
json: `{
"suggest": {
"autoImports": false,
"includeCompletionsForImportStatements": true
}
}`,
expected: UserPreferences{
IncludeCompletionsForModuleExports: core.TSFalse,
IncludeCompletionsForImportStatements: core.TSTrue,
},
},
{
name: "inlayHints with invert",
json: `{
"inlayHints": {
"parameterNames": {
"enabled": "all",
"suppressWhenArgumentMatchesName": true
}
}
}`,
expected: UserPreferences{
InlayHints: InlayHintsPreferences{
IncludeInlayParameterNameHints: IncludeInlayParameterNameHintsAll,
IncludeInlayParameterNameHintsWhenArgumentMatchesName: core.TSFalse, // inverted
},
},
},
{
name: "mixed config",
json: `{
"unstable": {
"displayPartsForJSDoc": true
},
"preferences": {
"importModuleSpecifier": "relative"
},
"workspaceSymbols": {
"excludeLibrarySymbols": true
}
}`,
expected: UserPreferences{
DisplayPartsForJSDoc: core.TSTrue,
ImportModuleSpecifierPreference: modulespecifiers.ImportModuleSpecifierPreferenceRelative,
ExcludeLibrarySymbolsInNavTo: core.TSTrue,
},
},
{
name: "stable config overrides unstable",
json: `{
"unstable": {
"quotePreference": "double"
},
"preferences": {
"quoteStyle": "single"
}
}`,
expected: UserPreferences{
QuotePreference: QuotePreferenceSingle, // stable wins
},
},
{
name: "unstable sets value when no stable config",
json: `{
"unstable": {
"includeAutomaticOptionalChainCompletions": false
}
}`,
expected: UserPreferences{
IncludeAutomaticOptionalChainCompletions: core.TSFalse,
},
},
{
name: "any field can be passed via unstable by its raw name",
json: `{
"unstable": {
"quotePreference": "double",
"includeCompletionsForModuleExports": true,
"excludeLibrarySymbolsInNavTo": true
}
}`,
expected: UserPreferences{
QuotePreference: QuotePreferenceDouble,
IncludeCompletionsForModuleExports: core.TSTrue,
ExcludeLibrarySymbolsInNavTo: core.TSTrue,
},
},
{
name: "TypeScript raw names work in unstable section",
json: `{
"unstable": {
"includeCompletionsForModuleExports": true,
"quotePreference": "single",
"providePrefixAndSuffixTextForRename": true,
"includeInlayParameterNameHints": "all",
"organizeImportsLocale": "en"
}
}`,
expected: UserPreferences{
IncludeCompletionsForModuleExports: core.TSTrue,
QuotePreference: QuotePreferenceSingle,
UseAliasesForRename: core.TSTrue,
OrganizeImportsLocale: "en",
InlayHints: InlayHintsPreferences{
IncludeInlayParameterNameHints: IncludeInlayParameterNameHintsAll,
},
},
},
{
name: "old raw organize imports unicode preferences load as raw state",
json: `{
"unstable": {
"organizeImportsCollation": "unicode",
"organizeImportsCaseFirst": "upper",
"organizeImportsIgnoreCase": false,
"organizeImportsNumericCollation": true
}
}`,
expected: UserPreferences{
OrganizeImportsCollation: OrganizeImportsCollationUnicode,
OrganizeImportsCaseFirst: OrganizeImportsCaseFirstUpper,
OrganizeImportsIgnoreCase: core.TSFalse,
OrganizeImportsNumericCollation: core.TSTrue,
},
},
{
name: "old top-level raw organize imports unicode preferences load as raw state",
json: `{
"organizeImportsCollation": "unicode",
"organizeImportsIgnoreCase": true
}`,
expected: UserPreferences{
OrganizeImportsCollation: OrganizeImportsCollationUnicode,
OrganizeImportsIgnoreCase: core.TSTrue,
},
},
{
name: "new top-level raw organize imports sort is accepted",
json: `{
"organizeImportsSort": "natural"
}`,
expected: UserPreferences{
OrganizeImportsSort: OrganizeImportsSortNatural,
},
},
{
name: "old raw organize imports ignore case loads as raw state",
json: `{
"unstable": {
"organizeImportsIgnoreCase": true
}
}`,
expected: UserPreferences{
OrganizeImportsIgnoreCase: core.TSTrue,
},
},
{
name: "new raw organize imports sort loads alongside old raw preferences",
json: `{
"unstable": {
"organizeImportsSort": "ordinal",
"organizeImportsCollation": "unicode",
"organizeImportsIgnoreCase": true
}
}`,
expected: UserPreferences{
OrganizeImportsSort: OrganizeImportsSortOrdinal,
OrganizeImportsCollation: OrganizeImportsCollationUnicode,
OrganizeImportsIgnoreCase: core.TSTrue,
},
},
{
name: "old nested organize imports unicode preferences load as raw state",
json: `{
"preferences": {
"organizeImports": {
"unicodeCollation": "unicode",
"caseSensitivity": "caseSensitive",
"numericCollation": true,
"caseFirst": "upper"
}
}
}`,
expected: UserPreferences{
OrganizeImportsCollation: OrganizeImportsCollationUnicode,
OrganizeImportsIgnoreCase: core.TSFalse,
OrganizeImportsNumericCollation: core.TSTrue,
OrganizeImportsCaseFirst: OrganizeImportsCaseFirstUpper,
},
},
{
name: "new nested organize imports sort loads alongside old nested preferences",
json: `{
"preferences": {
"organizeImports": {
"sort": "ordinalIgnoreCase",
"unicodeCollation": "unicode",
"caseSensitivity": "caseSensitive"
}
}
}`,
expected: UserPreferences{
OrganizeImportsSort: OrganizeImportsSortOrdinalIgnoreCase,
OrganizeImportsCollation: OrganizeImportsCollationUnicode,
OrganizeImportsIgnoreCase: core.TSFalse,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var config map[string]any
err := json.Unmarshal([]byte(tt.json), &config)
assert.NilError(t, err)
parsed := UserPreferences{}.withConfig(config)
assert.DeepEqual(t, tt.expected, parsed)
})
}
}
func TestUserPreferencesReportStyleChecksAsWarnings(t *testing.T) {
t.Parallel()
t.Run("reportStyleChecksAsWarnings via config path", func(t *testing.T) {
t.Parallel()
prefs := ParseUserPreferences(map[string]any{
"js/ts": map[string]any{
"reportStyleChecksAsWarnings": false,
},
})
assert.Equal(t, prefs.ReportStyleChecksAsWarnings, core.TSFalse)
})
t.Run("reportStyleChecksAsWarnings defaults to true", func(t *testing.T) {
t.Parallel()
prefs := NewDefaultUserPreferences()
assert.Equal(t, prefs.ReportStyleChecksAsWarnings, core.TSTrue)
})
t.Run("reportStyleChecksAsWarnings via unstable section", func(t *testing.T) {
t.Parallel()
prefs := ParseUserPreferences(map[string]any{
"js/ts": map[string]any{
"unstable": map[string]any{
"reportStyleChecksAsWarnings": false,
},
},
})
assert.Equal(t, prefs.ReportStyleChecksAsWarnings, core.TSFalse)
})
}
func TestUserPreferencesParseServerFeaturePreferences(t *testing.T) {
t.Parallel()
t.Run("preferred server feature settings", func(t *testing.T) {
t.Parallel()
prefs := ParseUserPreferences(map[string]any{
"js/ts": map[string]any{
"validate": map[string]any{"enabled": false},
"format": map[string]any{"enabled": false},
"autoClosingTags": map[string]any{
"enabled": false,
},
},
})
assert.Equal(t, prefs.EnableValidation, core.TSFalse)
assert.Equal(t, prefs.EnableFormatting, core.TSFalse)
assert.Equal(t, prefs.EnableAutoClosingTags, core.TSFalse)
})
t.Run("legacy server feature fallbacks", func(t *testing.T) {
t.Parallel()
prefs := ParseUserPreferences(map[string]any{
"typescript": map[string]any{
"validate": map[string]any{"enable": false},
"format": map[string]any{"enable": false},
"autoClosingTags": false,
},
})
assert.Equal(t, prefs.EnableValidation, core.TSFalse)
assert.Equal(t, prefs.EnableFormatting, core.TSFalse)
assert.Equal(t, prefs.EnableAutoClosingTags, core.TSFalse)
})
t.Run("preferred settings take precedence over fallbacks", func(t *testing.T) {
t.Parallel()
prefs := ParseUserPreferences(map[string]any{
"typescript": map[string]any{
"validate": map[string]any{"enable": false},
"format": map[string]any{"enable": false},
"autoClosingTags": false,
},
"js/ts": map[string]any{
"validate": map[string]any{"enabled": true},
"format": map[string]any{"enabled": true},
"autoClosingTags": map[string]any{
"enabled": true,
},
},
})
assert.Equal(t, prefs.EnableValidation, core.TSTrue)
assert.Equal(t, prefs.EnableFormatting, core.TSTrue)
assert.Equal(t, prefs.EnableAutoClosingTags, core.TSTrue)
})
}
func TestUserPreferencesParseJSDocCompletionPreferences(t *testing.T) {
t.Parallel()
t.Run("unified jsdoc enabled setting", func(t *testing.T) {
t.Parallel()
prefs := ParseUserPreferences(map[string]any{
"js/ts": map[string]any{
"suggest": map[string]any{
"jsdoc": map[string]any{
"enabled": false,
},
},
},
})
assert.Equal(t, prefs.EnableJSDocCompletions, core.TSFalse)
})
t.Run("language fallback completeJSDocs setting", func(t *testing.T) {
t.Parallel()
prefs := ParseUserPreferences(map[string]any{
"typescript": map[string]any{
"suggest": map[string]any{
"completeJSDocs": false,
},
},
})
assert.Equal(t, prefs.EnableJSDocCompletions, core.TSFalse)
})
t.Run("unified jsdoc enabled takes precedence over language fallback", func(t *testing.T) {
t.Parallel()
prefs := ParseUserPreferences(map[string]any{
"typescript": map[string]any{
"suggest": map[string]any{
"completeJSDocs": false,
},
},
"js/ts": map[string]any{
"suggest": map[string]any{
"jsdoc": map[string]any{
"enabled": true,
},
},
},
})
assert.Equal(t, prefs.EnableJSDocCompletions, core.TSTrue)
})
t.Run("unified jsdoc generateReturns setting", func(t *testing.T) {
t.Parallel()
prefs := ParseUserPreferences(map[string]any{
"js/ts": map[string]any{
"suggest": map[string]any{
"jsdoc": map[string]any{
"generateReturns": false,
},
},
},
})
assert.Equal(t, prefs.GenerateReturnInDocTemplate, core.TSFalse)
})
t.Run("language jsdoc generateReturns setting", func(t *testing.T) {
t.Parallel()
prefs := ParseUserPreferences(map[string]any{
"typescript": map[string]any{
"suggest": map[string]any{
"jsdoc": map[string]any{
"generateReturns": false,
},
},
},
})
assert.Equal(t, prefs.GenerateReturnInDocTemplate, core.TSFalse)
})
}
func TestUserPreferencesParseATA(t *testing.T) {
t.Parallel()
t.Run("ParseUserPreferences with unified ATA setting in js/ts section", func(t *testing.T) {
t.Parallel()
prefs := ParseUserPreferences(map[string]any{
"js/ts": map[string]any{
"tsserver": map[string]any{
"automaticTypeAcquisition": map[string]any{
"enabled": false,
},
},
},
})
assert.Assert(t, prefs.IsATADisabled())
assert.Equal(t, prefs.AutomaticTypeAcquisitionEnabled, core.TSFalse)
})
t.Run("ParseUserPreferences with deprecated disableAutomaticTypeAcquisition in typescript section", func(t *testing.T) {
t.Parallel()
prefs := ParseUserPreferences(map[string]any{
"typescript": map[string]any{
"disableAutomaticTypeAcquisition": true,
},
})
assert.Assert(t, prefs.IsATADisabled())
assert.Equal(t, prefs.DisableAutomaticTypeAcquisition, core.TSTrue)
})
t.Run("unified setting takes precedence over deprecated setting", func(t *testing.T) {
t.Parallel()
// Both settings set: unified (js/ts) should take precedence
prefs := ParseUserPreferences(map[string]any{
"typescript": map[string]any{
"disableAutomaticTypeAcquisition": true,
},
"js/ts": map[string]any{
"tsserver": map[string]any{
"automaticTypeAcquisition": map[string]any{
"enabled": true,
},
},
},
})
assert.Assert(t, !prefs.IsATADisabled())
assert.Equal(t, prefs.AutomaticTypeAcquisitionEnabled, core.TSTrue)
})
t.Run("IsATADisabled returns false when neither setting is configured", func(t *testing.T) {
t.Parallel()
prefs := NewDefaultUserPreferences()
assert.Assert(t, !prefs.IsATADisabled())
})
}