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