Repository: oclif/core
Pull request #540 (2022-10-28 18:11:21)

feat: warn about alias deprecations
- Adds `deprecateAliases` to `Command` - allowing you to emit warnings anytime a command alias is used
- Adds `deprecateAliases` to all flags - allowing you to emit warnings anytime a flag alias is used
- Parser is now interprets single character aliases as short char flags


Patch:
diff --git a/package.json b/package.json
index 6e863d5..9e2facd 100644
--- a/package.json
+++ b/package.json
@@ -114,4 +114,4 @@
     "pretest": "yarn build --noEmit && tsc -p test --noEmit --skipLibCheck"
   },
   "types": "lib/index.d.ts"
-}
\ No newline at end of file
+}
diff --git a/src/command.ts b/src/command.ts
index 7efd940..f054a50 100644
--- a/src/command.ts
+++ b/src/command.ts
@@ -9,7 +9,7 @@ import {PrettyPrintableError} from './errors'
 import * as Parser from './parser'
 import * as Flags from './flags'
 import {Deprecation} from './interfaces/parser'
-import {formatCommandDeprecationWarning, formatFlagDeprecationWarning} from './help/util'
+import {formatCommandDeprecationWarning, formatFlagDeprecationWarning, normalizeArgv} from './help/util'
 
 const pjson = require('../package.json')
 
@@ -62,6 +62,11 @@ export default abstract class Command {
 
   static deprecationOptions?: Deprecation;
 
+  /**
+   * Emit deprecation warning when a command alias is used
+   */
+  static deprecateAliases?: boolean
+
   /**
    * An override string (or strings) for the default usage documentation.
    */
@@ -255,10 +260,27 @@ export default abstract class Command {
       if (deprecated) {
         this.warn(formatFlagDeprecationWarning(flag, deprecated))
       }
+
+      const deprecateAliases = this.ctor.flags[flag]?.deprecateAliases
+      const aliases = (this.ctor.flags[flag]?.aliases ?? []).map(a => a.length === 1 ? `-${a}` : `--${a}`)
+      if (deprecateAliases && aliases.length > 0) {
+        const foundAliases = this.argv.filter(a => aliases.includes(a))
+        for (const alias of foundAliases) {
+          this.warn(formatFlagDeprecationWarning(alias, {to: this.ctor.flags[flag]?.name}))
+        }
+      }
     }
   }
 
   protected warnIfCommandDeprecated(): void {
+    const [id] = normalizeArgv(this.config)
+
+    if (this.ctor.deprecateAliases && this.ctor.aliases.includes(id)) {
+      const cmdName = toConfiguredId(this.ctor.id, this.config)
+      const aliasName = toConfiguredId(id, this.config)
+      this.warn(formatCommandDeprecationWarning(aliasName, {to: cmdName}))
+    }
+
     if (this.ctor.state === 'deprecated') {
       const cmdName = toConfiguredId(this.ctor.id, this.config)
       this.warn(formatCommandDeprecationWarning(cmdName, this.ctor.deprecationOptions))
diff --git a/src/config/config.ts b/src/config/config.ts
index f24a335..6ca1e57 100644
--- a/src/config/config.ts
+++ b/src/config/config.ts
@@ -754,6 +754,7 @@ export async function toCached(c: Command.Class, plugin?: IPlugin): Promise<Comm
         relationships: flag.relationships,
         exclusive: flag.exclusive,
         deprecated: flag.deprecated,
+        deprecateAliases: c.deprecateAliases,
         aliases: flag.aliases,
       }
     } else {
@@ -775,6 +776,7 @@ export async function toCached(c: Command.Class, plugin?: IPlugin): Promise<Comm
         exclusive: flag.exclusive,
         default: await defaultToCached(flag),
         deprecated: flag.deprecated,
+        deprecateAliases: c.deprecateAliases,
         aliases: flag.aliases,
       }
       // a command-level placeholder in the manifest so that oclif knows it should regenerate the command during help-time
@@ -809,6 +811,7 @@ export async function toCached(c: Command.Class, plugin?: IPlugin): Promise<Comm
     aliases: c.aliases || [],
     examples: c.examples || (c as any).example,
     deprecationOptions: c.deprecationOptions,
+    deprecateAliases: c.deprecateAliases,
     flags,
     args,
   }
diff --git a/src/flags.ts b/src/flags.ts
index 6fa1fde..dc94eff 100644
--- a/src/flags.ts
+++ b/src/flags.ts
@@ -37,7 +37,7 @@ export const help = (opts: Partial<BooleanFlag<boolean>> = {}) => {
     description: 'Show CLI help.',
     ...opts,
     parse: async (_: any, cmd: Command) => {
-      new Help(cmd.config).showHelp(cmd.argv)
+      new Help(cmd.config).showHelp(cmd.id ? [cmd.id, ...cmd.argv] : cmd.argv)
       cmd.exit(0)
     },
   })
diff --git a/src/help/index.ts b/src/help/index.ts
index 93df1cc..569113c 100644
--- a/src/help/index.ts
+++ b/src/help/index.ts
@@ -9,7 +9,7 @@ import {formatCommandDeprecationWarning, getHelpFlagAdditions, standardizeIDFrom
 import {HelpFormatter} from './formatter'
 import {toCached} from '../config/config'
 export {CommandHelp} from './command'
-export {standardizeIDFromArgv, loadHelpClass, getHelpFlagAdditions} from './util'
+export {standardizeIDFromArgv, loadHelpClass, getHelpFlagAdditions, normalizeArgv} from './util'
 
 function getHelpSubject(args: string[], config: Interfaces.Config): string | undefined {
   // for each help flag that starts with '--' create a new flag with same name sans '--'
diff --git a/src/help/util.ts b/src/help/util.ts
index 95c5942..e84357d 100644
--- a/src/help/util.ts
+++ b/src/help/util.ts
@@ -108,7 +108,7 @@ export function formatFlagDeprecationWarning(flag: string, opts: true | Deprecat
     message += ` and will be removed in version ${opts.version}`
   }
 
-  message += opts.to ? `. Use "${opts.to}" instead.` : '.'
+  message += opts.to ? `. Use "--${opts.to}" instead.` : '.'
 
   return message
 }
@@ -127,3 +127,8 @@ export function formatCommandDeprecationWarning(command: string, opts?: Deprecat
 
   return message
 }
+
+export function normalizeArgv(config: IConfig, argv = process.argv.slice(2)): string[] {
+  if (config.topicSeparator !== ':' && !argv[0]?.includes(':')) argv = standardizeIDFromArgv(argv, config)
+  return argv
+}
diff --git a/src/interfaces/command.ts b/src/interfaces/command.ts
index 438837b..52446b9 100644
--- a/src/interfaces/command.ts
+++ b/src/interfaces/command.ts
@@ -22,6 +22,11 @@ export interface CommandProps {
    */
   deprecationOptions?: Deprecation;
 
+  /**
+   * Emit a deprecation warning when a command alias is used.
+   */
+  deprecateAliases?: boolean
+
   /** An array of aliases for this command. */
   aliases: string[];
 
diff --git a/src/interfaces/parser.ts b/src/interfaces/parser.ts
index 54090a9..6b47962 100644
--- a/src/interfaces/parser.ts
+++ b/src/interfaces/parser.ts
@@ -157,6 +157,10 @@ export type FlagProps = {
    * Alternate names that can be used for this flag.
    */
   aliases?: string[];
+  /**
+   * Emit deprecation warning when a flag alias is provided
+   */
+  deprecateAliases?: boolean
 }
 
 export type BooleanFlagProps = FlagProps & {
diff --git a/src/main.ts b/src/main.ts
index 21b1770..c3e0585 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -5,7 +5,7 @@ import {format, inspect} from 'util'
 import * as Interfaces from './interfaces'
 import {URL} from 'url'
 import {Config} from './config'
-import {getHelpFlagAdditions, loadHelpClass, standardizeIDFromArgv} from './help'
+import {getHelpFlagAdditions, loadHelpClass, normalizeArgv} from './help'
 
 const log = (message = '', ...args: any[]) => {
   // tslint:disable-next-line strict-type-predicates
@@ -41,8 +41,7 @@ export async function run(argv = process.argv.slice(2), options?: Interfaces.Loa
   // return Main.run(argv, options)
   const config = await Config.load(options || (module.parent && module.parent.parent && module.parent.parent.filename) || __dirname) as Config
 
-  if (config.topicSeparator !== ':' && !argv[0]?.includes(':')) argv = standardizeIDFromArgv(argv, config)
-  let [id, ...argvSlice] = argv
+  let [id, ...argvSlice] = normalizeArgv(config, argv)
   // run init hook
   await config.runHook('init', {id, argv: argvSlice})
 
diff --git a/src/parser/parse.ts b/src/parser/parse.ts
index c58df74..ffd8c7d 100644
--- a/src/parser/parse.ts
+++ b/src/parser/parse.ts
@@ -79,8 +79,12 @@ export class Parser<T extends ParserInput, TFlags extends OutputFlags<T['flags']
       }
     }
 
-    const findShortFlag = (arg: string) => {
-      return Object.keys(this.input.flags).find(k => this.input.flags[k].char === arg[1])
+    const findShortFlag = ([_, char]: string) => {
+      if (this.flagAliases[char]) {
+        return this.flagAliases[char].name
+      }
+
+      return Object.keys(this.input.flags).find(k => this.input.flags[k].char === char)
     }
 
     const parseFlag = (arg: string): boolean => {


Test patch:
diff --git a/test/command/command.test.ts b/test/command/command.test.ts
index e8b7629..f6f2118 100644
--- a/test/command/command.test.ts
+++ b/test/command/command.test.ts
@@ -233,6 +233,65 @@ describe('command', () => {
     .it('uses util.format()')
   })
 
+  describe('flags with deprecated aliases', () => {
+    class CMD extends Command {
+      static flags = {
+        name: Flags.string({
+          aliases: ['username', 'target-user', 'u'],
+          deprecateAliases: true,
+        }),
+        other: Flags.string(),
+      }
+
+      async run() {
+        await this.parse(CMD)
+        this.log('running command')
+      }
+    }
+
+    fancy
+    .stdout()
+    .stderr()
+    .do(async () => CMD.run(['--username', 'astro']))
+    .do(ctx => expect(ctx.stderr).to.include('Warning: The "--username" flag has been deprecated'))
+    .it('shows warning for deprecated flag alias')
+
+    fancy
+    .stdout()
+    .stderr()
+    .do(async () => CMD.run(['--target-user', 'astro']))
+    .do(ctx => expect(ctx.stderr).to.include('Warning: The "--target-user" flag has been deprecated'))
+    .it('shows warning for deprecated flag alias')
+
+    fancy
+    .stdout()
+    .stderr()
+    .do(async () => CMD.run(['-u', 'astro']))
+    .do(ctx => expect(ctx.stderr).to.include('Warning: The "-u" flag has been deprecated'))
+    .it('shows warning for deprecated short char flag alias')
+
+    fancy
+    .stdout()
+    .stderr()
+    .do(async () => CMD.run(['--name', 'username']))
+    .do(ctx => expect(ctx.stderr).to.be.empty)
+    .it('shows no warning when using proper flag name with a value that matches a flag alias')
+
+    fancy
+    .stdout()
+    .stderr()
+    .do(async () => CMD.run(['--other', 'target-user']))
+    .do(ctx => expect(ctx.stderr).to.be.empty)
+    .it('shows no warning when using another flag with a value that matches a deprecated flag alias')
+
+    fancy
+    .stdout()
+    .stderr()
+    .do(async () => CMD.run(['--name', 'u']))
+    .do(ctx => expect(ctx.stderr).to.be.empty)
+    .it('shows no warning when proper flag name with a value that matches a short char flag alias')
+  })
+
   describe('deprecated flags', () => {
     fancy
     .stdout()