Skip to content

Commit badbee1

Browse files
Merge pull request #2101 from github/robertbrignull/unique-command-use-query
Add unique-command-use.ql
2 parents 2f92ea3 + 4d73e1a commit badbee1

File tree

1 file changed

+134
-0
lines changed

1 file changed

+134
-0
lines changed
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/**
2+
* @name A VS Code command should not be used in multiple locations
3+
* @kind problem
4+
* @problem.severity warning
5+
* @id vscode-codeql/unique-command-use
6+
* @description Using each VS Code command from only one location makes
7+
* our telemetry more useful, because we can differentiate more user
8+
* interactions and know which features of the UI our users are using.
9+
* To fix this alert, new commands will need to be made so that each one
10+
* is only used from one location. The commands should share the same
11+
* implementation so we do not introduce duplicate code.
12+
* When fixing this alert, search the codebase for all other references
13+
* to the command name. The location of the alert is an arbitrarily
14+
* chosen usage of the command, and may not necessarily be the location
15+
* that should be changed to fix the alert.
16+
*/
17+
18+
import javascript
19+
20+
/**
21+
* The name of a VS Code command.
22+
*/
23+
class CommandName extends string {
24+
CommandName() { exists(CommandUsage e | e.getCommandName() = this) }
25+
26+
/**
27+
* In how many ways is this command used. Will always be at least 1.
28+
*/
29+
int getNumberOfUsages() { result = count(this.getAUse()) }
30+
31+
/**
32+
* Get a usage of this command.
33+
*/
34+
CommandUsage getAUse() { result.getCommandName() = this }
35+
36+
/**
37+
* Get the canonical first usage of this command, to use for the location
38+
* of the alert. The implementation of this ordering of usages is arbitrary
39+
* and the usage given may not be the one that should be changed when fixing
40+
* the alert.
41+
*/
42+
CommandUsage getFirstUsage() {
43+
result =
44+
max(CommandUsage use |
45+
use = this.getAUse()
46+
|
47+
use
48+
order by
49+
use.getFile().getRelativePath(), use.getLocation().getStartLine(),
50+
use.getLocation().getStartColumn()
51+
)
52+
}
53+
}
54+
55+
/**
56+
* Represents a single usage of a command, either from within code or
57+
* from the command's definition in package.json
58+
*/
59+
abstract class CommandUsage extends Locatable {
60+
abstract string getCommandName();
61+
}
62+
63+
/**
64+
* A usage of a command from the typescript code, by calling `executeCommand`.
65+
*/
66+
class CommandUsageCallExpr extends CommandUsage, CallExpr {
67+
CommandUsageCallExpr() {
68+
this.getCalleeName() = "executeCommand" and
69+
this.getArgument(0).(StringLiteral).getValue().matches("%codeQL%") and
70+
not this.getFile().getRelativePath().matches("extensions/ql-vscode/test/%")
71+
}
72+
73+
override string getCommandName() { result = this.getArgument(0).(StringLiteral).getValue() }
74+
}
75+
76+
/**
77+
* A usage of a command from any menu that isn't the command palette.
78+
* This means a user could invoke the command by clicking on a button in
79+
* something like a menu or a dropdown.
80+
*/
81+
class CommandUsagePackageJsonMenuItem extends CommandUsage, JsonObject {
82+
CommandUsagePackageJsonMenuItem() {
83+
exists(this.getPropValue("command")) and
84+
exists(PackageJson packageJson, string menuName |
85+
packageJson
86+
.getPropValue("contributes")
87+
.getPropValue("menus")
88+
.getPropValue(menuName)
89+
.getElementValue(_) = this and
90+
menuName != "commandPalette"
91+
)
92+
}
93+
94+
override string getCommandName() { result = this.getPropValue("command").getStringValue() }
95+
}
96+
97+
/**
98+
* Is the given command disabled for use in the command palette by
99+
* a block with a `"when": "false"` field.
100+
*/
101+
predicate isDisabledInCommandPalette(string commandName) {
102+
exists(PackageJson packageJson, JsonObject commandPaletteObject |
103+
packageJson
104+
.getPropValue("contributes")
105+
.getPropValue("menus")
106+
.getPropValue("commandPalette")
107+
.getElementValue(_) = commandPaletteObject and
108+
commandPaletteObject.getPropValue("command").getStringValue() = commandName and
109+
commandPaletteObject.getPropValue("when").getStringValue() = "false"
110+
)
111+
}
112+
113+
/**
114+
* Represents a command being usable from the command palette.
115+
* This means that a user could choose to manually invoke the command.
116+
*/
117+
class CommandUsagePackageJsonCommandPalette extends CommandUsage, JsonObject {
118+
CommandUsagePackageJsonCommandPalette() {
119+
this.getFile().getBaseName() = "package.json" and
120+
exists(this.getPropValue("command")) and
121+
exists(PackageJson packageJson |
122+
packageJson.getPropValue("contributes").getPropValue("commands").getElementValue(_) = this
123+
) and
124+
not isDisabledInCommandPalette(this.getPropValue("command").getStringValue())
125+
}
126+
127+
override string getCommandName() { result = this.getPropValue("command").getStringValue() }
128+
}
129+
130+
from CommandName c
131+
where c.getNumberOfUsages() > 1
132+
select c.getFirstUsage(),
133+
"The " + c + " command is used from " + c.getNumberOfUsages() + " locations"
134+

0 commit comments

Comments
 (0)