160 lines
5.6 KiB
Plaintext
160 lines
5.6 KiB
Plaintext
/**
|
|
* @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()
|
|
)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Matches one of the members of `BuiltInVsCodeCommands` from `extensions/ql-vscode/src/common/commands.ts`.
|
|
*/
|
|
class BuiltInVSCodeCommand extends string {
|
|
BuiltInVSCodeCommand() {
|
|
exists(TypeAliasDeclaration tad |
|
|
tad.getIdentifier().getName() = "BuiltInVsCodeCommands" and
|
|
tad.getDefinition().(InterfaceTypeExpr).getAMember().getName() = this
|
|
)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 and not c instanceof BuiltInVSCodeCommand
|
|
select c.getFirstUsage(),
|
|
"The " + c + " command is used from " + c.getNumberOfUsages() + " locations"
|