/**
 * @name A VS Code command should not be used in multiple locations
 * @kind problem
 * @problem.severity warning
 * @id vscode-codeql/unique-command-use
 * @description Using each VS Code command from only one location makes
 * our telemetry more useful, because we can differentiate more user
 * interactions and know which features of the UI our users are using.
 * To fix this alert, new commands will need to be made so that each one
 * is only used from one location. The commands should share the same
 * implementation so we do not introduce duplicate code.
 * When fixing this alert, search the codebase for all other references
 * to the command name. The location of the alert is an arbitrarily
 * chosen usage of the command, and may not necessarily be the location
 * that should be changed to fix the alert.
 */

 import javascript

 /**
  * The name of a VS Code command.
  */
 class CommandName extends string {
   CommandName() { exists(CommandUsage e | e.getCommandName() = this) }
 
   /**
    * In how many ways is this command used. Will always be at least 1.
    */
   int getNumberOfUsages() { result = count(this.getAUse()) }

   /**
    * Get a usage of this command.
    */
   CommandUsage getAUse() { result.getCommandName() = this }
 
   /**
    * Get the canonical first usage of this command, to use for the location
    * of the alert. The implementation of this ordering of usages is arbitrary
    * and the usage given may not be the one that should be changed when fixing
    * the alert.
    */
   CommandUsage getFirstUsage() {
     result =
       max(CommandUsage use |
         use = this.getAUse()
       |
         use
         order by
           use.getFile().getRelativePath(), use.getLocation().getStartLine(),
           use.getLocation().getStartColumn()
       )
   }
 }
 
 /**
  * Represents a single usage of a command, either from within code or
  * from the command's definition in package.json
  */
 abstract class CommandUsage extends Locatable {
   abstract string getCommandName();
 }
 
 /**
  * A usage of a command from the typescript code, by calling `executeCommand`.
  */
 class CommandUsageCallExpr extends CommandUsage, CallExpr {
   CommandUsageCallExpr() {
     this.getCalleeName() = "executeCommand" and
     this.getArgument(0).(StringLiteral).getValue().matches("%codeQL%") and
     not this.getFile().getRelativePath().matches("extensions/ql-vscode/test/%")
   }
 
   override string getCommandName() { result = this.getArgument(0).(StringLiteral).getValue() }
 }

 /**
  * A usage of a command from the typescript code, by calling `CommandManager.execute`.
  */
 class CommandUsageCommandManagerMethodCallExpr extends CommandUsage, MethodCallExpr {
   CommandUsageCommandManagerMethodCallExpr() {
     this.getCalleeName() = "execute" and
     this.getReceiver().getType().unfold().(TypeReference).getTypeName().getName() = "CommandManager" and
     this.getArgument(0).(StringLiteral).getValue().matches("%codeQL%") and
     not this.getFile().getRelativePath().matches("extensions/ql-vscode/test/%")
   }

   override string getCommandName() { result = this.getArgument(0).(StringLiteral).getValue() }
 }
 
 /**
  * A usage of a command from any menu that isn't the command palette.
  * This means a user could invoke the command by clicking on a button in
  * something like a menu or a dropdown.
  */
 class CommandUsagePackageJsonMenuItem extends CommandUsage, JsonObject {
   CommandUsagePackageJsonMenuItem() {
     exists(this.getPropValue("command")) and
     exists(PackageJson packageJson, string menuName |
       packageJson
           .getPropValue("contributes")
           .getPropValue("menus")
           .getPropValue(menuName)
           .getElementValue(_) = this and
       menuName != "commandPalette"
     )
   }
 
   override string getCommandName() { result = this.getPropValue("command").getStringValue() }
 }
 
 /**
  * Is the given command disabled for use in the command palette by
  * a block with a `"when": "false"` field.
  */
 predicate isDisabledInCommandPalette(string commandName) {
   exists(PackageJson packageJson, JsonObject commandPaletteObject |
     packageJson
         .getPropValue("contributes")
         .getPropValue("menus")
         .getPropValue("commandPalette")
         .getElementValue(_) = commandPaletteObject and
     commandPaletteObject.getPropValue("command").getStringValue() = commandName and
     commandPaletteObject.getPropValue("when").getStringValue() = "false"
   )
 }
 
 /**
  * Represents a command being usable from the command palette.
  * This means that a user could choose to manually invoke the command.
  */
 class CommandUsagePackageJsonCommandPalette extends CommandUsage, JsonObject {
   CommandUsagePackageJsonCommandPalette() {
     this.getFile().getBaseName() = "package.json" and
     exists(this.getPropValue("command")) and
     exists(PackageJson packageJson |
       packageJson.getPropValue("contributes").getPropValue("commands").getElementValue(_) = this
     ) and
     not isDisabledInCommandPalette(this.getPropValue("command").getStringValue())
   }
 
   override string getCommandName() { result = this.getPropValue("command").getStringValue() }
 }
 
 from CommandName c
 where c.getNumberOfUsages() > 1
 select c.getFirstUsage(),
   "The " + c + " command is used from " + c.getNumberOfUsages() + " locations"
 