diff --git a/packages/lint/README.md b/packages/lint/README.md
index a51eb5038..84196ea63 100644
--- a/packages/lint/README.md
+++ b/packages/lint/README.md
@@ -804,7 +804,7 @@ Source: [`eslint-plugin-promise`](https://github.com/eslint-community/eslint-plu
### Unicorn
-Modernization and style rules spanning array iteration, string and regex idioms, Node.js APIs, error handling, module syntax, DOM APIs, and code shape. Rewrite legacy patterns into their modern counterparts (`for...of` over `forEach`, `Array#flatMap` over `map().flat()`, `String#replaceAll` over a global-regex `replace`, `Math.trunc` over `| 0`), forbid known anti-patterns (`null` literals, abusive `eslint-disable`, `new Buffer`, `process.exit`, `instanceof Array`), and pin a consistent style for things [ESLint core](#eslint-core) and [TypeScript](#typescript) leave underspecified (filename case, numeric separators, catch-binding names, expiring TODOs, escape-sequence case, switch-case braces). Pure-AST; no checker dependencies.
+Modernization and style rules spanning array iteration, string and regex idioms, Node.js APIs, error handling, module syntax, DOM APIs, and code shape. Rewrite legacy patterns into their modern counterparts (`for...of` over `forEach`, `Array#flatMap` over `map().flat()`, `String#replaceAll` over a global-regex `replace`, `Math.trunc` over `| 0`), forbid known anti-patterns (`null` literals, abusive `eslint-disable`, `new Buffer`, `process.exit`, `instanceof Array`), and pin a consistent style for things [ESLint core](#eslint-core) and [TypeScript](#typescript) leave underspecified (filename case, numeric separators, catch-binding names, expiring TODOs, escape-sequence case, switch-case braces). Most rules are pure AST checks; binding-aware rules use the TypeScript checker when lexical identity is part of the contract.
Source: [`eslint-plugin-unicorn`](https://github.com/sindresorhus/eslint-plugin-unicorn).
@@ -941,7 +941,7 @@ Source: [`eslint-plugin-unicorn`](https://github.com/sindresorhus/eslint-plugin-
- [`unicorn/prefer-ternary`](https://github.com/samchon/ttsc/blob/master/tests/test-lint/src/cases/unicorn-prefer-ternary.ts): prefer a ternary over `if` / `else` whose two branches differ only in the right-hand side of a common assignment, `return`, or `throw`.
- [`unicorn/prefer-top-level-await`](https://github.com/samchon/ttsc/blob/master/tests/test-lint/src/cases/unicorn-prefer-top-level-await.ts): prefer top-level `await` over `.then` / IIFE wrappers in ES modules.
- [`unicorn/prefer-type-error`](https://github.com/samchon/ttsc/blob/master/tests/test-lint/src/cases/unicorn-prefer-type-error.ts): require throwing `TypeError` (not a bare `Error`) when the surrounding `if` is a runtime type check.
-- [`unicorn/prevent-abbreviations`](https://github.com/samchon/ttsc/blob/master/tests/test-lint/src/cases/unicorn-prevent-abbreviations.ts): reject common identifier abbreviations (`btn`, `arr`, `idx`) and replace them with their long forms.
+- [`unicorn/prevent-abbreviations`](https://github.com/samchon/ttsc/blob/master/tests/test-lint/src/cases/unicorn-prevent-abbreviations.ts): apply the canonical word-replacement table to bindings, compound names, opt-in property checks, and filenames. This compatibility ID retains the final upstream behavior from before the rule was renamed to `name-replacements`.
- [`unicorn/relative-url-style`](https://github.com/samchon/ttsc/blob/master/tests/test-lint/src/cases/unicorn-relative-url-style.ts): enforce a single style (always leading `./` vs. never) for relative URLs passed to `new URL`.
- [`unicorn/require-array-join-separator`](https://github.com/samchon/ttsc/blob/master/tests/test-lint/src/cases/unicorn-require-array-join-separator.ts): require an explicit separator argument to `Array#join` instead of relying on the default `","`.
- [`unicorn/require-module-attributes`](https://github.com/samchon/ttsc/blob/master/tests/test-lint/src/cases/unicorn-require-module-attributes.ts): require non-empty `with` / `assert` options on `import` / `export` statements that use them at all.
diff --git a/packages/lint/go.mod b/packages/lint/go.mod
index 68a5bd94f..58064bb5e 100644
--- a/packages/lint/go.mod
+++ b/packages/lint/go.mod
@@ -2,9 +2,10 @@ module github.com/samchon/ttsc/packages/lint
go 1.26
-// The plugin depends only on the `microsoft/typescript-go/shim/*`
-// modules so that `go mod tidy` works against the public proxy. The
-// bootstrap glue (Program creation, diagnostic rendering, emit) is
+// The plugin's TypeScript integration depends only on the
+// `microsoft/typescript-go/shim/*` modules so that `go mod tidy` works against
+// the public proxy. The bootstrap glue (Program creation, diagnostic
+// rendering, emit) is
// inlined under ./plugin/host.go to avoid a transitive dependency on the
// in-tree `github.com/samchon/ttsc/packages/ttsc` module — that module
// has no public version tag and would only resolve through ttsc's
@@ -25,6 +26,7 @@ require (
github.com/microsoft/typescript-go/shim/vfs v0.0.0
github.com/microsoft/typescript-go/shim/vfs/cachedvfs v0.0.0
github.com/microsoft/typescript-go/shim/vfs/osvfs v0.0.0
+ golang.org/x/text v0.35.0
)
require (
@@ -34,5 +36,4 @@ require (
github.com/zeebo/xxh3 v1.1.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect
- golang.org/x/text v0.35.0 // indirect
)
diff --git a/packages/lint/linthost/rules_unicorn_prevent_abbreviations.go b/packages/lint/linthost/rules_unicorn_prevent_abbreviations.go
index f976b0735..34f00a0b3 100644
--- a/packages/lint/linthost/rules_unicorn_prevent_abbreviations.go
+++ b/packages/lint/linthost/rules_unicorn_prevent_abbreviations.go
@@ -1,127 +1,1995 @@
-// unicorn/prevent-abbreviations: abbreviated identifier names (`err`,
-// `cb`, `ctx`, `idx`) are a project-wide readability tax — every
-// reader has to expand the shorthand mentally on the spot. The
-// upstream rule maintains a large allowlist + replacements map; this
-// MVP carries the most universal abbreviations only and fires on any
-// identifier whose lower-cased text matches one of them, in both
-// reference and declaration positions, so a `param` parameter and a
-// `void param` read are both reported.
+// unicorn/prevent-abbreviations keeps the final pre-rename contract from
+// eslint-plugin-unicorn. Upstream renamed the rule to `name-replacements` in
+// June 2026; ttsc retains the established rule ID so existing configurations
+// remain source-compatible while using the complete replacement semantics.
//
-// AST-only: visit every `Identifier`, lowercase the lexeme, look it up
-// in `unicornPreventAbbreviationsDictionary`. No scope analysis is
-// performed because the rule's diagnostic is about the *name*, not the
-// binding — every occurrence of `idx` is equally noisy.
+// Variable diagnostics are binding-based. The TypeScript checker connects one
+// declaration to all of its references, including shadowed names, shorthand
+// syntax, JSX, and TypeScript type predicates. A binding is reported once at
+// its declaration. A single unambiguous replacement is autofixed only when the
+// whole rename is safe; ambiguous replacements are exposed as suggestions.
//
-// Hot-path budget: `KindIdentifier` is the most frequent AST kind in
-// any TypeScript program, so this Check is invoked tens-of-thousands
-// of times per file. A length pre-filter eliminates ~90 % of inputs
-// without touching memory (the longest dictionary entry is six
-// characters); an additional `isAllLowerASCII` check skips
-// `strings.ToLower`'s allocation for the common case of already-
-// lower-cased identifiers.
-// https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prevent-abbreviations.md
+// The canonical replacement table and defaults are pinned to the last
+// `prevent-abbreviations` implementation before the upstream rename:
+// https://github.com/sindresorhus/eslint-plugin-unicorn/blob/42abe74908e8/rules/prevent-abbreviations.js
package linthost
import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "math"
+ "path/filepath"
+ "regexp"
+ "sort"
"strings"
+ "unicode"
+ "unicode/utf8"
shimast "github.com/microsoft/typescript-go/shim/ast"
+ shimscanner "github.com/microsoft/typescript-go/shim/scanner"
+ "golang.org/x/text/cases"
+ "golang.org/x/text/language"
)
-// unicornPreventAbbreviationsDictionary is the MVP allowlist of
-// abbreviations that fire the rule. Lower-cased keys; the rule
-// case-folds the identifier before lookup. Intentionally conservative
-// — picks the abbreviations universally agreed across upstream
-// presets so the rule doesn't flap on neutral words.
-var unicornPreventAbbreviationsDictionary = map[string]struct{}{
- "arr": {},
- "args": {},
- "attr": {},
- "btn": {},
- "cb": {},
- "cmd": {},
- "ctx": {},
- "db": {},
- "dest": {},
- "dir": {},
- "doc": {},
- "el": {},
- "elem": {},
- "env": {},
- "err": {},
- "evt": {},
- "fn": {},
- "func": {},
- "idx": {},
- "len": {},
- "lib": {},
- "mgr": {},
- "mod": {},
- "msg": {},
- "num": {},
- "obj": {},
- "opts": {},
- "param": {},
- "params": {},
- "pkg": {},
- "prev": {},
- "prop": {},
- "props": {},
- "ref": {},
- "res": {},
- "ret": {},
- "src": {},
- "str": {},
- "tmp": {},
- "val": {},
- "var": {},
-}
-
-// unicornPreventAbbreviationsMaxLen is the longest dictionary key
-// length. Any identifier longer than this cannot match — the Check
-// returns immediately without touching the dictionary.
-const unicornPreventAbbreviationsMaxLen = 6
-
type unicornPreventAbbreviations struct{}
+type unicornPreventAbbreviationsImportMode uint8
+
+const (
+ unicornPreventAbbreviationsImportOff unicornPreventAbbreviationsImportMode = iota
+ unicornPreventAbbreviationsImportInternal
+ unicornPreventAbbreviationsImportAll
+)
+
+type unicornPreventAbbreviationsOptions struct {
+ checkProperties bool
+ checkVariables bool
+ checkDefaultAndNamespaceImports unicornPreventAbbreviationsImportMode
+ checkShorthandImports unicornPreventAbbreviationsImportMode
+ checkShorthandProperties bool
+ checkFilenames bool
+ replacements map[string][]string
+ allowList map[string]bool
+ ignore []*regexp.Regexp
+}
+
+type unicornPreventAbbreviationsRawOptions struct {
+ CheckProperties *bool `json:"checkProperties"`
+ CheckVariables *bool `json:"checkVariables"`
+ CheckDefaultAndNamespaceImports json.RawMessage `json:"checkDefaultAndNamespaceImports"`
+ CheckShorthandImports json.RawMessage `json:"checkShorthandImports"`
+ CheckShorthandProperties *bool `json:"checkShorthandProperties"`
+ CheckFilenames *bool `json:"checkFilenames"`
+ ExtendDefaultReplacements *bool `json:"extendDefaultReplacements"`
+ Replacements json.RawMessage `json:"replacements"`
+ ExtendDefaultAllowList *bool `json:"extendDefaultAllowList"`
+ AllowList json.RawMessage `json:"allowList"`
+ Ignore json.RawMessage `json:"ignore"`
+}
+
+type unicornPreventAbbreviationsReplacementPatch struct {
+ disabled bool
+ values map[string]bool
+}
+
+type unicornPreventAbbreviationsNameReplacements struct {
+ total int
+ samples []string
+}
+
+type unicornPreventAbbreviationsBinding struct {
+ declaration *shimast.Node
+ nameNode *shimast.Node
+ name string
+ symbol *shimast.Symbol
+ scope *shimast.Node
+ references []*shimast.Node
+ seen map[*shimast.Node]struct{}
+}
+
+type unicornPreventAbbreviationsOccupiedName struct {
+ scope *shimast.Node
+ reference bool
+}
+
+// This is the complete canonical table, not a project-specific subset.
+// Replacement order is normalized before diagnostics and suggestions.
+var unicornPreventAbbreviationsDefaultReplacements = map[string][]string{
+ "acc": {"accumulator"},
+ "arg": {"argument"},
+ "args": {"arguments"},
+ "arr": {"array"},
+ "attr": {"attribute"},
+ "attrs": {"attributes"},
+ "btn": {"button"},
+ "cb": {"callback"},
+ "conf": {"config"},
+ "ctx": {"context"},
+ "cur": {"current"},
+ "curr": {"current"},
+ "db": {"database"},
+ "def": {"defer", "deferred", "define", "definition"},
+ "dest": {"destination"},
+ "dev": {"development"},
+ "dir": {"direction", "directory"},
+ "dirs": {"directories"},
+ "dist": {"distribution"},
+ "doc": {"document"},
+ "docs": {"documentation", "documents"},
+ "dst": {"daylightSavingTime", "destination", "distribution"},
+ "e": {"error", "event"},
+ "el": {"element"},
+ "elem": {"element"},
+ "elems": {"elements"},
+ "env": {"environment"},
+ "envs": {"environments"},
+ "err": {"error"},
+ "ev": {"event"},
+ "evt": {"event"},
+ "ext": {"extension"},
+ "exts": {"extensions"},
+ "fn": {"function"},
+ "func": {"function"},
+ "i": {"index"},
+ "idx": {"index"},
+ "j": {"index"},
+ "len": {"length"},
+ "lib": {"library"},
+ "mod": {"module"},
+ "msg": {"message"},
+ "num": {"number"},
+ "obj": {"object"},
+ "opts": {"options"},
+ "param": {"parameter"},
+ "params": {"parameters"},
+ "pkg": {"package"},
+ "prev": {"previous"},
+ "prod": {"production"},
+ "prop": {"property"},
+ "props": {"properties"},
+ "ref": {"reference"},
+ "refs": {"references"},
+ "rel": {"related", "relationship", "relative"},
+ "req": {"request"},
+ "res": {"resource", "response", "result"},
+ "ret": {"returnValue"},
+ "retval": {"returnValue"},
+ "sep": {"separator"},
+ "src": {"source"},
+ "stdDev": {"standardDeviation"},
+ "str": {"string"},
+ "tbl": {"table"},
+ "temp": {"temporary"},
+ "tit": {"title"},
+ "tmp": {"temporary"},
+ "util": {"utility"},
+ "utils": {"utilities"},
+ "val": {"value"},
+ "var": {"variable"},
+ "vars": {"variables"},
+ "ver": {"version"},
+}
+
+var unicornPreventAbbreviationsDefaultAllowList = map[string]bool{
+ "defaultProps": true,
+ "devDependencies": true,
+ "EmberENV": true,
+ "getDerivedStateFromProps": true,
+ "getInitialProps": true,
+ "getServerSideProps": true,
+ "getStaticProps": true,
+ "iOS": true,
+ "propTypes": true,
+ "setupFilesAfterEnv": true,
+}
+
+var unicornPreventAbbreviationsDefaultIgnore = []string{
+ "i18n",
+ "l10n",
+ "a11y",
+ "e2e",
+ "jQuery",
+}
+
+// A source-only rename must remain a valid binding in modules and strict-mode
+// code. TypeScript's contextual keywords are deliberately absent: names such
+// as `type`, `module`, and `constructor` remain legal lexical bindings.
+var unicornPreventAbbreviationsReservedWords = map[string]struct{}{
+ "arguments": {}, "await": {},
+ "break": {}, "case": {}, "catch": {}, "class": {}, "const": {},
+ "continue": {}, "debugger": {}, "default": {}, "delete": {},
+ "do": {}, "else": {}, "enum": {}, "export": {}, "extends": {},
+ "eval": {}, "false": {}, "finally": {}, "for": {}, "function": {},
+ "if": {}, "implements": {}, "import": {}, "in": {}, "instanceof": {},
+ "interface": {}, "let": {}, "new": {}, "null": {}, "package": {},
+ "private": {}, "protected": {}, "public": {}, "return": {}, "static": {},
+ "super": {}, "switch": {}, "this": {}, "throw": {}, "true": {},
+ "try": {}, "typeof": {}, "var": {}, "void": {}, "while": {},
+ "with": {}, "yield": {},
+}
+
func (unicornPreventAbbreviations) Name() string { return "unicorn/prevent-abbreviations" }
+func (unicornPreventAbbreviations) NeedsTypeChecker() bool {
+ return true
+}
func (unicornPreventAbbreviations) Visits() []shimast.Kind {
- return []shimast.Kind{shimast.KindIdentifier}
+ return []shimast.Kind{shimast.KindSourceFile}
}
+func (unicornPreventAbbreviations) ValidateOptions(raw json.RawMessage) error {
+ _, err := decodeUnicornPreventAbbreviationsOptions(raw)
+ return err
+}
+
func (unicornPreventAbbreviations) Check(ctx *Context, node *shimast.Node) {
+ if ctx == nil || ctx.File == nil || ctx.Checker == nil || node == nil {
+ return
+ }
+ options, err := decodeUnicornPreventAbbreviationsOptions(ctx.Options)
+ if err != nil {
+ return
+ }
+
+ bindings, bySymbol, byDeclaration, occupied := collectUnicornPreventAbbreviationsBindings(ctx, node)
+ collectUnicornPreventAbbreviationsReferences(ctx, node, bySymbol, byDeclaration)
+
+ generated := make(map[string][]*unicornPreventAbbreviationsBinding)
+ if options.checkVariables {
+ comments := collectUnicornPreventAbbreviationsComments(ctx.File)
+ for _, binding := range bindings {
+ reportUnicornPreventAbbreviationsBinding(ctx, binding, options, occupied, generated, comments)
+ }
+ }
+ if options.checkProperties {
+ walkDescendants(node, func(candidate *shimast.Node) {
+ reportUnicornPreventAbbreviationsProperty(ctx, candidate, options)
+ })
+ }
+ if options.checkFilenames {
+ reportUnicornPreventAbbreviationsFilename(ctx, node, options)
+ }
+}
+
+func decodeUnicornPreventAbbreviationsOptions(raw json.RawMessage) (unicornPreventAbbreviationsOptions, error) {
+ options := unicornPreventAbbreviationsOptions{
+ checkVariables: true,
+ checkDefaultAndNamespaceImports: unicornPreventAbbreviationsImportInternal,
+ checkShorthandImports: unicornPreventAbbreviationsImportInternal,
+ checkFilenames: true,
+ replacements: cloneUnicornPreventAbbreviationsReplacements(unicornPreventAbbreviationsDefaultReplacements),
+ allowList: cloneUnicornPreventAbbreviationsAllowList(unicornPreventAbbreviationsDefaultAllowList),
+ }
+ for _, pattern := range unicornPreventAbbreviationsDefaultIgnore {
+ options.ignore = append(options.ignore, regexp.MustCompile(pattern))
+ }
+ trimmed := bytes.TrimSpace(raw)
+ if len(trimmed) == 0 {
+ return options, nil
+ }
+ if trimmed[0] != '{' {
+ return options, errors.New("options must be an object")
+ }
+
+ var rawFields map[string]json.RawMessage
+ if err := json.Unmarshal(trimmed, &rawFields); err != nil {
+ return options, fmt.Errorf("options must be an object: %w", err)
+ }
+ // The value marks boolean fields, whose pointer-backed decoding would
+ // otherwise make an explicit null indistinguishable from omission.
+ knownFields := map[string]bool{
+ "checkProperties": true,
+ "checkVariables": true,
+ "checkDefaultAndNamespaceImports": false,
+ "checkShorthandImports": false,
+ "checkShorthandProperties": true,
+ "checkFilenames": true,
+ "extendDefaultReplacements": true,
+ "replacements": false,
+ "extendDefaultAllowList": true,
+ "allowList": false,
+ "ignore": false,
+ }
+ for name, value := range rawFields {
+ boolean, known := knownFields[name]
+ if !known {
+ return options, fmt.Errorf("unknown option %q", name)
+ }
+ if boolean && bytes.Equal(bytes.TrimSpace(value), []byte("null")) {
+ return options, fmt.Errorf("option %q must be a boolean", name)
+ }
+ }
+
+ decoder := json.NewDecoder(bytes.NewReader(trimmed))
+ decoder.DisallowUnknownFields()
+ var configured unicornPreventAbbreviationsRawOptions
+ if err := decoder.Decode(&configured); err != nil {
+ return options, fmt.Errorf("options must be an object: %w", err)
+ }
+ if err := requireUnicornPreventAbbreviationsEOF(decoder); err != nil {
+ return options, err
+ }
+
+ if configured.CheckProperties != nil {
+ options.checkProperties = *configured.CheckProperties
+ }
+ if configured.CheckVariables != nil {
+ options.checkVariables = *configured.CheckVariables
+ }
+ if configured.CheckShorthandProperties != nil {
+ options.checkShorthandProperties = *configured.CheckShorthandProperties
+ }
+ if configured.CheckFilenames != nil {
+ options.checkFilenames = *configured.CheckFilenames
+ }
+
+ var err error
+ options.checkDefaultAndNamespaceImports, err = decodeUnicornPreventAbbreviationsImportMode(
+ configured.CheckDefaultAndNamespaceImports,
+ "checkDefaultAndNamespaceImports",
+ unicornPreventAbbreviationsImportInternal,
+ )
+ if err != nil {
+ return options, err
+ }
+ options.checkShorthandImports, err = decodeUnicornPreventAbbreviationsImportMode(
+ configured.CheckShorthandImports,
+ "checkShorthandImports",
+ unicornPreventAbbreviationsImportInternal,
+ )
+ if err != nil {
+ return options, err
+ }
+
+ extendReplacements := configured.ExtendDefaultReplacements == nil || *configured.ExtendDefaultReplacements
+ if !extendReplacements {
+ options.replacements = make(map[string][]string)
+ }
+ patches, err := decodeUnicornPreventAbbreviationsReplacementPatches(configured.Replacements)
+ if err != nil {
+ return options, err
+ }
+ applyUnicornPreventAbbreviationsReplacementPatches(options.replacements, patches)
+
+ extendAllowList := configured.ExtendDefaultAllowList == nil || *configured.ExtendDefaultAllowList
+ if !extendAllowList {
+ options.allowList = make(map[string]bool)
+ }
+ allowList, err := decodeUnicornPreventAbbreviationsBooleanMap(configured.AllowList, "allowList")
+ if err != nil {
+ return options, err
+ }
+ for name, allowed := range allowList {
+ if allowed {
+ options.allowList[name] = true
+ } else {
+ delete(options.allowList, name)
+ }
+ }
+
+ ignore, err := decodeUnicornPreventAbbreviationsIgnore(configured.Ignore)
+ if err != nil {
+ return options, err
+ }
+ options.ignore = append(options.ignore, ignore...)
+ return options, nil
+}
+
+func requireUnicornPreventAbbreviationsEOF(decoder *json.Decoder) error {
+ var trailing any
+ if err := decoder.Decode(&trailing); err != io.EOF {
+ if err == nil {
+ return errors.New("options must contain exactly one object")
+ }
+ return fmt.Errorf("options contain trailing data: %w", err)
+ }
+ return nil
+}
+
+func decodeUnicornPreventAbbreviationsImportMode(
+ raw json.RawMessage,
+ name string,
+ fallback unicornPreventAbbreviationsImportMode,
+) (unicornPreventAbbreviationsImportMode, error) {
+ if len(raw) == 0 {
+ return fallback, nil
+ }
+ var value any
+ if err := json.Unmarshal(raw, &value); err != nil {
+ return fallback, fmt.Errorf("option %q must be a boolean or \"internal\"", name)
+ }
+ switch configured := value.(type) {
+ case bool:
+ if configured {
+ return unicornPreventAbbreviationsImportAll, nil
+ }
+ return unicornPreventAbbreviationsImportOff, nil
+ case string:
+ if configured == "internal" {
+ return unicornPreventAbbreviationsImportInternal, nil
+ }
+ }
+ return fallback, fmt.Errorf("option %q must be a boolean or \"internal\"", name)
+}
+
+func decodeUnicornPreventAbbreviationsReplacementPatches(
+ raw json.RawMessage,
+) (map[string]unicornPreventAbbreviationsReplacementPatch, error) {
+ patches := make(map[string]unicornPreventAbbreviationsReplacementPatch)
+ if len(raw) == 0 {
+ return patches, nil
+ }
+ var entries map[string]json.RawMessage
+ if err := json.Unmarshal(raw, &entries); err != nil || entries == nil {
+ return nil, errors.New("option \"replacements\" must be an object")
+ }
+ for name, value := range entries {
+ var disabled *bool
+ if err := json.Unmarshal(value, &disabled); err == nil && disabled != nil {
+ if *disabled {
+ return nil, fmt.Errorf("option \"replacements.%s\" must be false or an object", name)
+ }
+ patches[name] = unicornPreventAbbreviationsReplacementPatch{disabled: true}
+ continue
+ }
+ replacements, err := decodeUnicornPreventAbbreviationsBooleanMap(value, "replacements."+name)
+ if err != nil {
+ return nil, err
+ }
+ patches[name] = unicornPreventAbbreviationsReplacementPatch{values: replacements}
+ }
+ return patches, nil
+}
+
+func decodeUnicornPreventAbbreviationsBooleanMap(raw json.RawMessage, name string) (map[string]bool, error) {
+ values := make(map[string]bool)
+ if len(raw) == 0 {
+ return values, nil
+ }
+ if err := json.Unmarshal(raw, &values); err != nil || values == nil {
+ return nil, fmt.Errorf("option %q must be an object with boolean values", name)
+ }
+ return values, nil
+}
+
+func decodeUnicornPreventAbbreviationsIgnore(raw json.RawMessage) ([]*regexp.Regexp, error) {
+ if len(raw) == 0 {
+ return nil, nil
+ }
+ var patterns []string
+ if err := json.Unmarshal(raw, &patterns); err != nil || patterns == nil {
+ return nil, errors.New("option \"ignore\" must be an array of regular-expression strings")
+ }
+ compiled := make([]*regexp.Regexp, 0, len(patterns))
+ seen := make(map[string]struct{}, len(patterns))
+ for index, pattern := range patterns {
+ if _, duplicate := seen[pattern]; duplicate {
+ return nil, fmt.Errorf("option \"ignore\"[%d] duplicates %q", index, pattern)
+ }
+ seen[pattern] = struct{}{}
+ expression, err := regexp.Compile(pattern)
+ if err != nil {
+ return nil, fmt.Errorf("option \"ignore\"[%d] must be a valid regular expression: %w", index, err)
+ }
+ compiled = append(compiled, expression)
+ }
+ return compiled, nil
+}
+
+func cloneUnicornPreventAbbreviationsReplacements(source map[string][]string) map[string][]string {
+ clone := make(map[string][]string, len(source))
+ for name, replacements := range source {
+ clone[name] = append([]string(nil), replacements...)
+ }
+ return clone
+}
+
+func cloneUnicornPreventAbbreviationsAllowList(source map[string]bool) map[string]bool {
+ clone := make(map[string]bool, len(source))
+ for name, allowed := range source {
+ clone[name] = allowed
+ }
+ return clone
+}
+
+func applyUnicornPreventAbbreviationsReplacementPatches(
+ replacements map[string][]string,
+ patches map[string]unicornPreventAbbreviationsReplacementPatch,
+) {
+ for name, patch := range patches {
+ if patch.disabled {
+ // Keep an empty entry so lower-first lookup stops here instead of
+ // falling through to a differently cased custom key.
+ replacements[name] = nil
+ continue
+ }
+ values := make(map[string]bool)
+ for _, replacement := range replacements[name] {
+ values[replacement] = true
+ }
+ for replacement, enabled := range patch.values {
+ values[replacement] = enabled
+ }
+ merged := make([]string, 0, len(values))
+ for replacement, enabled := range values {
+ if enabled {
+ merged = append(merged, replacement)
+ }
+ }
+ sort.Strings(merged)
+ replacements[name] = merged
+ }
+}
+
+func collectUnicornPreventAbbreviationsBindings(
+ ctx *Context,
+ root *shimast.Node,
+) (
+ []*unicornPreventAbbreviationsBinding,
+ map[*shimast.Symbol]*unicornPreventAbbreviationsBinding,
+ map[*shimast.Node]*unicornPreventAbbreviationsBinding,
+ map[string][]unicornPreventAbbreviationsOccupiedName,
+) {
+ bindings := make([]*unicornPreventAbbreviationsBinding, 0)
+ bySymbol := make(map[*shimast.Symbol]*unicornPreventAbbreviationsBinding)
+ byDeclaration := make(map[*shimast.Node]*unicornPreventAbbreviationsBinding)
+ occupied := make(map[string][]unicornPreventAbbreviationsOccupiedName)
+ seenNames := make(map[*shimast.Node]struct{})
+
+ walkDescendants(root, func(node *shimast.Node) {
+ nameNode := unicornPreventAbbreviationsBindingIdentifier(node)
+ if nameNode == nil {
+ return
+ }
+ if _, duplicate := seenNames[nameNode]; duplicate {
+ return
+ }
+ seenNames[nameNode] = struct{}{}
+ name := identifierText(nameNode)
+ if name == "" {
+ return
+ }
+ scope := unicornPreventAbbreviationsBindingScope(node)
+ occupied[name] = append(occupied[name], unicornPreventAbbreviationsOccupiedName{scope: scope})
+
+ symbol := unicornPreventAbbreviationsCanonicalSymbol(ctx, nameNode)
+ if symbol == nil {
+ return
+ }
+ if existing := bySymbol[symbol]; existing != nil {
+ byDeclaration[node] = existing
+ return
+ }
+ binding := &unicornPreventAbbreviationsBinding{
+ declaration: node,
+ nameNode: nameNode,
+ name: name,
+ symbol: symbol,
+ scope: scope,
+ seen: make(map[*shimast.Node]struct{}),
+ }
+ bindings = append(bindings, binding)
+ bySymbol[symbol] = binding
+ byDeclaration[node] = binding
+ for _, declaration := range symbol.Declarations {
+ byDeclaration[declaration] = binding
+ }
+ })
+ collectUnicornPreventAbbreviationsOccupiedReferences(
+ ctx,
+ root,
+ occupied,
+ )
+ return bindings, bySymbol, byDeclaration, occupied
+}
+
+// Unresolved lexical references also constrain a rename. Resolved local,
+// ambient, and global symbols are covered by ResolveName at every declaration
+// and reference site; retaining only unresolved reads here avoids treating
+// property-like syntax as a lexical collision.
+func collectUnicornPreventAbbreviationsOccupiedReferences(
+ ctx *Context,
+ root *shimast.Node,
+ occupied map[string][]unicornPreventAbbreviationsOccupiedName,
+) {
+ walkDescendants(root, func(node *shimast.Node) {
+ if node.Kind != shimast.KindIdentifier || !unicornPreventAbbreviationsIsNameReference(node) {
+ return
+ }
+ symbol := unicornPreventAbbreviationsCanonicalSymbol(ctx, node)
+ if symbol != nil {
+ return
+ }
+ name := identifierText(node)
+ if name != "" {
+ occupied[name] = append(occupied[name], unicornPreventAbbreviationsOccupiedName{
+ scope: unicornPreventAbbreviationsReferenceScope(node),
+ reference: true,
+ })
+ }
+ })
+}
+
+// GetExportSymbolOfSymbol normalizes both value and type exports. The shared
+// canonicalValueSymbol helper intentionally performs only value-export
+// normalization, while this rule also renames interfaces, aliases, enums, and
+// their type-position references.
+func unicornPreventAbbreviationsCanonicalSymbol(ctx *Context, node *shimast.Node) *shimast.Symbol {
+ symbol := valueSymbolAtIdentifier(ctx, node)
+ if symbol == nil {
+ return nil
+ }
+ return ctx.Checker.GetExportSymbolOfSymbol(symbol)
+}
+
+func unicornPreventAbbreviationsReferenceScope(node *shimast.Node) *shimast.Node {
+ for scope := node.Parent; scope != nil; scope = scope.Parent {
+ if unicornPreventAbbreviationsIsFunctionLike(scope) {
+ return scope
+ }
+ switch scope.Kind {
+ case shimast.KindMappedType,
+ shimast.KindConditionalType:
+ return scope
+ case shimast.KindCaseBlock,
+ shimast.KindForStatement,
+ shimast.KindForInStatement,
+ shimast.KindForOfStatement,
+ shimast.KindCatchClause:
+ return scope
+ case shimast.KindClassDeclaration,
+ shimast.KindClassExpression,
+ shimast.KindInterfaceDeclaration,
+ shimast.KindTypeAliasDeclaration,
+ shimast.KindModuleBlock,
+ shimast.KindSourceFile,
+ shimast.KindClassStaticBlockDeclaration:
+ return scope
+ case shimast.KindBlock:
+ if scope.Parent != nil && unicornPreventAbbreviationsIsFunctionLike(scope.Parent) {
+ return scope.Parent
+ }
+ return scope
+ }
+ }
+ return nil
+}
+
+// TypeScript signature nodes introduce parameter/type-parameter scopes just
+// like runtime functions, but the shared runtime-only helper deliberately does
+// not classify them as function-like.
+func unicornPreventAbbreviationsIsFunctionLike(node *shimast.Node) bool {
+ if isFunctionLikeKind(node) {
+ return true
+ }
+ if node == nil {
+ return false
+ }
+ switch node.Kind {
+ case shimast.KindFunctionType,
+ shimast.KindConstructorType,
+ shimast.KindCallSignature,
+ shimast.KindConstructSignature,
+ shimast.KindMethodSignature,
+ shimast.KindIndexSignature:
+ return true
+ }
+ return false
+}
+
+func unicornPreventAbbreviationsIsNameReference(node *shimast.Node) bool {
+ if node == nil || node.Parent == nil {
+ return true
+ }
+ if unicornPreventAbbreviationsIsIntrinsicJSXTagName(node) ||
+ unicornPreventAbbreviationsIsImportTypeQualifier(node) {
+ return false
+ }
+ parent := node.Parent
+ switch parent.Kind {
+ case shimast.KindPropertyAccessExpression:
+ access := parent.AsPropertyAccessExpression()
+ return access == nil || access.Name() != node
+ case shimast.KindQualifiedName:
+ name := parent.AsQualifiedName()
+ return name == nil || name.Right != node
+ case shimast.KindPropertyAssignment:
+ assignment := parent.AsPropertyAssignment()
+ return assignment == nil || assignment.Name() != node
+ case shimast.KindBindingElement:
+ element := parent.AsBindingElement()
+ return element == nil || element.PropertyName != node
+ case shimast.KindMethodDeclaration,
+ shimast.KindPropertyDeclaration,
+ shimast.KindGetAccessor,
+ shimast.KindSetAccessor,
+ shimast.KindMethodSignature,
+ shimast.KindPropertySignature,
+ shimast.KindEnumMember:
+ return parent.Name() != node
+ case shimast.KindImportSpecifier:
+ specifier := parent.AsImportSpecifier()
+ return specifier == nil || specifier.PropertyName != node
+ case shimast.KindExportSpecifier:
+ specifier := parent.AsExportSpecifier()
+ if unicornPreventAbbreviationsExportSpecifierHasModuleSource(parent) {
+ return false
+ }
+ return specifier == nil || specifier.Name() != node || specifier.PropertyName == nil
+ case shimast.KindLabeledStatement:
+ statement := parent.AsLabeledStatement()
+ return statement == nil || statement.Label != node
+ case shimast.KindBreakStatement:
+ statement := parent.AsBreakStatement()
+ return statement == nil || statement.Label != node
+ case shimast.KindContinueStatement:
+ statement := parent.AsContinueStatement()
+ return statement == nil || statement.Label != node
+ case shimast.KindJsxAttribute:
+ return parent.Name() != node
+ case shimast.KindNamedTupleMember,
+ shimast.KindImportAttribute,
+ shimast.KindNamespaceExport,
+ shimast.KindNamespaceExportDeclaration,
+ shimast.KindMetaProperty:
+ return parent.Name() != node
+ case shimast.KindJsxNamespacedName:
+ return false
+ }
+ return true
+}
+
+func unicornPreventAbbreviationsIsIntrinsicJSXTagName(node *shimast.Node) bool {
+ if node == nil || node.Parent == nil || !shimscanner.IsIntrinsicJsxName(identifierText(node)) {
+ return false
+ }
+ switch node.Parent.Kind {
+ case shimast.KindJsxOpeningElement,
+ shimast.KindJsxSelfClosingElement,
+ shimast.KindJsxClosingElement:
+ return node.Parent.TagName() == node
+ }
+ return false
+}
+
+func unicornPreventAbbreviationsIsImportTypeQualifier(node *shimast.Node) bool {
+ current := node
+ for current != nil && current.Parent != nil && current.Parent.Kind == shimast.KindQualifiedName {
+ current = current.Parent
+ }
+ if current == nil || current.Parent == nil || current.Parent.Kind != shimast.KindImportType {
+ return false
+ }
+ imported := current.Parent.AsImportTypeNode()
+ return imported != nil && imported.Qualifier == current
+}
+
+func unicornPreventAbbreviationsExportSpecifierHasModuleSource(specifier *shimast.Node) bool {
+ for current := specifier; current != nil; current = current.Parent {
+ if current.Kind == shimast.KindExportDeclaration {
+ declaration := current.AsExportDeclaration()
+ return declaration != nil && declaration.ModuleSpecifier != nil
+ }
+ if current.Kind == shimast.KindSourceFile {
+ return false
+ }
+ }
+ return false
+}
+
+func unicornPreventAbbreviationsBindingIdentifier(node *shimast.Node) *shimast.Node {
+ if node == nil {
+ return nil
+ }
+ switch node.Kind {
+ case shimast.KindVariableDeclaration:
+ if declaration := node.AsVariableDeclaration(); declaration != nil {
+ return unicornPreventAbbreviationsPlainBindingName(declaration.Name())
+ }
+ case shimast.KindParameter:
+ if declaration := node.AsParameterDeclaration(); declaration != nil {
+ return unicornPreventAbbreviationsPlainBindingName(declaration.Name())
+ }
+ case shimast.KindBindingElement:
+ if element := node.AsBindingElement(); element != nil {
+ return unicornPreventAbbreviationsPlainBindingName(element.Name())
+ }
+ case shimast.KindFunctionDeclaration,
+ shimast.KindFunctionExpression,
+ shimast.KindClassDeclaration,
+ shimast.KindClassExpression,
+ shimast.KindEnumDeclaration,
+ shimast.KindModuleDeclaration,
+ shimast.KindInterfaceDeclaration,
+ shimast.KindTypeAliasDeclaration,
+ shimast.KindTypeParameter:
+ return unicornPreventAbbreviationsPlainBindingName(node.Name())
+ case shimast.KindImportClause,
+ shimast.KindNamespaceImport,
+ shimast.KindImportSpecifier,
+ shimast.KindImportEqualsDeclaration:
+ return unicornPreventAbbreviationsPlainBindingName(node.Name())
+ }
+ return nil
+}
+
+func unicornPreventAbbreviationsPlainBindingName(node *shimast.Node) *shimast.Node {
+ if node != nil && node.Kind == shimast.KindIdentifier {
+ return node
+ }
+ return nil
+}
+
+func unicornPreventAbbreviationsBindingScope(declaration *shimast.Node) *shimast.Node {
+ if declaration == nil {
+ return nil
+ }
+ if declaration.Kind == shimast.KindFunctionExpression || declaration.Kind == shimast.KindClassExpression {
+ return declaration
+ }
+ if parameter := unicornPreventAbbreviationsEnclosingParameter(declaration); parameter != nil {
+ for scope := parameter.Parent; scope != nil; scope = scope.Parent {
+ if unicornPreventAbbreviationsIsFunctionLike(scope) {
+ return scope
+ }
+ }
+ return nil
+ }
+ root := unicornPreventAbbreviationsRootDeclaration(declaration)
+ if root != nil && root.Kind == shimast.KindVariableDeclaration && root.Parent != nil &&
+ root.Parent.Kind == shimast.KindVariableDeclarationList && shimast.IsVar(root.Parent) {
+ for scope := root.Parent.Parent; scope != nil; scope = scope.Parent {
+ if unicornPreventAbbreviationsIsFunctionLike(scope) {
+ return scope
+ }
+ switch scope.Kind {
+ case shimast.KindSourceFile, shimast.KindModuleBlock, shimast.KindClassStaticBlockDeclaration:
+ return scope
+ }
+ }
+ return nil
+ }
+ if declaration.Kind == shimast.KindTypeParameter {
+ for scope := declaration.Parent; scope != nil; scope = scope.Parent {
+ if unicornPreventAbbreviationsIsFunctionLike(scope) {
+ return scope
+ }
+ switch scope.Kind {
+ case shimast.KindMappedType,
+ shimast.KindConditionalType:
+ return scope
+ case shimast.KindClassDeclaration,
+ shimast.KindClassExpression,
+ shimast.KindInterfaceDeclaration,
+ shimast.KindTypeAliasDeclaration,
+ shimast.KindMethodSignature:
+ return scope
+ }
+ }
+ return nil
+ }
+ scope := preferConstLexicalScope(declaration)
+ if scope != nil && scope.Kind == shimast.KindBlock && scope.Parent != nil &&
+ scope.Parent.Kind == shimast.KindCatchClause {
+ return scope.Parent
+ }
+ return scope
+}
+
+// Destructuring BindingElements inherit declaration-kind semantics from the
+// root Parameter, catch VariableDeclaration, or variable declaration.
+func unicornPreventAbbreviationsRootDeclaration(declaration *shimast.Node) *shimast.Node {
+ for declaration != nil && declaration.Kind == shimast.KindBindingElement &&
+ declaration.Parent != nil && declaration.Parent.Parent != nil {
+ declaration = declaration.Parent.Parent
+ }
+ return declaration
+}
+
+func unicornPreventAbbreviationsEnclosingParameter(declaration *shimast.Node) *shimast.Node {
+ for current := declaration; current != nil; current = current.Parent {
+ if current.Kind == shimast.KindParameter {
+ return current
+ }
+ if current != declaration && unicornPreventAbbreviationsIsFunctionLike(current) {
+ return nil
+ }
+ if current.Kind == shimast.KindSourceFile {
+ return nil
+ }
+ }
+ return nil
+}
+
+func collectUnicornPreventAbbreviationsReferences(
+ ctx *Context,
+ root *shimast.Node,
+ bySymbol map[*shimast.Symbol]*unicornPreventAbbreviationsBinding,
+ byDeclaration map[*shimast.Node]*unicornPreventAbbreviationsBinding,
+) {
+ walkDescendants(root, func(node *shimast.Node) {
+ if node.Kind != shimast.KindIdentifier {
+ return
+ }
+ symbol := unicornPreventAbbreviationsReferenceSymbol(ctx, node)
+ if symbol == nil {
+ return
+ }
+ binding := bySymbol[symbol]
+ if binding == nil {
+ for _, declaration := range symbol.Declarations {
+ if candidate := byDeclaration[declaration]; candidate != nil {
+ binding = candidate
+ break
+ }
+ }
+ }
+ if binding == nil {
+ return
+ }
+ if _, duplicate := binding.seen[node]; duplicate {
+ return
+ }
+ binding.seen[node] = struct{}{}
+ binding.references = append(binding.references, node)
+ })
+}
+
+// The checker binds an export specifier's written name to the exported alias,
+// not to the local declaration that supplies it. Resolve the local side through
+// TypeScript's dedicated API so a local rename can preserve the public spelling
+// (`export { err }` becomes `export { error as err }`). Re-exports have no local
+// binding and stay on the ordinary symbol-at-location path.
+func unicornPreventAbbreviationsReferenceSymbol(ctx *Context, node *shimast.Node) *shimast.Symbol {
+ if node != nil && node.Parent != nil && node.Parent.Kind == shimast.KindExportSpecifier &&
+ !unicornPreventAbbreviationsExportSpecifierHasModuleSource(node.Parent) {
+ specifier := node.Parent.AsExportSpecifier()
+ if specifier != nil && (specifier.PropertyName == node ||
+ specifier.PropertyName == nil && specifier.Name() == node) {
+ symbol := ctx.Checker.GetExportSpecifierLocalTargetSymbol(node.Parent)
+ if symbol != nil {
+ return ctx.Checker.GetExportSymbolOfSymbol(symbol)
+ }
+ }
+ }
+ return unicornPreventAbbreviationsCanonicalSymbol(ctx, node)
+}
+
+func reportUnicornPreventAbbreviationsBinding(
+ ctx *Context,
+ binding *unicornPreventAbbreviationsBinding,
+ options unicornPreventAbbreviationsOptions,
+ occupied map[string][]unicornPreventAbbreviationsOccupiedName,
+ generated map[string][]*unicornPreventAbbreviationsBinding,
+ comments map[int]commentToken,
+) {
+ if binding == nil || !unicornPreventAbbreviationsShouldCheckBinding(binding, options) {
+ return
+ }
+ replacements := getUnicornPreventAbbreviationsNameReplacements(binding.name, options, 3)
+ if replacements.total == 0 {
+ return
+ }
+
+ safeToRename := unicornPreventAbbreviationsCanRenameBinding(ctx, binding, comments)
+ scopes := unicornPreventAbbreviationsBindingReferenceScopes(binding)
+ available := make([]string, 0, len(replacements.samples))
+ for _, replacement := range replacements.samples {
+ name := unicornPreventAbbreviationsAvailableName(
+ ctx,
+ binding,
+ replacement,
+ scopes,
+ occupied,
+ generated,
+ )
+ if name != "" {
+ available = append(available, name)
+ }
+ }
+ replacements.samples = available
+ message := unicornPreventAbbreviationsMessage(binding.name, replacements, "variable")
+
+ if replacements.total == 1 && len(replacements.samples) == 1 && safeToRename {
+ replacement := replacements.samples[0]
+ edits := unicornPreventAbbreviationsRenameEdits(ctx, binding, replacement)
+ if len(edits) > 0 {
+ generated[replacement] = append(generated[replacement], binding)
+ ctx.ReportFix(binding.nameNode, message, edits...)
+ return
+ }
+ }
+
+ suggestions := make([]Suggestion, 0, len(replacements.samples))
+ if replacements.total > 1 && safeToRename {
+ for _, replacement := range replacements.samples {
+ edits := unicornPreventAbbreviationsRenameEdits(ctx, binding, replacement)
+ if len(edits) == 0 {
+ continue
+ }
+ suggestions = append(suggestions, Suggestion{
+ Title: fmt.Sprintf("Rename to `%s`.", replacement),
+ Edits: edits,
+ })
+ }
+ }
+ ctx.ReportFixSuggestions(binding.nameNode, message, nil, suggestions...)
+}
+
+func unicornPreventAbbreviationsShouldCheckBinding(
+ binding *unicornPreventAbbreviationsBinding,
+ options unicornPreventAbbreviationsOptions,
+) bool {
+ declaration := binding.declaration
+ if declaration == nil {
+ return false
+ }
+ if unicornPreventAbbreviationsIsShorthandBinding(declaration) && !options.checkShorthandProperties {
+ return false
+ }
+ switch declaration.Kind {
+ case shimast.KindVariableDeclaration:
+ variable := declaration.AsVariableDeclaration()
+ if variable != nil {
+ if _, required := unicornPreventAbbreviationsStaticRequireModule(variable.Initializer); required {
+ return unicornPreventAbbreviationsImportAllowed(
+ options.checkDefaultAndNamespaceImports,
+ unicornPreventAbbreviationsIsInternalImport(declaration),
+ )
+ }
+ }
+ case shimast.KindImportEqualsDeclaration:
+ if !unicornPreventAbbreviationsIsExternalImportEquals(declaration) {
+ return true
+ }
+ return unicornPreventAbbreviationsImportAllowed(
+ options.checkDefaultAndNamespaceImports,
+ unicornPreventAbbreviationsIsInternalImport(declaration),
+ )
+ case shimast.KindImportClause, shimast.KindNamespaceImport:
+ return unicornPreventAbbreviationsImportAllowed(
+ options.checkDefaultAndNamespaceImports,
+ unicornPreventAbbreviationsIsInternalImport(declaration),
+ )
+ case shimast.KindImportSpecifier:
+ specifier := declaration.AsImportSpecifier()
+ if specifier != nil && specifier.PropertyName != nil &&
+ moduleExportNameText(specifier.PropertyName) == "default" {
+ return unicornPreventAbbreviationsImportAllowed(
+ options.checkDefaultAndNamespaceImports,
+ unicornPreventAbbreviationsIsInternalImport(declaration),
+ )
+ }
+ if specifier != nil && specifier.PropertyName == nil {
+ return unicornPreventAbbreviationsImportAllowed(
+ options.checkShorthandImports,
+ unicornPreventAbbreviationsIsInternalImport(declaration),
+ )
+ }
+ }
+ return true
+}
+
+func unicornPreventAbbreviationsIsShorthandBinding(declaration *shimast.Node) bool {
+ if declaration == nil || declaration.Kind != shimast.KindBindingElement {
+ return false
+ }
+ element := declaration.AsBindingElement()
+ return element != nil && element.PropertyName == nil && element.DotDotDotToken == nil &&
+ declaration.Parent != nil && declaration.Parent.Kind == shimast.KindObjectBindingPattern
+}
+
+func unicornPreventAbbreviationsImportAllowed(
+ mode unicornPreventAbbreviationsImportMode,
+ internal bool,
+) bool {
+ return mode == unicornPreventAbbreviationsImportAll ||
+ mode == unicornPreventAbbreviationsImportInternal && internal
+}
+
+func unicornPreventAbbreviationsIsInternalImport(declaration *shimast.Node) bool {
+ if declaration != nil && declaration.Kind == shimast.KindVariableDeclaration {
+ variable := declaration.AsVariableDeclaration()
+ if variable == nil {
+ return false
+ }
+ source, required := unicornPreventAbbreviationsStaticRequireModule(variable.Initializer)
+ return required && unicornPreventAbbreviationsIsInternalModule(source)
+ }
+ for ancestor := declaration; ancestor != nil; ancestor = ancestor.Parent {
+ if ancestor.Kind == shimast.KindImportDeclaration {
+ imported := ancestor.AsImportDeclaration()
+ if imported == nil {
+ return false
+ }
+ source := stringLiteralText(imported.ModuleSpecifier)
+ return unicornPreventAbbreviationsIsInternalModule(source)
+ }
+ if ancestor.Kind == shimast.KindImportEqualsDeclaration {
+ imported := ancestor.AsImportEqualsDeclaration()
+ if imported == nil || imported.ModuleReference == nil ||
+ imported.ModuleReference.Kind != shimast.KindExternalModuleReference {
+ return false
+ }
+ external := imported.ModuleReference.AsExternalModuleReference()
+ if external == nil {
+ return false
+ }
+ source := stringLiteralText(external.Expression)
+ return unicornPreventAbbreviationsIsInternalModule(source)
+ }
+ }
+ return false
+}
+
+func unicornPreventAbbreviationsIsInternalModule(source string) bool {
+ return !strings.Contains(source, "node_modules") &&
+ (strings.HasPrefix(source, ".") || strings.HasPrefix(source, "/"))
+}
+
+// Import controls apply only to upstream's exact static-require shape: one
+// ordinary string-literal argument on a non-optional bare require call.
+func unicornPreventAbbreviationsStaticRequireModule(node *shimast.Node) (string, bool) {
+ node = stripParens(node)
+ if node == nil || node.Kind != shimast.KindCallExpression {
+ return "", false
+ }
+ call := node.AsCallExpression()
+ if call == nil || call.QuestionDotToken != nil || callCalleeName(call) != "require" ||
+ call.Arguments == nil || len(call.Arguments.Nodes) != 1 {
+ return "", false
+ }
+ argument := call.Arguments.Nodes[0]
+ if argument == nil || argument.Kind != shimast.KindStringLiteral {
+ return "", false
+ }
+ literal := argument.AsStringLiteral()
+ if literal == nil {
+ return "", false
+ }
+ return literal.Text, true
+}
+
+func unicornPreventAbbreviationsIsExternalImportEquals(declaration *shimast.Node) bool {
+ if declaration == nil || declaration.Kind != shimast.KindImportEqualsDeclaration {
+ return false
+ }
+ imported := declaration.AsImportEqualsDeclaration()
+ return imported != nil && imported.ModuleReference != nil &&
+ imported.ModuleReference.Kind == shimast.KindExternalModuleReference
+}
+
+func unicornPreventAbbreviationsCanRenameBinding(
+ ctx *Context,
+ binding *unicornPreventAbbreviationsBinding,
+ comments map[int]commentToken,
+) bool {
+ if binding == nil || len(binding.references) == 0 || unicornPreventAbbreviationsBindingIsExternallyVisible(ctx, binding) {
+ return false
+ }
+ if strings.EqualFold(filepath.Ext(ctx.File.FileName()), ".vue") {
+ return false
+ }
+ parameter := unicornPreventAbbreviationsEnclosingParameter(binding.declaration)
+ if parameter != nil && unicornPreventAbbreviationsParameterHasJSDoc(ctx, parameter, comments) {
+ return false
+ }
+ if parameter != nil && isParameterProperty(parameter) {
+ return false
+ }
+ for _, reference := range binding.references {
+ if noFallthroughNodeIsJSXTagName(reference) {
+ return false
+ }
+ if pos, end := tokenRange(ctx.File, reference); pos < 0 || end <= pos {
+ return false
+ }
+ }
+ return true
+}
+
+func unicornPreventAbbreviationsBindingIsExternallyVisible(
+ ctx *Context,
+ binding *unicornPreventAbbreviationsBinding,
+) bool {
+ if ctx == nil || ctx.File == nil || binding == nil || binding.symbol == nil || ctx.File.IsDeclarationFile {
+ return true
+ }
+ declarations := make([]*shimast.Node, 0, len(binding.symbol.Declarations)+1)
+ declarations = append(declarations, binding.declaration)
+ declarations = append(declarations, binding.symbol.Declarations...)
+ seen := make(map[*shimast.Node]struct{}, len(declarations))
+ for _, declaration := range declarations {
+ if declaration == nil {
+ return true
+ }
+ if _, duplicate := seen[declaration]; duplicate {
+ continue
+ }
+ seen[declaration] = struct{}{}
+ source := shimast.GetSourceFileOfNode(declaration)
+ // A source-only edit is safe only when every declaration is proven to
+ // belong to the current file. Detached or synthetic declarations fail
+ // closed just like declarations owned by another source file.
+ if source == nil || source != ctx.File {
+ return true
+ }
+ if unicornPreventAbbreviationsDeclarationIsExportedOrAmbient(declaration) {
+ return true
+ }
+ }
+ return false
+}
+
+func unicornPreventAbbreviationsDeclarationIsExportedOrAmbient(declaration *shimast.Node) bool {
+ if declaration == nil {
+ return true
+ }
+ for owner := declaration; owner != nil; owner = owner.Parent {
+ if owner.Flags&shimast.NodeFlagsAmbient != 0 ||
+ owner.ModifierFlags()&shimast.ModifierFlagsAmbient != 0 {
+ return true
+ }
+ if owner.Kind == shimast.KindSourceFile {
+ break
+ }
+ }
+
+ owner := declaration
+ for current := declaration; current != nil; current = current.Parent {
+ if current.Kind == shimast.KindVariableDeclaration {
+ owner = current
+ break
+ }
+ if current.Kind == shimast.KindParameter || current.Kind == shimast.KindCatchClause ||
+ unicornPreventAbbreviationsIsFunctionLike(current) || current.Kind == shimast.KindSourceFile {
+ break
+ }
+ }
+ flags := shimast.GetCombinedModifierFlags(owner)
+ namedExport := flags&shimast.ModifierFlagsExport != 0 && flags&shimast.ModifierFlagsDefault == 0
+ return namedExport || flags&shimast.ModifierFlagsAmbient != 0
+}
+
+func collectUnicornPreventAbbreviationsComments(file *shimast.SourceFile) map[int]commentToken {
+ comments := make(map[int]commentToken)
+ forEachCommentToken(file, func(kind shimast.Kind, pos, end int) {
+ comments[end] = commentToken{kind: kind, pos: pos, end: end}
+ })
+ return comments
+}
+
+func unicornPreventAbbreviationsParameterHasJSDoc(
+ ctx *Context,
+ parameter *shimast.Node,
+ comments map[int]commentToken,
+) bool {
+ if ctx == nil || ctx.File == nil || parameter == nil {
+ return false
+ }
+ function := parameter.Parent
+ for function != nil && !unicornPreventAbbreviationsIsFunctionLike(function) {
+ function = function.Parent
+ }
+ if function == nil {
+ return false
+ }
+ commentable := function
+ for commentable.Parent != nil {
+ parent := commentable.Parent
+ attachable := false
+ switch parent.Kind {
+ case shimast.KindVariableDeclaration,
+ shimast.KindVariableDeclarationList,
+ shimast.KindVariableStatement,
+ shimast.KindPropertyDeclaration,
+ shimast.KindPropertyAssignment,
+ shimast.KindPropertySignature,
+ shimast.KindTypeAliasDeclaration,
+ shimast.KindExportAssignment,
+ shimast.KindExpressionStatement:
+ attachable = true
+ case shimast.KindBinaryExpression:
+ expression := parent.AsBinaryExpression()
+ attachable = expression != nil && expression.Right == commentable &&
+ expression.OperatorToken != nil && isAssignmentOperator(expression.OperatorToken.Kind)
+ case shimast.KindParenthesizedExpression,
+ shimast.KindAsExpression,
+ shimast.KindSatisfiesExpression,
+ shimast.KindNonNullExpression,
+ shimast.KindTypeAssertionExpression:
+ attachable = parent.Expression() == commentable
+ }
+ if !attachable {
+ break
+ }
+ commentable = parent
+ }
+ start, _ := tokenRange(ctx.File, commentable)
+ if start <= 0 {
+ return false
+ }
+ text := ctx.File.Text()
+ prefix := strings.TrimRightFunc(text[:start], unicode.IsSpace)
+ attached, ok := comments[len(prefix)]
+ if !ok || attached.kind != shimast.KindMultiLineCommentTrivia ||
+ attached.pos < 0 || attached.end > len(text) ||
+ !strings.HasPrefix(text[attached.pos:attached.end], "/**") {
+ return false
+ }
+ if unicornPreventAbbreviationsLineBreakCount(text[attached.end:start]) > 1 {
+ return false
+ }
+ comment := text[attached.pos:attached.end]
+ for offset := 0; ; {
+ index := strings.Index(comment[offset:], "@param")
+ if index < 0 {
+ return false
+ }
+ end := offset + index + len("@param")
+ if end == len(comment) || !unicornPreventAbbreviationsRegExpWordByte(comment[end]) {
+ return true
+ }
+ offset = end
+ }
+}
+
+func unicornPreventAbbreviationsLineBreakCount(text string) int {
+ count := 0
+ for index := 0; index < len(text); {
+ character, size := utf8.DecodeRuneInString(text[index:])
+ switch character {
+ case '\r':
+ count++
+ if index+size < len(text) && text[index+size] == '\n' {
+ size++
+ }
+ case '\n', '\u2028', '\u2029':
+ count++
+ }
+ index += size
+ }
+ return count
+}
+
+// JavaScript's /\b/u word boundary still uses ASCII \w semantics.
+func unicornPreventAbbreviationsRegExpWordByte(character byte) bool {
+ return character >= 'a' && character <= 'z' ||
+ character >= 'A' && character <= 'Z' ||
+ character >= '0' && character <= '9' || character == '_'
+}
+
+func unicornPreventAbbreviationsRenameEdits(
+ ctx *Context,
+ binding *unicornPreventAbbreviationsBinding,
+ replacement string,
+) []TextEdit {
+ if !unicornPreventAbbreviationsValidIdentifier(replacement) {
+ return nil
+ }
+ edits := make([]TextEdit, 0, len(binding.references))
+ seen := make(map[[2]int]struct{}, len(binding.references))
+ for _, reference := range binding.references {
+ pos, end := tokenRange(ctx.File, reference)
+ if pos < 0 || end <= pos {
+ return nil
+ }
+ key := [2]int{pos, end}
+ if _, duplicate := seen[key]; duplicate {
+ continue
+ }
+ seen[key] = struct{}{}
+ edits = append(edits, TextEdit{
+ Pos: pos,
+ End: end,
+ Text: unicornPreventAbbreviationsReferenceReplacement(reference, binding.name, replacement),
+ })
+ }
+ sort.Slice(edits, func(i, j int) bool {
+ if edits[i].Pos == edits[j].Pos {
+ return edits[i].End < edits[j].End
+ }
+ return edits[i].Pos < edits[j].Pos
+ })
+ return edits
+}
+
+func unicornPreventAbbreviationsReferenceReplacement(node *shimast.Node, oldName, newName string) string {
+ if node == nil || node.Parent == nil {
+ return newName
+ }
+ parent := node.Parent
+ switch parent.Kind {
+ case shimast.KindShorthandPropertyAssignment:
+ shorthand := parent.AsShorthandPropertyAssignment()
+ if shorthand != nil && shorthand.Name() == node {
+ return oldName + ": " + newName
+ }
+ case shimast.KindBindingElement:
+ element := parent.AsBindingElement()
+ if element != nil && element.Name() == node && element.PropertyName == nil && element.DotDotDotToken == nil &&
+ parent.Parent != nil && parent.Parent.Kind == shimast.KindObjectBindingPattern {
+ return oldName + ": " + newName
+ }
+ case shimast.KindImportSpecifier:
+ specifier := parent.AsImportSpecifier()
+ if specifier != nil && specifier.Name() == node && specifier.PropertyName == nil {
+ return oldName + " as " + newName
+ }
+ case shimast.KindExportSpecifier:
+ specifier := parent.AsExportSpecifier()
+ if specifier != nil && specifier.Name() == node && specifier.PropertyName == nil {
+ return newName + " as " + oldName
+ }
+ }
+ return newName
+}
+
+func unicornPreventAbbreviationsAvailableName(
+ ctx *Context,
+ binding *unicornPreventAbbreviationsBinding,
+ desired string,
+ scopes []*shimast.Node,
+ occupied map[string][]unicornPreventAbbreviationsOccupiedName,
+ generated map[string][]*unicornPreventAbbreviationsBinding,
+) string {
+ candidate := desired
+ if !unicornPreventAbbreviationsValidIdentifier(candidate) {
+ candidate += "_"
+ if !unicornPreventAbbreviationsValidIdentifier(candidate) {
+ return ""
+ }
+ }
+ for unicornPreventAbbreviationsCheckerNameCollides(ctx, candidate, binding) ||
+ unicornPreventAbbreviationsOccupiedNameCollides(candidate, binding.scope, occupied) ||
+ unicornPreventAbbreviationsGeneratedNameCollides(candidate, binding, scopes, generated) {
+ candidate += "_"
+ }
+ return candidate
+}
+
+// ResolveName covers every declaration visible where the renamed binding is
+// declared or read, including compiler-provided globals which do not occur as
+// identifier nodes in the source file. The unresolved-reference pass remains
+// necessary because the checker intentionally returns nil for those names.
+func unicornPreventAbbreviationsCheckerNameCollides(
+ ctx *Context,
+ name string,
+ binding *unicornPreventAbbreviationsBinding,
+) bool {
+ if ctx == nil || ctx.Checker == nil || binding == nil {
+ return false
+ }
+ meaning := shimast.SymbolFlagsValue | shimast.SymbolFlagsType |
+ shimast.SymbolFlagsNamespace | shimast.SymbolFlagsAlias
+ locations := make([]*shimast.Node, 0, len(binding.references)+1)
+ locations = append(locations, binding.nameNode)
+ locations = append(locations, binding.references...)
+ seen := make(map[*shimast.Node]struct{}, len(locations))
+ for _, location := range locations {
+ if location == nil {
+ continue
+ }
+ if _, duplicate := seen[location]; duplicate {
+ continue
+ }
+ seen[location] = struct{}{}
+ if ctx.Checker.ResolveName(name, location, meaning, false /*excludeGlobals*/) != nil {
+ return true
+ }
+ }
+ return false
+}
+
+// Each generated name retains its binding plus the precise scopes where it is
+// declared or referenced. This allows legal shadowing while still detecting
+// a newly renamed inner declaration that would capture an outer binding's read
+// in a deeper descendant scope.
+func unicornPreventAbbreviationsBindingReferenceScopes(
+ binding *unicornPreventAbbreviationsBinding,
+) []*shimast.Node {
+ if binding == nil {
+ return nil
+ }
+ scopes := make([]*shimast.Node, 0, len(binding.references)+1)
+ seen := make(map[*shimast.Node]struct{}, len(binding.references)+1)
+ add := func(scope *shimast.Node) {
+ if _, duplicate := seen[scope]; duplicate {
+ return
+ }
+ seen[scope] = struct{}{}
+ scopes = append(scopes, scope)
+ }
+ add(binding.scope)
+ for _, scope := range unicornPreventAbbreviationsVarRestrictedScopes(binding.declaration, binding.scope) {
+ add(scope)
+ }
+ for _, reference := range binding.references {
+ add(unicornPreventAbbreviationsReferenceScope(reference))
+ }
+ return scopes
+}
+
+// A var declaration is function-scoped, but ECMAScript early errors also
+// prohibit it from crossing lexical declarations in any containing block,
+// loop, switch, or catch before that function boundary. Record those syntactic
+// containers in addition to the var binding's function scope.
+func unicornPreventAbbreviationsVarRestrictedScopes(
+ declaration *shimast.Node,
+ bindingScope *shimast.Node,
+) []*shimast.Node {
+ root := unicornPreventAbbreviationsRootDeclaration(declaration)
+ if root == nil || root.Kind != shimast.KindVariableDeclaration || root.Parent == nil ||
+ root.Parent.Kind != shimast.KindVariableDeclarationList || !shimast.IsVar(root.Parent) {
+ return nil
+ }
+ scopes := make([]*shimast.Node, 0)
+ for current := root.Parent.Parent; current != nil && current != bindingScope; current = current.Parent {
+ switch current.Kind {
+ case shimast.KindBlock:
+ if current.Parent == nil || !unicornPreventAbbreviationsIsFunctionLike(current.Parent) {
+ scopes = append(scopes, current)
+ }
+ case shimast.KindCaseBlock,
+ shimast.KindForStatement,
+ shimast.KindForInStatement,
+ shimast.KindForOfStatement,
+ shimast.KindCatchClause:
+ scopes = append(scopes, current)
+ }
+ }
+ return scopes
+}
+
+func unicornPreventAbbreviationsOccupiedNameCollides(
+ name string,
+ scope *shimast.Node,
+ names map[string][]unicornPreventAbbreviationsOccupiedName,
+) bool {
+ for _, occupied := range names[name] {
+ if occupied.reference {
+ if scope == nil || occupied.scope == nil || scope == occupied.scope ||
+ unicornPreventAbbreviationsIsAncestor(scope, occupied.scope) {
+ return true
+ }
+ continue
+ }
+ if scope == nil || occupied.scope == nil || scope == occupied.scope ||
+ unicornPreventAbbreviationsIsAncestor(occupied.scope, scope) {
+ return true
+ }
+ }
+ return false
+}
+
+func unicornPreventAbbreviationsGeneratedNameCollides(
+ name string,
+ binding *unicornPreventAbbreviationsBinding,
+ scopes []*shimast.Node,
+ names map[string][]*unicornPreventAbbreviationsBinding,
+) bool {
+ for _, existing := range names[name] {
+ existingScopes := unicornPreventAbbreviationsBindingReferenceScopes(existing)
+ for _, scope := range scopes {
+ for _, existingScope := range existingScopes {
+ if scope == existingScope {
+ return true
+ }
+ }
+ }
+ if binding == nil || existing == nil || binding.scope == nil || existing.scope == nil {
+ return true
+ }
+ if unicornPreventAbbreviationsIsAncestor(binding.scope, existing.scope) {
+ if unicornPreventAbbreviationsBindingHasReferenceInScope(binding, existing.scope) {
+ return true
+ }
+ } else if unicornPreventAbbreviationsIsAncestor(existing.scope, binding.scope) {
+ if unicornPreventAbbreviationsBindingHasReferenceInScope(existing, binding.scope) {
+ return true
+ }
+ }
+ }
+ return false
+}
+
+func unicornPreventAbbreviationsBindingHasReferenceInScope(
+ binding *unicornPreventAbbreviationsBinding,
+ scope *shimast.Node,
+) bool {
+ if binding == nil || scope == nil {
+ return true
+ }
+ for _, reference := range binding.references {
+ referenceScope := unicornPreventAbbreviationsReferenceScope(reference)
+ if referenceScope == nil || referenceScope == scope ||
+ unicornPreventAbbreviationsIsAncestor(scope, referenceScope) {
+ return true
+ }
+ }
+ return false
+}
+
+func unicornPreventAbbreviationsIsAncestor(ancestor, node *shimast.Node) bool {
+ for current := node.Parent; current != nil; current = current.Parent {
+ if current == ancestor {
+ return true
+ }
+ }
+ return false
+}
+
+func reportUnicornPreventAbbreviationsProperty(
+ ctx *Context,
+ node *shimast.Node,
+ options unicornPreventAbbreviationsOptions,
+) {
+ if node == nil || node.Kind != shimast.KindIdentifier || identifierText(node) == "__proto__" ||
+ !unicornPreventAbbreviationsIsProperty(node) {
+ return
+ }
name := identifierText(node)
- if name == "" || len(name) > unicornPreventAbbreviationsMaxLen {
+ replacements := getUnicornPreventAbbreviationsNameReplacements(name, options, 3)
+ if replacements.total == 0 {
return
}
- // Fast path: dictionary keys are pre-lowercased, so an
- // already-lower-case identifier avoids `strings.ToLower`'s
- // allocation entirely. The Check is invoked once per identifier
- // in the file, so the saved allocations multiply.
- lookup := name
- if !isAllLowerASCII(name) {
- lookup = strings.ToLower(name)
+ message := unicornPreventAbbreviationsMessage(name, replacements, "property")
+ suggestions := make([]Suggestion, 0, len(replacements.samples))
+ if replacements.total > 1 && (node.Parent == nil || node.Parent.Kind != shimast.KindExportSpecifier) {
+ pos, end := tokenRange(ctx.File, node)
+ if pos >= 0 {
+ for _, replacement := range replacements.samples {
+ // Property IdentifierNames may use reserved words; only lexical
+ // identifier validity matters here.
+ if !shimscanner.IsValidIdentifier(replacement) {
+ continue
+ }
+ suggestions = append(suggestions, Suggestion{
+ Title: fmt.Sprintf("Rename to `%s`.", replacement),
+ Edits: []TextEdit{{Pos: pos, End: end, Text: replacement}},
+ })
+ }
+ }
}
- if _, ok := unicornPreventAbbreviationsDictionary[lookup]; !ok {
+ ctx.ReportFixSuggestions(node, message, nil, suggestions...)
+}
+
+func unicornPreventAbbreviationsIsProperty(node *shimast.Node) bool {
+ if node == nil || node.Parent == nil {
+ return false
+ }
+ parent := node.Parent
+ switch parent.Kind {
+ case shimast.KindPropertyAccessExpression:
+ access := parent.AsPropertyAccessExpression()
+ if access == nil || access.Name() != node || parent.Parent == nil {
+ return false
+ }
+ switch parent.Parent.Kind {
+ case shimast.KindBinaryExpression:
+ expression := parent.Parent.AsBinaryExpression()
+ return expression != nil && expression.Left == parent && expression.OperatorToken != nil &&
+ isAssignmentOperator(expression.OperatorToken.Kind)
+ case shimast.KindPrefixUnaryExpression:
+ expression := parent.Parent.AsPrefixUnaryExpression()
+ return expression != nil && expression.Operand == parent &&
+ (expression.Operator == shimast.KindPlusPlusToken || expression.Operator == shimast.KindMinusMinusToken)
+ case shimast.KindPostfixUnaryExpression:
+ expression := parent.Parent.AsPostfixUnaryExpression()
+ return expression != nil && expression.Operand == parent &&
+ (expression.Operator == shimast.KindPlusPlusToken || expression.Operator == shimast.KindMinusMinusToken)
+ }
+ case shimast.KindPropertyAssignment:
+ assignment := parent.AsPropertyAssignment()
+ return assignment != nil && assignment.Name() == node &&
+ !isDestructuringAssignmentTarget(parent)
+ case shimast.KindMethodDeclaration,
+ shimast.KindPropertyDeclaration,
+ shimast.KindGetAccessor,
+ shimast.KindSetAccessor,
+ shimast.KindMethodSignature,
+ shimast.KindPropertySignature:
+ return parent.Name() == node
+ case shimast.KindExportSpecifier:
+ specifier := parent.AsExportSpecifier()
+ return specifier != nil && specifier.PropertyName != nil && specifier.Name() == node
+ }
+ return false
+}
+
+func reportUnicornPreventAbbreviationsFilename(
+ ctx *Context,
+ node *shimast.Node,
+ options unicornPreventAbbreviationsOptions,
+) {
+ filenameWithPath := ctx.File.FileName()
+ if filenameWithPath == "" || unicornPreventAbbreviationsIsVirtualFilename(filenameWithPath) {
+ return
+ }
+ filename := filepath.Base(filenameWithPath)
+ extension := unicornPreventAbbreviationsFilenameExtension(filename)
+ basename := strings.TrimSuffix(filename, extension)
+ replacements := getUnicornPreventAbbreviationsNameReplacements(basename, options, 3)
+ if replacements.total == 0 {
return
}
- ctx.Report(node, "Prefer the long form over abbreviated identifiers (e.g. `error` over `err`).")
+ for index := range replacements.samples {
+ replacements.samples[index] += extension
+ }
+ ctx.Report(node, unicornPreventAbbreviationsMessage(filename, replacements, "filename"))
+}
+
+func unicornPreventAbbreviationsIsVirtualFilename(filename string) bool {
+ return filename == "" || filename == ""
+}
+
+// Node's path.extname treats a leading-dot basename such as `.err` as having
+// no extension, while filepath.Ext returns the whole basename. Preserve the
+// upstream filename contract without changing platform-specific separators.
+func unicornPreventAbbreviationsFilenameExtension(filename string) string {
+ extension := filepath.Ext(filename)
+ if extension == filename {
+ return ""
+ }
+ return extension
+}
+
+func getUnicornPreventAbbreviationsNameReplacements(
+ name string,
+ options unicornPreventAbbreviationsOptions,
+ limit int,
+) unicornPreventAbbreviationsNameReplacements {
+ if name == "" || unicornPreventAbbreviationsUppercase(name) == name || options.allowList[name] {
+ return unicornPreventAbbreviationsNameReplacements{}
+ }
+ for _, pattern := range options.ignore {
+ if pattern.MatchString(name) {
+ return unicornPreventAbbreviationsNameReplacements{}
+ }
+ }
+
+ exact := getUnicornPreventAbbreviationsWordReplacements(name, options)
+ if len(exact) > 0 {
+ total := len(exact)
+ if len(exact) > limit {
+ exact = exact[:limit]
+ }
+ return unicornPreventAbbreviationsNameReplacements{total: total, samples: exact}
+ }
+
+ words := splitUnicornPreventAbbreviationsWords(name)
+ combinations := make([][]string, 0, len(words))
+ total := 1
+ changed := false
+ for _, word := range words {
+ replacements := getUnicornPreventAbbreviationsWordReplacements(word, options)
+ if len(replacements) == 0 {
+ replacements = []string{word}
+ } else {
+ changed = true
+ }
+ combinations = append(combinations, replacements)
+ if total > math.MaxInt/len(replacements) {
+ total = math.MaxInt
+ } else {
+ total *= len(replacements)
+ }
+ }
+ if !changed {
+ return unicornPreventAbbreviationsNameReplacements{}
+ }
+ samples := cartesianUnicornPreventAbbreviationsSamples(combinations, limit)
+ for index, parts := range samples {
+ for part := len(parts) - 1; part > 0; part-- {
+ if unicornPreventAbbreviationsASCIIWord(parts[part]) && strings.HasSuffix(parts[part-1], parts[part]) {
+ parts = append(parts[:part], parts[part+1:]...)
+ }
+ }
+ samples[index] = parts
+ }
+ joined := make([]string, 0, len(samples))
+ for _, parts := range samples {
+ joined = append(joined, strings.Join(parts, ""))
+ }
+ return unicornPreventAbbreviationsNameReplacements{total: total, samples: joined}
+}
+
+func getUnicornPreventAbbreviationsWordReplacements(
+ word string,
+ options unicornPreventAbbreviationsOptions,
+) []string {
+ if word == "" || unicornPreventAbbreviationsUppercase(word) == word || options.allowList[word] {
+ return nil
+ }
+ keys := []string{
+ lowerUnicornPreventAbbreviationsFirst(word),
+ word,
+ upperUnicornPreventAbbreviationsFirst(word),
+ }
+ var replacements []string
+ for _, key := range keys {
+ if configured, ok := options.replacements[key]; ok {
+ replacements = configured
+ break
+ }
+ }
+ if len(replacements) == 0 {
+ return nil
+ }
+ transformed := make([]string, 0, len(replacements))
+ upperFirst := unicornPreventAbbreviationsStartsUpper(word)
+ for _, replacement := range replacements {
+ if upperFirst {
+ transformed = append(transformed, upperUnicornPreventAbbreviationsFirst(replacement))
+ } else {
+ transformed = append(transformed, lowerUnicornPreventAbbreviationsFirst(replacement))
+ }
+ }
+ sort.Strings(transformed)
+ return transformed
+}
+
+func splitUnicornPreventAbbreviationsWords(name string) []string {
+ runes := []rune(name)
+ if len(runes) == 0 {
+ return nil
+ }
+ words := make([]string, 0, len(runes))
+ start := 0
+ flush := func(end int) {
+ if end > start {
+ words = append(words, string(runes[start:end]))
+ }
+ start = end
+ }
+ for index, current := range runes {
+ if index == 0 {
+ continue
+ }
+ // This is the rune-level equivalent of upstream's
+ // /(?=\P{Lowercase_Letter})|(?<=\P{Letter})/u split expression.
+ if !unicode.Is(unicode.Ll, current) || !unicode.IsLetter(runes[index-1]) {
+ flush(index)
+ }
+ }
+ flush(len(runes))
+ return words
+}
+
+func cartesianUnicornPreventAbbreviationsSamples(parts [][]string, limit int) [][]string {
+ if len(parts) == 0 || limit <= 0 {
+ return nil
+ }
+ samples := make([][]string, 0, limit)
+ current := make([]string, len(parts))
+ var visit func(int)
+ visit = func(index int) {
+ if len(samples) >= limit {
+ return
+ }
+ if index == len(parts) {
+ samples = append(samples, append([]string(nil), current...))
+ return
+ }
+ for _, value := range parts[index] {
+ current[index] = value
+ visit(index + 1)
+ if len(samples) >= limit {
+ return
+ }
+ }
+ }
+ visit(0)
+ return samples
}
-// isAllLowerASCII reports whether `s` is a non-empty ASCII string
-// whose every byte is a lower-case letter. Used by hot-path Checks
-// that look up an already-normalized dictionary — the result is
-// equivalent to `s == strings.ToLower(s)` for all-ASCII inputs but
-// avoids the lowercase allocation.
-func isAllLowerASCII(s string) bool {
- for i := 0; i < len(s); i++ {
- c := s[i]
- if c >= 'A' && c <= 'Z' {
+func unicornPreventAbbreviationsMessage(
+ discouragedName string,
+ replacements unicornPreventAbbreviationsNameReplacements,
+ nameType string,
+) string {
+ const suffix = " A more descriptive name will do too."
+ if replacements.total == 1 && len(replacements.samples) == 1 {
+ return fmt.Sprintf(
+ "The %s `%s` should be named `%s`.%s",
+ nameType,
+ discouragedName,
+ replacements.samples[0],
+ suffix,
+ )
+ }
+ names := make([]string, 0, len(replacements.samples))
+ for _, replacement := range replacements.samples {
+ names = append(names, "`"+replacement+"`")
+ }
+ rendered := strings.Join(names, ", ")
+ omitted := replacements.total - len(replacements.samples)
+ if omitted > 0 {
+ count := fmt.Sprintf("%d", omitted)
+ if omitted > 99 {
+ count = "99+"
+ }
+ rendered += fmt.Sprintf(", ... (%s more omitted)", count)
+ }
+ return fmt.Sprintf(
+ "Please rename the %s `%s`. Suggested names are: %s.%s",
+ nameType,
+ discouragedName,
+ rendered,
+ suffix,
+ )
+}
+
+func unicornPreventAbbreviationsValidIdentifier(name string) bool {
+ if !shimscanner.IsValidIdentifier(name) {
+ return false
+ }
+ if _, reserved := unicornPreventAbbreviationsReservedWords[name]; reserved {
+ return false
+ }
+ return true
+}
+
+func unicornPreventAbbreviationsASCIIWord(value string) bool {
+ if value == "" {
+ return false
+ }
+ for _, character := range value {
+ if character > unicode.MaxASCII || !unicode.IsLetter(character) {
return false
}
}
return true
}
+func unicornPreventAbbreviationsStartsUpper(value string) bool {
+ first, size := utf8.DecodeRuneInString(value)
+ // Upstream compares the first UTF-16 code unit with its upper-case form.
+ // Consequently uncased ASCII characters (`$`, `_`, digits) and either half
+ // of an astral rune count as upper-first too.
+ return first > 0xFFFF || unicornPreventAbbreviationsUppercase(value[:size]) == value[:size]
+}
+
+func lowerUnicornPreventAbbreviationsFirst(value string) string {
+ first, size := utf8.DecodeRuneInString(value)
+ if size == 0 {
+ return value
+ }
+ // JavaScript's charAt(0) sees only the high surrogate of an astral rune, so
+ // the final upstream implementation leaves that first rune unchanged.
+ if first > 0xFFFF {
+ return value
+ }
+ if first <= unicode.MaxASCII {
+ return string(unicode.ToLower(first)) + value[size:]
+ }
+ return cases.Lower(language.Und).String(value[:size]) + value[size:]
+}
+
+func upperUnicornPreventAbbreviationsFirst(value string) string {
+ first, size := utf8.DecodeRuneInString(value)
+ if size == 0 {
+ return value
+ }
+ if first > 0xFFFF {
+ return value
+ }
+ if first <= unicode.MaxASCII {
+ return string(unicode.ToUpper(first)) + value[size:]
+ }
+ return unicornPreventAbbreviationsUppercase(value[:size]) + value[size:]
+}
+
+func unicornPreventAbbreviationsUppercase(value string) string {
+ for index := 0; index < len(value); index++ {
+ if value[index] >= utf8.RuneSelf {
+ return cases.Upper(language.Und).String(value)
+ }
+ }
+ return strings.ToUpper(value)
+}
+
func init() {
Register(unicornPreventAbbreviations{})
}
diff --git a/packages/lint/src/structures/rules/ITtscLintRuleOptionsMap.ts b/packages/lint/src/structures/rules/ITtscLintRuleOptionsMap.ts
index 4cff1bc4f..e56e1cf9a 100644
--- a/packages/lint/src/structures/rules/ITtscLintRuleOptionsMap.ts
+++ b/packages/lint/src/structures/rules/ITtscLintRuleOptionsMap.ts
@@ -44,6 +44,7 @@ import type {
ITtscLintTypeScriptNoRestrictedTypesRuleOptions,
ITtscLintTypeScriptSwitchExhaustivenessCheckRuleOptions,
} from "./ITtscLintTypeScriptRuleOptions";
+import type { ITtscLintUnicornPreventAbbreviationsRuleOptions } from "./ITtscLintUnicornRuleOptions";
/**
* Index from typed rule name to its single options-object slot.
@@ -116,4 +117,5 @@ export interface ITtscLintRuleOptionsMap {
"typescript/no-misused-promises": ITtscLintTypeScriptNoMisusedPromisesRuleOptions;
"typescript/no-restricted-types": ITtscLintTypeScriptNoRestrictedTypesRuleOptions;
"typescript/switch-exhaustiveness-check": ITtscLintTypeScriptSwitchExhaustivenessCheckRuleOptions;
+ "unicorn/prevent-abbreviations": ITtscLintUnicornPreventAbbreviationsRuleOptions;
}
diff --git a/packages/lint/src/structures/rules/ITtscLintUnicornRuleOptions.ts b/packages/lint/src/structures/rules/ITtscLintUnicornRuleOptions.ts
new file mode 100644
index 000000000..52dc1985f
--- /dev/null
+++ b/packages/lint/src/structures/rules/ITtscLintUnicornRuleOptions.ts
@@ -0,0 +1,62 @@
+/** Import categories accepted by `unicorn/prevent-abbreviations`. */
+export type TtscLintUnicornPreventAbbreviationsImportMode =
+ | boolean
+ | "internal";
+
+/**
+ * Replacement patch for one discouraged name.
+ *
+ * `false` disables every replacement for the name. An object enables or
+ * disables individual replacement spellings.
+ */
+export type TtscLintUnicornPreventAbbreviationsReplacement =
+ | false
+ | Readonly>;
+
+/** Options for `unicorn/prevent-abbreviations`. */
+export interface ITtscLintUnicornPreventAbbreviationsRuleOptions {
+ /** Also check property definitions and writes. @default false */
+ checkProperties?: boolean;
+
+ /** Check lexical bindings. @default true */
+ checkVariables?: boolean;
+
+ /**
+ * Check default and namespace imports from all modules, internal modules, or
+ * no modules.
+ *
+ * @default "internal"
+ */
+ checkDefaultAndNamespaceImports?: TtscLintUnicornPreventAbbreviationsImportMode;
+
+ /**
+ * Check unaliased named imports from all modules, internal modules, or no
+ * modules.
+ *
+ * @default "internal"
+ */
+ checkShorthandImports?: TtscLintUnicornPreventAbbreviationsImportMode;
+
+ /** Check bindings introduced by shorthand object destructuring. @default false */
+ checkShorthandProperties?: boolean;
+
+ /** Check the physical source filename. @default true */
+ checkFilenames?: boolean;
+
+ /** Merge `replacements` into the canonical default table. @default true */
+ extendDefaultReplacements?: boolean;
+
+ /** Add, remove, or replace discouraged-name mappings. */
+ replacements?: Readonly<
+ Record
+ >;
+
+ /** Merge `allowList` into the canonical default allow list. @default true */
+ extendDefaultAllowList?: boolean;
+
+ /** Case-sensitive full names to allow or remove from the allow list. */
+ allowList?: Readonly>;
+
+ /** Regular-expression strings matched against a complete name or basename. */
+ ignore?: readonly string[];
+}
diff --git a/packages/lint/src/structures/rules/ITtscLintUnicornRules.ts b/packages/lint/src/structures/rules/ITtscLintUnicornRules.ts
index 12fd36cec..bb70ceb43 100644
--- a/packages/lint/src/structures/rules/ITtscLintUnicornRules.ts
+++ b/packages/lint/src/structures/rules/ITtscLintUnicornRules.ts
@@ -1,4 +1,8 @@
-import type { TtscLintRuleSetting } from "../TtscLintRuleSetting";
+import type {
+ TtscLintRuleOptionsSetting,
+ TtscLintRuleSetting,
+} from "../TtscLintRuleSetting";
+import type { ITtscLintUnicornPreventAbbreviationsRuleOptions } from "./ITtscLintUnicornRuleOptions";
/**
* Modernization and style rules from `eslint-plugin-unicorn`.
@@ -8,7 +12,8 @@ import type { TtscLintRuleSetting } from "../TtscLintRuleSetting";
* counterparts, forbid known anti-patterns, and pin a consistent style for
* things ESLint core and `typescript/*` leave underspecified.
*
- * Pure-AST; no checker dependencies.
+ * Most rules are pure AST checks. Binding-aware rules use the TypeScript
+ * checker when lexical identity is part of the upstream contract.
*
* @reference https://github.com/sindresorhus/eslint-plugin-unicorn
*/
@@ -1042,12 +1047,14 @@ export interface ITtscLintUnicornRules {
"unicorn/prefer-type-error"?: TtscLintRuleSetting;
/**
- * Reject common identifier abbreviations (`btn`, `arr`, `idx`) and replace
- * them with their long forms.
+ * Replace discouraged words in bindings, compound names, properties, and
+ * filenames according to the canonical replacement table. The legacy rule
+ * ID retains the final upstream behavior from before its June 2026 rename to
+ * `name-replacements`.
*
- * @reference https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prevent-abbreviations.md
+ * @reference https://github.com/sindresorhus/eslint-plugin-unicorn/blob/42abe74908e8/docs/rules/prevent-abbreviations.md
*/
- "unicorn/prevent-abbreviations"?: TtscLintRuleSetting;
+ "unicorn/prevent-abbreviations"?: TtscLintRuleOptionsSetting;
/**
* Enforce a single style (always leading `./` vs. never) for relative URLs
diff --git a/packages/lint/src/structures/rules/index.ts b/packages/lint/src/structures/rules/index.ts
index 8905125da..0aba31347 100644
--- a/packages/lint/src/structures/rules/index.ts
+++ b/packages/lint/src/structures/rules/index.ts
@@ -29,5 +29,6 @@ export * from "./ITtscLintTestingLibraryRuleOptions";
export * from "./ITtscLintTestingLibraryRules";
export * from "./ITtscLintTypeScriptRuleOptions";
export * from "./ITtscLintTypeScriptRules";
+export * from "./ITtscLintUnicornRuleOptions";
export * from "./ITtscLintUnicornRules";
export * from "./ITtscLintVitestRules";
diff --git a/packages/lint/test/rules/unicorn/unicorn_prevent_abbreviations_test.go b/packages/lint/test/rules/unicorn/unicorn_prevent_abbreviations_test.go
index 0e2e4bb43..8d7b2c1f9 100644
--- a/packages/lint/test/rules/unicorn/unicorn_prevent_abbreviations_test.go
+++ b/packages/lint/test/rules/unicorn/unicorn_prevent_abbreviations_test.go
@@ -1,22 +1,870 @@
package linthost
-import "testing"
-
-// TestRuleCorpusUnicornPreventAbbreviations verifies
-// unicorn/prevent-abbreviations reports the abbreviation `idx` in both
-// declaration and reference positions.
-//
-// The rule visits every `Identifier`, lowercases the lexeme, and looks
-// it up in a static MVP dictionary. There is no scope analysis — every
-// occurrence of an abbreviated name is equally noisy — so this fixture
-// pins both the declaration arm (parameter) and the reference arm (use)
-// in one shot.
-//
-// 1. Enable unicorn/prevent-abbreviations via two stacked expect
-// annotations, one for each occurrence.
-// 2. Declare a function `f(idx: number)` and read `idx` once inside the
-// body.
-// 3. Assert the rule reports on both occurrences.
+import (
+ "encoding/json"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+)
+
+const unicornPreventAbbreviationsRuleName = "unicorn/prevent-abbreviations"
+
func TestRuleCorpusUnicornPreventAbbreviations(t *testing.T) {
- assertRuleCorpusCase(t, "unicorn/prevent-abbreviations.ts", "// expect: unicorn/prevent-abbreviations error\nfunction f(idx: number) {\n // expect: unicorn/prevent-abbreviations error\n void idx;\n}\n")
+ source := "// expect: unicorn/prevent-abbreviations error\nconst errCb = (error: Error): void => {\n console.error(error);\n};\n\nerrCb(new Error(\"fixture\"));\n"
+ expected := parseRuleExpectations(t, source)
+ _, _, findings := runRuleFindingsSnapshot(t, unicornPreventAbbreviationsRuleName, source, nil)
+ if len(findings) == 0 {
+ t.Fatalf("unicorn/prevent-abbreviations.ts: want %v, got no findings", expected)
+ }
+ actual := normalizeRuleFindings(findings[0].File, findings)
+ if len(actual) != len(expected) {
+ t.Fatalf("unicorn/prevent-abbreviations.ts: want %v, got %v", expected, actual)
+ }
+ for index := range expected {
+ if actual[index] != expected[index] {
+ t.Fatalf("unicorn/prevent-abbreviations.ts[%d]: want %+v, got %+v", index, expected[index], actual[index])
+ }
+ }
+}
+
+func TestUnicornPreventAbbreviationsReportsBindingOnceAndFixesEveryReference(t *testing.T) {
+ source := "function read(idx: number) {\n const value = idx;\n return { idx };\n}\nvoid read;\n"
+ _, _, findings := runRuleFindingsSnapshot(t, unicornPreventAbbreviationsRuleName, source, nil)
+ if len(findings) != 1 {
+ t.Fatalf("expected one binding diagnostic, got %d (%+v)", len(findings), findings)
+ }
+ if len(findings[0].Fix) != 3 {
+ t.Fatalf("expected declaration and two reference edits, got %+v", findings[0].Fix)
+ }
+ assertFixSnapshot(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ source,
+ "function read(index: number) {\n const value = index;\n return { idx: index };\n}\nvoid read;\n",
+ )
+}
+
+func TestUnicornPreventAbbreviationsKeepsShadowedBindingsIndependentAndAvoidsCollisions(t *testing.T) {
+ source := "function outer(err: string) {\n const error = \"kept\";\n return err + error;\n}\nfunction sibling(err: string) {\n return err;\n}\nvoid [outer, sibling];\n"
+ assertFixSnapshot(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ source,
+ "function outer(error_: string) {\n const error = \"kept\";\n return error_ + error;\n}\nfunction sibling(error: string) {\n return error;\n}\nvoid [outer, sibling];\n",
+ )
+}
+
+func TestUnicornPreventAbbreviationsDoesNotCaptureUnresolvedNames(t *testing.T) {
+ source := "function render(err: string): string {\n console.log(error);\n return err;\n}\nvoid render;\n"
+ assertFixSnapshot(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ source,
+ "function render(error_: string): string {\n console.log(error);\n return error_;\n}\nvoid render;\n",
+ )
+}
+
+func TestUnicornPreventAbbreviationsDoesNotTreatDestructuringPropertyKeysAsCollisions(t *testing.T) {
+ source := "function render(err: string, source: { error: string }): string {\n const { error: value } = source;\n return err + value;\n}\nvoid render;\n"
+ assertFixSnapshot(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ source,
+ "function render(error: string, source: { error: string }): string {\n const { error: value } = source;\n return error + value;\n}\nvoid render;\n",
+ )
+}
+
+func TestUnicornPreventAbbreviationsAvoidsCompilerProvidedGlobalBindings(t *testing.T) {
+ source := "class Err {}\nvoid Err;\n"
+ assertFixSnapshot(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ source,
+ "class Error_ {}\nvoid Error_;\n",
+ )
+}
+
+func TestUnicornPreventAbbreviationsRenamesClassInnerAndOuterReferencesTogether(t *testing.T) {
+ source := "class err {\n static create(): err {\n return new err();\n }\n}\nvoid err;\n"
+ assertFixSnapshot(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ source,
+ "class error {\n static create(): error {\n return new error();\n }\n}\nvoid error;\n",
+ )
+}
+
+func TestUnicornPreventAbbreviationsUsesFunctionScopeForVarCollisions(t *testing.T) {
+ source := "function render(condition: boolean): string {\n if (condition) {\n var err = \"first\";\n }\n if (!condition) {\n var error = \"second\";\n }\n return err + error;\n}\nvoid render;\n"
+ assertFixSnapshot(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ source,
+ "function render(condition: boolean): string {\n if (condition) {\n var error_ = \"first\";\n }\n if (!condition) {\n var error = \"second\";\n }\n return error_ + error;\n}\nvoid render;\n",
+ )
+}
+
+func TestUnicornPreventAbbreviationsUsesFunctionScopeForDestructuredVarCollisions(t *testing.T) {
+ source := "function read(source: Record): void {\n if (source.first) {\n var { first: idx } = source;\n console.log(idx);\n }\n if (source.second) {\n var { second: i } = source;\n console.log(i);\n }\n}\nvoid read;\n"
+ assertFixSnapshot(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ source,
+ "function read(source: Record): void {\n if (source.first) {\n var { first: index } = source;\n console.log(index);\n }\n if (source.second) {\n var { second: index_ } = source;\n console.log(index_);\n }\n}\nvoid read;\n",
+ )
+}
+
+func TestUnicornPreventAbbreviationsAvoidsCatchParameterBodyRedeclarations(t *testing.T) {
+ source := "try {\n throw 0;\n} catch (idx) {\n const i = 1;\n console.log(i);\n}\n"
+ assertFixSnapshot(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ source,
+ "try {\n throw 0;\n} catch (index) {\n const index_ = 1;\n console.log(index_);\n}\n",
+ )
+}
+
+func TestUnicornPreventAbbreviationsAvoidsVarLexicalEarlyErrorCollisions(t *testing.T) {
+ source := "function check(values: number[]): void {\n {\n const cur = 0;\n if (cur === 0) {\n var curr = 1;\n console.log(curr);\n }\n }\n for (const idx of values) {\n if (values.length > 0) {\n var i = 1;\n console.log(i);\n }\n }\n try {\n throw { value: 0 };\n } catch ({ value: fn }) {\n if (values.length > 0) {\n var func = 1;\n console.log(func);\n }\n }\n}\nvoid check;\n"
+ assertFixSnapshot(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ source,
+ "function check(values: number[]): void {\n {\n const current = 0;\n if (current === 0) {\n var current_ = 1;\n console.log(current_);\n }\n }\n for (const index of values) {\n if (values.length > 0) {\n var index_ = 1;\n console.log(index_);\n }\n }\n try {\n throw { value: 0 };\n } catch ({ value: function_ }) {\n if (values.length > 0) {\n var function__ = 1;\n console.log(function__);\n }\n }\n}\nvoid check;\n",
+ )
+}
+
+func TestUnicornPreventAbbreviationsSeparatesGeneratedNamesInOneScope(t *testing.T) {
+ source := "const idx = 0;\nconst i = 1;\nconsole.log(idx, i);\n"
+ assertFixSnapshot(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ source,
+ "const index = 0;\nconst index_ = 1;\nconsole.log(index, index_);\n",
+ )
+}
+
+func TestUnicornPreventAbbreviationsAllowsGeneratedNamesToShadowWithoutCapture(t *testing.T) {
+ source := "const errCb = \"outer\";\n{\n const errCb = \"inner\";\n console.log(errCb);\n}\nconsole.log(errCb);\n"
+ assertFixSnapshot(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ source,
+ "const errorCallback = \"outer\";\n{\n const errorCallback = \"inner\";\n console.log(errorCallback);\n}\nconsole.log(errorCallback);\n",
+ )
+}
+
+func TestUnicornPreventAbbreviationsKeepsIndependentLoopAndSwitchScopesIndependent(t *testing.T) {
+ source := "function log(errors: string[], first: number, second: number): void {\n for (const err of errors) console.log(err);\n for (const err of errors) console.log(err);\n switch (first) {\n case 0:\n const err = \"first\";\n console.log(err);\n break;\n }\n switch (second) {\n case 0:\n const err = \"second\";\n console.log(err);\n break;\n }\n}\nvoid log;\n"
+ assertFixSnapshot(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ source,
+ "function log(errors: string[], first: number, second: number): void {\n for (const error of errors) console.log(error);\n for (const error of errors) console.log(error);\n switch (first) {\n case 0:\n const error = \"first\";\n console.log(error);\n break;\n }\n switch (second) {\n case 0:\n const error = \"second\";\n console.log(error);\n break;\n }\n}\nvoid log;\n",
+ )
+}
+
+func TestUnicornPreventAbbreviationsIgnoresNonLexicalJSXAndTupleNamesForCollisions(t *testing.T) {
+ source := "type Pair = [error: string];\nconst btn = document.createElement(\"button\");\nconst err = new Error();\nconst view = ;\nconsole.log(btn, err, view);\n"
+ assertFixSnapshotFile(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ "main.tsx",
+ source,
+ "type Pair = [error: string];\nconst button = document.createElement(\"button\");\nconst error = new Error();\nconst view = ;\nconsole.log(button, error, view);\n",
+ )
+}
+
+func TestUnicornPreventAbbreviationsSeparatesGeneratedNamesAtCrossScopeReads(t *testing.T) {
+ source := "const errCb = \"outer\";\n{\n console.log(errCb);\n const errorCb = \"inner\";\n console.log(errorCb);\n}\n"
+ assertFixSnapshot(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ source,
+ "const errorCallback = \"outer\";\n{\n console.log(errorCallback);\n const errorCallback_ = \"inner\";\n console.log(errorCallback_);\n}\n",
+ )
+}
+
+func TestUnicornPreventAbbreviationsDoesNotCaptureOuterReadsWithUnusedInnerBindings(t *testing.T) {
+ source := "const cur = 1;\n{\n const curr = 2;\n {\n console.log(cur);\n }\n}\n"
+ assertFixSnapshot(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ source,
+ "const current = 1;\n{\n const current_ = 2;\n {\n console.log(current);\n }\n}\n",
+ )
+}
+
+func TestUnicornPreventAbbreviationsExpandsCompoundAndCasedNamesButSkipsConstants(t *testing.T) {
+ source := "class BtnFactory {}\nconst errCb = (): void => {};\nconst err文 = 1;\nconst errʰ = 2;\nconst ENV = \"test\";\nvoid [BtnFactory, errCb, err文, errʰ, ENV];\n"
+ assertFixSnapshot(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ source,
+ "class ButtonFactory {}\nconst errorCallback = (): void => {};\nconst error文 = 1;\nconst errorʰ = 2;\nconst ENV = \"test\";\nvoid [ButtonFactory, errorCallback, error文, errorʰ, ENV];\n",
+ )
+}
+
+func TestUnicornPreventAbbreviationsOffersWholeBindingSuggestionsForAmbiguousNames(t *testing.T) {
+ source := "const e = 1;\nconsole.log(e);\n"
+ _, _, findings := runRuleFindingsSnapshot(t, unicornPreventAbbreviationsRuleName, source, nil)
+ if len(findings) != 1 {
+ t.Fatalf("expected one diagnostic, got %d (%+v)", len(findings), findings)
+ }
+ finding := findings[0]
+ if len(finding.Fix) != 0 || len(finding.Suggestions) != 2 {
+ t.Fatalf("expected two suggestions and no autofix, got fix=%+v suggestions=%+v", finding.Fix, finding.Suggestions)
+ }
+ if finding.Suggestions[0].Title != "Rename to `error`." || len(finding.Suggestions[0].Edits) != 2 ||
+ finding.Suggestions[1].Title != "Rename to `event_`." || len(finding.Suggestions[1].Edits) != 2 {
+ t.Fatalf("unexpected suggestions: %+v", finding.Suggestions)
+ }
+}
+
+func TestUnicornPreventAbbreviationsHonorsReplacementAllowAndIgnoreOptions(t *testing.T) {
+ source := "const err = 1;\nconst cmd = 2;\nconst ignoredCmd = 3;\nconst allowedCmd = 4;\nvoid [err, cmd, ignoredCmd, allowedCmd];\n"
+ options := `{
+ "extendDefaultReplacements": false,
+ "replacements": {"cmd": {"command": true}},
+ "allowList": {"allowedCmd": true},
+ "ignore": ["^ignored"]
+ }`
+ _, _, findings := runRuleFindingsSnapshot(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ source,
+ json.RawMessage(options),
+ )
+ if len(findings) != 1 || !strings.Contains(findings[0].Message, "`cmd`") {
+ t.Fatalf("expected only custom cmd diagnostic, got %+v", findings)
+ }
+ assertFixSnapshotWithOptions(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ source,
+ options,
+ "const err = 1;\nconst command = 2;\nconst ignoredCmd = 3;\nconst allowedCmd = 4;\nvoid [err, command, ignoredCmd, allowedCmd];\n",
+ )
+}
+
+func TestUnicornPreventAbbreviationsHonorsNegativeReplacementAndAllowListPatches(t *testing.T) {
+ assertFixSnapshotWithOptions(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ "const e = new Error();\nvoid e;\n",
+ `{"replacements":{"e":{"event":false}}}`,
+ "const error = new Error();\nvoid error;\n",
+ )
+
+ source := "const defaultProps = {};\nvoid defaultProps;\n"
+ assertRuleSkipsSource(t, unicornPreventAbbreviationsRuleName, source)
+ assertFixSnapshotWithOptions(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ source,
+ `{"allowList":{"defaultProps":false}}`,
+ "const defaultProperties = {};\nvoid defaultProperties;\n",
+ )
+ assertFixSnapshotWithOptions(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ source,
+ `{"extendDefaultAllowList":false}`,
+ "const defaultProperties = {};\nvoid defaultProperties;\n",
+ )
+}
+
+func TestUnicornPreventAbbreviationsFalseReplacementDisablesCompoundMatches(t *testing.T) {
+ source := "const ref = 1;\nconst someRef = ref;\nvoid someRef;\n"
+ assertRuleSkipsSourceWithOptions(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ source,
+ `{"replacements":{"ref":false}}`,
+ )
+}
+
+func TestUnicornPreventAbbreviationsFalseReplacementStopsCaseVariantFallback(t *testing.T) {
+ assertRuleSkipsSourceWithOptions(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ "const err = new Error();\nvoid err;\n",
+ `{"replacements":{"err":false,"Err":{"failure":true}}}`,
+ )
+}
+
+func TestUnicornPreventAbbreviationsSupportsCanonicalEmptyReplacementSpelling(t *testing.T) {
+ assertFixSnapshotWithOptions(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ "const err = new Error();\nvoid err;\n",
+ `{"extendDefaultReplacements":false,"replacements":{"err":{"":true}}}`,
+ "const _ = new Error();\nvoid _;\n",
+ )
+}
+
+func TestUnicornPreventAbbreviationsUsesStrictBindingGrammarForCustomReplacements(t *testing.T) {
+ for _, name := range []string{
+ "arguments", "await", "break", "case", "catch", "class", "const", "continue",
+ "debugger", "default", "delete", "do", "else", "enum", "eval", "export",
+ "extends", "false", "finally", "for", "function", "if", "implements", "import",
+ "in", "instanceof", "interface", "let", "new", "null", "package", "private",
+ "protected", "public", "return", "static", "super", "switch", "this", "throw",
+ "true", "try", "typeof", "var", "void", "while", "with", "yield",
+ } {
+ if unicornPreventAbbreviationsValidIdentifier(name) {
+ t.Fatalf("strict binding reserved word %q must not be used directly", name)
+ }
+ }
+ for _, name := range []string{
+ "any", "as", "boolean", "constructor", "declare", "from", "get", "module",
+ "number", "of", "require", "set", "string", "symbol", "type",
+ } {
+ if !unicornPreventAbbreviationsValidIdentifier(name) {
+ t.Fatalf("contextual TypeScript keyword %q is a valid binding name", name)
+ }
+ }
+
+ assertFixSnapshotWithOptions(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ "export {};\nconst err = new Error();\nvoid err;\n",
+ `{"extendDefaultReplacements":false,"replacements":{"err":{"eval":true}}}`,
+ "export {};\nconst eval_ = new Error();\nvoid eval_;\n",
+ )
+ assertFixSnapshotWithOptions(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ "const err = new Error();\nvoid err;\n",
+ `{"extendDefaultReplacements":false,"replacements":{"err":{"type":true}}}`,
+ "const type = new Error();\nvoid type;\n",
+ )
+}
+
+func TestUnicornPreventAbbreviationsPreservesCanonicalUpperFirstForUncasedPrefixes(t *testing.T) {
+ assertFixSnapshotWithOptions(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ "const $err = new Error();\nvoid $err;\n",
+ `{"extendDefaultReplacements":false,"replacements":{"$err":{"failure":true}}}`,
+ "const Failure = new Error();\nvoid Failure;\n",
+ )
+}
+
+func TestUnicornPreventAbbreviationsPreservesAstralFirstRuneCasing(t *testing.T) {
+ const astralUpper = "\U00010400Name"
+ const astralLower = "\U00010428Name"
+ if actual := lowerUnicornPreventAbbreviationsFirst(astralUpper); actual != astralUpper {
+ t.Fatalf("lower-first must preserve an astral first rune: %q", actual)
+ }
+ if actual := upperUnicornPreventAbbreviationsFirst(astralLower); actual != astralLower {
+ t.Fatalf("upper-first must preserve an astral first rune: %q", actual)
+ }
+ if !unicornPreventAbbreviationsStartsUpper(astralLower) {
+ t.Fatal("an astral first rune must match JavaScript's upper-first classification")
+ }
+}
+
+func TestUnicornPreventAbbreviationsUsesJavaScriptFullUnicodeCasing(t *testing.T) {
+ assertFixSnapshotWithOptions(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ "const ß = 1;\nvoid ß;\n",
+ `{"extendDefaultReplacements":false,"replacements":{"ß":{"sharpSValue":true}}}`,
+ "const sharpSValue = 1;\nvoid sharpSValue;\n",
+ )
+ if actual := upperUnicornPreventAbbreviationsFirst("ßeta"); actual != "SSeta" {
+ t.Fatalf("upper-first must use full Unicode casing: %q", actual)
+ }
+ if actual := lowerUnicornPreventAbbreviationsFirst("İtem"); actual != "i\u0307tem" {
+ t.Fatalf("lower-first must use full Unicode casing: %q", actual)
+ }
+}
+
+func TestUnicornPreventAbbreviationsAppliesInternalImportDefaultsAndPreservesImportedNames(t *testing.T) {
+ source := "import err from \"./local-default\";\nimport * as ctx from \"external-ns\";\nimport doc from \"./node_modules/external-default\";\nimport { prop } from \"./local-named\";\nimport { ref } from \"external-named\";\nvoid [err, ctx, doc, prop, ref];\n"
+ assertFixSnapshot(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ source,
+ "import error from \"./local-default\";\nimport * as ctx from \"external-ns\";\nimport doc from \"./node_modules/external-default\";\nimport { prop as property } from \"./local-named\";\nimport { ref } from \"external-named\";\nvoid [error, ctx, doc, property, ref];\n",
+ )
+ _, _, findings := runRuleFindingsSnapshot(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ source,
+ json.RawMessage(`{"checkDefaultAndNamespaceImports":true,"checkShorthandImports":true}`),
+ )
+ if len(findings) != 5 {
+ t.Fatalf("expected all five imports when enabled, got %d (%+v)", len(findings), findings)
+ }
+ assertRuleSkipsSourceWithOptions(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ source,
+ `{"checkDefaultAndNamespaceImports":false,"checkShorthandImports":false}`,
+ )
+}
+
+func TestUnicornPreventAbbreviationsUsesDefaultImportControlForNamedDefaultSyntax(t *testing.T) {
+ source := "import { default as err } from \"external\";\nvoid err;\n"
+ assertRuleSkipsSource(t, unicornPreventAbbreviationsRuleName, source)
+ assertFixSnapshotWithOptions(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ source,
+ `{"checkDefaultAndNamespaceImports":true}`,
+ "import { default as error } from \"external\";\nvoid error;\n",
+ )
+}
+
+func TestUnicornPreventAbbreviationsPreservesExplicitImportedAliasNames(t *testing.T) {
+ assertFixSnapshot(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ "import { err as err } from \"external\";\nvoid err;\n",
+ "import { err as error } from \"external\";\nvoid error;\n",
+ )
+}
+
+func TestUnicornPreventAbbreviationsDoesNotApplyRequireModeToQualifiedImportEquals(t *testing.T) {
+ source := "namespace Source {\n export const err = new Error();\n}\nimport err = Source.err;\nvoid err;\n"
+ assertFixSnapshotWithOptions(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ source,
+ `{"checkDefaultAndNamespaceImports":false}`,
+ "namespace Source {\n export const err = new Error();\n}\nimport error = Source.err;\nvoid error;\n",
+ )
+}
+
+func TestUnicornPreventAbbreviationsAppliesImportModeToExternalImportEquals(t *testing.T) {
+ source := "import err = require(\"./local\");\nvoid err;\n"
+ assertFixSnapshot(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ source,
+ "import error = require(\"./local\");\nvoid error;\n",
+ )
+ assertRuleSkipsSourceWithOptions(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ source,
+ `{"checkDefaultAndNamespaceImports":false}`,
+ )
+}
+
+func TestUnicornPreventAbbreviationsAppliesImportControlsToStaticRequireBindings(t *testing.T) {
+ source := "declare function require(name: string): unknown;\nconst err = require(\"./local\");\nconst ctx = require(\"external\");\nvoid [err, ctx];\n"
+ assertFixSnapshot(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ source,
+ "declare function require(name: string): unknown;\nconst error = require(\"./local\");\nconst ctx = require(\"external\");\nvoid [error, ctx];\n",
+ )
+ assertRuleSkipsSourceWithOptions(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ source,
+ `{"checkDefaultAndNamespaceImports":false}`,
+ )
+}
+
+func TestUnicornPreventAbbreviationsRecognizesOnlyCanonicalStaticRequireCalls(t *testing.T) {
+ cases := []struct {
+ name string
+ initializer string
+ }{
+ {name: "extra argument", initializer: `require("./local", {})`},
+ {name: "optional call", initializer: `require?.("./local")`},
+ {name: "template argument", initializer: "require(`./local`)"},
+ }
+ for _, testCase := range cases {
+ t.Run(testCase.name, func(t *testing.T) {
+ source := "declare function require(...values: unknown[]): unknown;\nconst err = " + testCase.initializer + ";\nvoid err;\n"
+ assertFixSnapshotWithOptions(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ source,
+ `{"checkDefaultAndNamespaceImports":false}`,
+ "declare function require(...values: unknown[]): unknown;\nconst error = " + testCase.initializer + ";\nvoid error;\n",
+ )
+ })
+ }
+
+ assertRuleSkipsSourceWithOptions(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ "declare function require(...values: unknown[]): unknown;\nconst err = require(\"\");\nvoid err;\n",
+ `{"checkDefaultAndNamespaceImports":false}`,
+ )
+}
+
+func TestUnicornPreventAbbreviationsChecksShorthandDestructuringOnlyWhenEnabled(t *testing.T) {
+ source := "declare const source: { err: Error };\nconst { err } = source;\nconsole.error(err);\n"
+ assertRuleSkipsSource(t, unicornPreventAbbreviationsRuleName, source)
+ assertFixSnapshotWithOptions(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ source,
+ `{"checkShorthandProperties":true}`,
+ "declare const source: { err: Error };\nconst { err: error } = source;\nconsole.error(error);\n",
+ )
+}
+
+func TestUnicornPreventAbbreviationsPreservesExportedAliasNames(t *testing.T) {
+ source := "const err = new Error();\nexport { err };\n"
+ assertFixSnapshot(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ source,
+ "const error = new Error();\nexport { error as err };\n",
+ )
+ assertFixSnapshot(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ "const err = new Error();\nexport { err as err };\n",
+ "const error = new Error();\nexport { error as err };\n",
+ )
+ assertFixSnapshot(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ "const err = new Error();\nexport { err as publicError };\n",
+ "const error = new Error();\nexport { error as publicError };\n",
+ )
+}
+
+func TestUnicornPreventAbbreviationsRenamesDefaultExportLocalNames(t *testing.T) {
+ source := "export default function err(depth: number): void {\n if (depth > 0) err(depth - 1);\n}\n"
+ assertFixSnapshot(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ source,
+ "export default function error(depth: number): void {\n if (depth > 0) error(depth - 1);\n}\n",
+ )
+}
+
+func TestUnicornPreventAbbreviationsPropertyChecksAreOptInAndSuggestionOnly(t *testing.T) {
+ source := "class Store {\n e = 0;\n update(): void {\n this.e = 1;\n }\n}\nvoid Store;\n"
+ assertRuleSkipsSource(t, unicornPreventAbbreviationsRuleName, source)
+ _, _, findings := runRuleFindingsSnapshot(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ source,
+ json.RawMessage(`{"checkVariables":false,"checkProperties":true}`),
+ )
+ if len(findings) != 2 {
+ t.Fatalf("expected property definition and write diagnostics, got %d (%+v)", len(findings), findings)
+ }
+ for _, finding := range findings {
+ if len(finding.Fix) != 0 || len(finding.Suggestions) != 2 {
+ t.Fatalf("properties must be suggestion-only for ambiguous names: %+v", finding)
+ }
+ }
+}
+
+func TestUnicornPreventAbbreviationsDoesNotReportDestructuringAssignmentKeysAsProperties(t *testing.T) {
+ source := "const source = {} as Record;\nlet local = 0;\n({ err: local } = source);\nvoid local;\n"
+ assertRuleSkipsSourceWithOptions(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ source,
+ `{"checkVariables":false,"checkProperties":true}`,
+ )
+}
+
+func TestUnicornPreventAbbreviationsAllowsReservedWordsInPropertySuggestions(t *testing.T) {
+ _, _, findings := runRuleFindingsSnapshot(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ "({e: 1});\n",
+ json.RawMessage(`{
+ "checkVariables": false,
+ "checkProperties": true,
+ "extendDefaultReplacements": false,
+ "replacements": {"e": {"class": true, "function": true}}
+ }`),
+ )
+ if len(findings) != 1 || len(findings[0].Suggestions) != 2 ||
+ findings[0].Suggestions[0].Title != "Rename to `class`." ||
+ findings[0].Suggestions[1].Title != "Rename to `function`." {
+ t.Fatalf("unexpected reserved-word property suggestions: %+v", findings)
+ }
+}
+
+func TestUnicornPreventAbbreviationsChecksPhysicalFilenameWithoutOfferingEdits(t *testing.T) {
+ _, _, findings := runRuleFindingsSnapshotFile(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ "idx.ts",
+ "export {};\n",
+ nil,
+ )
+ if len(findings) != 1 || findings[0].Message != "The filename `idx.ts` should be named `index.ts`. A more descriptive name will do too." ||
+ len(findings[0].Fix) != 0 || len(findings[0].Suggestions) != 0 {
+ t.Fatalf("unexpected filename finding: %+v", findings)
+ }
+ _, _, findings = runRuleFindingsSnapshotFile(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ "idx.ts",
+ "export {};\n",
+ json.RawMessage(`{"checkFilenames":false}`),
+ )
+ if len(findings) != 0 {
+ t.Fatalf("checkFilenames false should suppress filename findings: %+v", findings)
+ }
+ if extension := unicornPreventAbbreviationsFilenameExtension(".err"); extension != "" {
+ t.Fatalf("leading-dot basename must not be treated as an extension: %q", extension)
+ }
+
+ if unicornPreventAbbreviationsIsVirtualFilename("foo.ts") {
+ t.Fatal("a physical filename containing angle brackets must not be treated as virtual")
+ }
+
+ for _, filename := range []string{"", ""} {
+ if !unicornPreventAbbreviationsIsVirtualFilename(filename) {
+ t.Fatalf("virtual filename %q must be recognized", filename)
+ }
+ }
+}
+
+func TestUnicornPreventAbbreviationsKeepsExportedJSDocAndJSXBindingsDiagnosticOnly(t *testing.T) {
+ cases := []struct {
+ name string
+ fileName string
+ source string
+ }{
+ {
+ name: "exported declaration",
+ fileName: "main.ts",
+ source: "export const err = new Error();\n",
+ },
+ {
+ name: "JSDoc parameter",
+ fileName: "main.ts",
+ source: "/** @param err supplied error */\nfunction log(err: Error): void {\n console.error(err);\n}\nvoid log;\n",
+ },
+ {
+ name: "JSX tag",
+ fileName: "main.tsx",
+ source: "const Btn = (): JSX.Element => ;\nconst view = ;\nvoid view;\n",
+ },
+ {
+ name: "merged exported declaration",
+ fileName: "main.ts",
+ source: "interface Ctx {}\nexport namespace Ctx {}\nconst value: Ctx = {};\nvoid value;\n",
+ },
+ {
+ name: "parameter property",
+ fileName: "main.ts",
+ source: "class Store {\n constructor(public err: Error) {}\n}\nvoid Store;\n",
+ },
+ {
+ name: "JSDoc function type parameter",
+ fileName: "main.ts",
+ source: "/** @param ctx middleware context */\ntype Middleware = (ctx: object) => void;\n",
+ },
+ {
+ name: "JSDoc destructured parameter",
+ fileName: "main.ts",
+ source: "/** @param options supplied options */\nfunction log({ cause: err }: { cause: Error }): void {\n console.error(err);\n}\nvoid log;\n",
+ },
+ {
+ name: "exported destructured declaration",
+ fileName: "main.ts",
+ source: "declare const source: { cause: Error };\nexport const { cause: err } = source;\n",
+ },
+ {
+ name: "nested ambient declaration",
+ fileName: "main.ts",
+ source: "declare namespace API {\n const err: Error;\n}\n",
+ },
+ {
+ name: "JSDoc parameter through parentheses",
+ fileName: "main.ts",
+ source: "/** @param ctx middleware context */\nconst middleware = ((ctx: object): object => ctx);\n",
+ },
+ {
+ name: "JSDoc parameter through as assertion",
+ fileName: "main.ts",
+ source: "/** @param ctx middleware context */\nconst middleware = (((ctx: object): object => ctx) as ((value: object) => object));\n",
+ },
+ {
+ name: "JSDoc parameter through satisfies",
+ fileName: "main.ts",
+ source: "/** @param ctx middleware context */\nconst middleware = (((ctx: object): object => ctx) satisfies ((value: object) => object));\n",
+ },
+ {
+ name: "JSDoc parameter through non-null assertion",
+ fileName: "main.ts",
+ source: "/** @param ctx middleware context */\nconst middleware = (((ctx: object): object => ctx)!);\n",
+ },
+ {
+ name: "JSDoc parameter through angle-bracket assertion",
+ fileName: "main.ts",
+ source: "/** @param ctx middleware context */\nconst middleware = <((value: object) => object)>((ctx: object): object => ctx);\n",
+ },
+ }
+ for _, testCase := range cases {
+ t.Run(testCase.name, func(t *testing.T) {
+ _, _, findings := runRuleFindingsSnapshotFile(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ testCase.fileName,
+ testCase.source,
+ nil,
+ )
+ if len(findings) != 1 || len(findings[0].Fix) != 0 || len(findings[0].Suggestions) != 0 {
+ t.Fatalf("expected one diagnostic-only binding, got %+v", findings)
+ }
+ })
+ }
+}
+
+func TestUnicornPreventAbbreviationsIgnoresDetachedJSDocWhenDecidingFixSafety(t *testing.T) {
+ cases := []struct {
+ name string
+ source string
+ want string
+ }{
+ {
+ name: "blank line",
+ source: "/** @param err historical text */\n\nfunction log(err: Error): void {\n console.error(err);\n}\nvoid log;\n",
+ want: "/** @param err historical text */\n\nfunction log(error: Error): void {\n console.error(error);\n}\nvoid log;\n",
+ },
+ {
+ name: "Unicode blank line",
+ source: "/** @param err historical text */\u2028\u2028function log(err: Error): void {\n console.error(err);\n}\nvoid log;\n",
+ want: "/** @param err historical text */\u2028\u2028function log(error: Error): void {\n console.error(error);\n}\nvoid log;\n",
+ },
+ {
+ name: "ordinary intervening comment",
+ source: "/** @param err historical text */\n/* ordinary */\nfunction log(err: Error): void {\n console.error(err);\n}\nvoid log;\n",
+ want: "/** @param err historical text */\n/* ordinary */\nfunction log(error: Error): void {\n console.error(error);\n}\nvoid log;\n",
+ },
+ {
+ name: "longer tag name",
+ source: "/** @parameter err historical text */\nfunction log(err: Error): void {\n console.error(err);\n}\nvoid log;\n",
+ want: "/** @parameter err historical text */\nfunction log(error: Error): void {\n console.error(error);\n}\nvoid log;\n",
+ },
+ }
+ for _, testCase := range cases {
+ t.Run(testCase.name, func(t *testing.T) {
+ assertFixSnapshot(t, unicornPreventAbbreviationsRuleName, testCase.source, testCase.want)
+ })
+ }
+}
+
+func TestUnicornPreventAbbreviationsKeepsTypeScriptSignatureScopesIndependent(t *testing.T) {
+ source := "type First = (ctx: object) => void;\ntype Second = new (ctx: object) => object;\ninterface Third {\n (ctx: object): void;\n method(ctx: object): void;\n}\n"
+ assertFixSnapshot(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ source,
+ "type First = (context: object) => void;\ntype Second = new (context: object) => object;\ninterface Third {\n (context: object): void;\n method(context: object): void;\n}\n",
+ )
+}
+
+func TestUnicornPreventAbbreviationsKeepsMappedAndConditionalTypeScopesIndependent(t *testing.T) {
+ source := "type Pair = [\n T extends infer Ctx ? Ctx : never,\n U extends infer Ctx ? Ctx : never,\n { [Ctx in keyof T]: T[Ctx] },\n { [Ctx in keyof U]: U[Ctx] },\n];\n"
+ assertFixSnapshot(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ source,
+ "type Pair = [\n T extends infer Context ? Context : never,\n U extends infer Context ? Context : never,\n { [Context in keyof T]: T[Context] },\n { [Context in keyof U]: U[Context] },\n];\n",
+ )
+}
+
+func TestUnicornPreventAbbreviationsRenamesMergedTypeAndValueReferencesTogether(t *testing.T) {
+ source := "interface Prop {\n id: number;\n}\nconst Prop: Prop = { id: 1 };\nexport default Prop;\n"
+ assertFixSnapshot(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ source,
+ "interface Property {\n id: number;\n}\nconst Property: Property = { id: 1 };\nexport default Property;\n",
+ )
+}
+
+func TestUnicornPreventAbbreviationsRenamesTypePredicateReferences(t *testing.T) {
+ source := "function isString(val: unknown): val is string {\n return typeof val === \"string\";\n}\ntype AssertString = (val: unknown) => asserts val is string;\nvoid isString;\n"
+ assertFixSnapshot(
+ t,
+ unicornPreventAbbreviationsRuleName,
+ source,
+ "function isString(value: unknown): value is string {\n return typeof value === \"string\";\n}\ntype AssertString = (value: unknown) => asserts value is string;\nvoid isString;\n",
+ )
+}
+
+func TestUnicornPreventAbbreviationsRejectsMalformedOptions(t *testing.T) {
+ invalid := []string{
+ `[]`,
+ `null`,
+ `{"unknown":true}`,
+ `{"CheckVariables":false}`,
+ `{"checkVariables":null}`,
+ `{"checkShorthandImports":"external"}`,
+ `{"replacements":{"err":true}}`,
+ `{"replacements":{"err":null}}`,
+ `{"allowList":{"err":"yes"}}`,
+ `{"ignore":["("]}`,
+ `{"ignore":["^skip","^skip"]}`,
+ }
+ rule := unicornPreventAbbreviations{}
+ for _, options := range invalid {
+ if err := rule.ValidateOptions(json.RawMessage(options)); err == nil {
+ t.Fatalf("expected options to fail validation: %s", options)
+ }
+ }
+}
+
+func TestCommandFixUnicornPreventAbbreviationsReparsesAndIsIdempotent(t *testing.T) {
+ root := seedLintProject(t, "const idx = 0;\nconsole.log({ idx });\n")
+ seedLintRules(t, root, map[string]string{unicornPreventAbbreviationsRuleName: "error"})
+ for pass := 1; pass <= 2; pass++ {
+ code, stdout, stderr := captureCommandOutput(t, func() int {
+ return run([]string{"fix", "--cwd", root, "--plugins-json", lintManifest(t)})
+ })
+ if code != 0 || stdout != "" || stderr != "" {
+ t.Fatalf("fix pass %d mismatch: code=%d stdout=%q stderr=%q", pass, code, stdout, stderr)
+ }
+ }
+ got, err := os.ReadFile(filepath.Join(root, "src", "main.ts"))
+ if err != nil {
+ t.Fatalf("ReadFile: %v", err)
+ }
+ want := "const index = 0;\nconsole.log({ idx: index });\n"
+ if string(got) != want {
+ t.Fatalf("fixed source mismatch:\nwant %q\ngot %q", want, string(got))
+ }
+}
+
+func TestCommandFixUnicornPreventAbbreviationsKeepsCrossFileMergesDiagnosticOnly(t *testing.T) {
+ const mainSource = "interface Ctx { first: string }\n"
+ const otherSource = "interface Ctx { second: string }\n"
+ root := seedLintProject(t, mainSource)
+ otherPath := filepath.Join(root, "src", "other.ts")
+ writeFile(t, otherPath, otherSource)
+ writeFile(t, filepath.Join(root, "tsconfig.json"), `{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "commonjs",
+ "strict": true,
+ "rootDir": "src",
+ "outDir": "dist"
+ },
+ "files": ["src/main.ts", "src/other.ts"]
+}
+`)
+ seedLintRules(t, root, map[string]string{unicornPreventAbbreviationsRuleName: "error"})
+
+ code, stdout, stderr := captureCommandOutput(t, func() int {
+ return run([]string{"fix", "--cwd", root, "--plugins-json", lintManifest(t)})
+ })
+ if code != 2 || stdout != "" || !strings.Contains(stderr, "["+unicornPreventAbbreviationsRuleName+"]") {
+ t.Fatalf("cross-file fix mismatch: code=%d stdout=%q stderr=%q", code, stdout, stderr)
+ }
+ assertFileText(t, filepath.Join(root, "src", "main.ts"), mainSource)
+ assertFileText(t, otherPath, otherSource)
}
diff --git a/tests/test-lint/src/cases/unicorn-prevent-abbreviations.ts b/tests/test-lint/src/cases/unicorn-prevent-abbreviations.ts
index 139854347..8f63d3173 100644
--- a/tests/test-lint/src/cases/unicorn-prevent-abbreviations.ts
+++ b/tests/test-lint/src/cases/unicorn-prevent-abbreviations.ts
@@ -1 +1,6 @@
-// @ttsc-corpus-skip: unicorn/prevent-abbreviations not yet implemented; fixture exists as the link target referenced from packages/lint/README.md and website/src/content/docs/lint/rules/unicorn.mdx. The skip directive is removed and replaced with a `// expect:` annotation once the rule lands in this PR (feat/lint-unicorn-rules).
+// expect: unicorn/prevent-abbreviations error
+const errCb = (error: Error): void => {
+ console.error(error);
+};
+
+errCb(new Error("fixture"));
diff --git a/tests/test-lint/src/features/plugin/test_lib_index_d_ts_rule_options_autocomplete_per_rule.ts b/tests/test-lint/src/features/plugin/test_lib_index_d_ts_rule_options_autocomplete_per_rule.ts
index c7341b607..7001fe85d 100644
--- a/tests/test-lint/src/features/plugin/test_lib_index_d_ts_rule_options_autocomplete_per_rule.ts
+++ b/tests/test-lint/src/features/plugin/test_lib_index_d_ts_rule_options_autocomplete_per_rule.ts
@@ -34,6 +34,7 @@ import assert from "node:assert/strict";
* - Cross-rule option leakage (`testIdPattern` on
* `cypress/unsafe-to-chain-command`) is rejected.
* - A typo in a switch-exhaustiveness option is rejected.
+ * - Unicorn replacement maps and import modes retain their exact public shape.
* - An empty object policy for `typescript/ban-ts-comment` is rejected because
* the object form requires `descriptionFormat`.
* - A lint-only rule (`no-var`) cannot carry an options object.
@@ -138,6 +139,17 @@ export const test_lib_index_d_ts_rule_options_autocomplete_per_rule = () => {
requireDefaultForNonUnion: true,
},
],
+ "unicorn/prevent-abbreviations": [
+ "error",
+ {
+ checkDefaultAndNamespaceImports: "internal",
+ checkProperties: true,
+ replacements: {
+ cmd: { command: true },
+ ref: false,
+ },
+ },
+ ],
},
};
@@ -344,6 +356,30 @@ export const test_lib_index_d_ts_rule_options_autocomplete_per_rule = () => {
],
},
};
+ const unicornImportModeTypo: ITtscLintConfig = {
+ rules: {
+ "unicorn/prevent-abbreviations": [
+ "error",
+ {
+ // @ts-expect-error — import checks accept booleans or the canonical `internal` mode.
+ checkShorthandImports: "external",
+ },
+ ],
+ },
+ };
+ const unicornReplacementShape: ITtscLintConfig = {
+ rules: {
+ "unicorn/prevent-abbreviations": [
+ "error",
+ {
+ replacements: {
+ // @ts-expect-error — a replacement entry is `false` or a boolean-valued replacement map.
+ err: true,
+ },
+ },
+ ],
+ },
+ };
const banTsCommentMissingDescriptionFormat: ITtscLintConfig = {
rules: {
"typescript/ban-ts-comment": [
@@ -384,6 +420,8 @@ export const test_lib_index_d_ts_rule_options_autocomplete_per_rule = () => {
assert.ok(crossRuleShape);
assert.ok(lintRuleWithOptions);
assert.ok(switchOptionTypo);
+ assert.ok(unicornImportModeTypo);
+ assert.ok(unicornReplacementShape);
assert.ok(banTsCommentMissingDescriptionFormat);
assert.ok(camelBuiltinName);
};
diff --git a/website/src/content/docs/lint/rules/unicorn.mdx b/website/src/content/docs/lint/rules/unicorn.mdx
index d9208e8b4..386b40a87 100644
--- a/website/src/content/docs/lint/rules/unicorn.mdx
+++ b/website/src/content/docs/lint/rules/unicorn.mdx
@@ -6,7 +6,7 @@ They span array iteration, string and regex idioms, Node.js APIs, error handling
They rewrite legacy patterns into modern counterparts, forbid known anti-patterns, and pin a consistent style for things [Core](./core) and [TypeScript](./typescript) leave underspecified.
-Pure-AST; no checker dependencies.
+Most rules are pure AST checks. Binding-aware rules use the TypeScript checker when lexical identity is part of the contract.
Source: [`eslint-plugin-unicorn`](https://github.com/sindresorhus/eslint-plugin-unicorn) (MIT).
@@ -170,7 +170,7 @@ Rules whose native port is registered but intentionally diagnostic-free are mark
- [`unicorn/new-for-builtins`](#rule-unicorn-new-for-builtins): Require `new` when calling builtin constructors like `Error`, `Map`, `Set`, `Date`.
- [`unicorn/number-literal-case`](#rule-unicorn-number-literal-case): Enforce one consistent case for the prefix and digits of hex / binary / octal literals.
- [`unicorn/numeric-separators-style`](#rule-unicorn-numeric-separators-style): Enforce `_` separator grouping in numeric literals.
-- [`unicorn/prevent-abbreviations`](#rule-unicorn-prevent-abbreviations): Reject common identifier abbreviations and replace them with their long forms.
+- [`unicorn/prevent-abbreviations`](#rule-unicorn-prevent-abbreviations): Apply canonical replacement semantics to bindings, compound names, opt-in property checks, and filenames.
- [`unicorn/relative-url-style`](#rule-unicorn-relative-url-style): Enforce a single style for relative URLs passed to `new URL`.
- [`unicorn/string-content`](#rule-unicorn-string-content): Enforce or replace configured string-content patterns.
- [`unicorn/switch-case-braces`](#rule-unicorn-switch-case-braces): Enforce a consistent presence/absence of `{}` braces around `case` clauses inside `switch`.
@@ -2131,13 +2131,37 @@ function f(x: unknown) {
### `unicorn/prevent-abbreviations`
-Reject common identifier abbreviations (`btn`, `arr`, `idx`) and replace them with their long forms.
+Apply the complete canonical replacement table to lexical bindings and their references, compound and cased names, and physical filenames.
+
+The established `unicorn/prevent-abbreviations` ID is retained for configuration compatibility. Its behavior follows the final upstream version before `eslint-plugin-unicorn` renamed the rule to `name-replacements` in June 2026.
+
+A binding is diagnosed once at its declaration. One collision-free replacement is autofixed across every reference, while ambiguous replacements are editor suggestions. Bindings declared with a named `export` modifier, plus ambient, cross-file-merged, JSX tag, `.vue` source, parameter-property, and attached-JSDoc `@param` bindings, remain diagnostic-only because a source-only rename cannot safely preserve their external names or references. A local binding re-exported through an export list remains fixable because the fixer preserves the external spelling (`export { err }` becomes `export { error as err }`). Default-export local names can likewise change without changing the external `default` name. The conservative `.vue` gate protects template references that are outside TypeScript's source-file AST.
+
+The option object supports `replacements`, `extendDefaultReplacements`, `allowList`, `extendDefaultAllowList`, `ignore`, `checkVariables`, `checkProperties`, `checkFilenames`, `checkShorthandProperties`, `checkDefaultAndNamespaceImports`, and `checkShorthandImports`. Import controls accept `true`, `false`, or `"internal"`. The `ignore` option accepts regular-expression strings.
Example:
```ts
-// reports: unicorn/prevent-abbreviations (error)
+// reports once and fixes both occurrences to `button`
const btn = document.querySelector("button");
+btn?.focus();
+```
+
+```ts
+export default {
+ rules: {
+ "unicorn/prevent-abbreviations": [
+ "error",
+ {
+ checkProperties: true,
+ replacements: {
+ cmd: { command: true },
+ ref: false,
+ },
+ },
+ ],
+ },
+};
```