Merge pull request #2101 from github/robertbrignull/unique-command-use-query

Add unique-command-use.ql
This commit is contained in:
Robert
2023-02-28 09:15:18 +00:00
committed by GitHub

View File

@@ -0,0 +1,134 @@
/**
* @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 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"