From c668b39b30cd8753bfb575e1a5fabb5ad7772f38 Mon Sep 17 00:00:00 2001 From: Anders Starcke Henriksen Date: Tue, 10 Oct 2023 13:53:13 +0200 Subject: [PATCH 1/6] Add language filter panel. --- extensions/ql-vscode/package.json | 18 ++++ extensions/ql-vscode/src/common/commands.ts | 8 ++ extensions/ql-vscode/src/extension.ts | 6 ++ .../ql-vscode/src/language-context-store.ts | 7 ++ .../language-selection-data-provider.ts | 93 +++++++++++++++++++ .../language-selection-panel.ts | 40 ++++++++ .../test/unit-tests/command-lint.test.ts | 1 + 7 files changed, 173 insertions(+) create mode 100644 extensions/ql-vscode/src/language-selection-panel/language-selection-data-provider.ts create mode 100644 extensions/ql-vscode/src/language-selection-panel/language-selection-panel.ts diff --git a/extensions/ql-vscode/package.json b/extensions/ql-vscode/package.json index 0e7ea511a..4227ae426 100644 --- a/extensions/ql-vscode/package.json +++ b/extensions/ql-vscode/package.json @@ -581,6 +581,10 @@ "command": "codeQL.copyVersion", "title": "CodeQL: Copy Version Information" }, + { + "command": "codeQLLanguageSelection.setSelectedItem", + "title": "Select" + }, { "command": "codeQLQueries.runLocalQueryFromQueriesPanel", "title": "Run local query", @@ -1163,6 +1167,11 @@ "when": "view == codeQLVariantAnalysisRepositories && viewItem =~ /canImportCodeSearch/", "group": "2_qlContextMenu@1" }, + { + "command": "codeQLLanguageSelection.setSelectedItem", + "when": "view == codeQLLanguageSelection && viewItem =~ /canBeSelected/", + "group": "inline" + }, { "command": "codeQLDatabases.setCurrentDatabase", "group": "inline", @@ -1511,6 +1520,10 @@ { "command": "codeQL.openModelEditor" }, + { + "command": "codeQLLanguageSelection.setSelectedItem", + "when": "false" + }, { "command": "codeQLQueries.runLocalQueryContextMenu", "when": "false" @@ -1977,6 +1990,11 @@ }, "views": { "ql-container": [ + { + "id": "codeQLLanguageSelection", + "name": "Language", + "when": "config.codeQL.canary && config.codeQL.showLanguageFilter" + }, { "id": "codeQLDatabases", "name": "Databases" diff --git a/extensions/ql-vscode/src/common/commands.ts b/extensions/ql-vscode/src/common/commands.ts index 917ae1aef..4fea925e7 100644 --- a/extensions/ql-vscode/src/common/commands.ts +++ b/extensions/ql-vscode/src/common/commands.ts @@ -12,6 +12,7 @@ import type { } from "../variant-analysis/shared/variant-analysis"; import type { QLDebugConfiguration } from "../debugger/debug-configuration"; import type { QueryTreeViewItem } from "../queries-panel/query-tree-view-item"; +import type { LanguageSelectionTreeViewItem } from "../language-selection-panel/language-selection-data-provider"; // A command function matching the signature that VS Code calls when // a command is invoked from a context menu on a TreeView with @@ -198,6 +199,13 @@ export type QueryHistoryCommands = { "codeQL.exportSelectedVariantAnalysisResults": () => Promise; }; +// Commands user for the language selector panel +export type LanguageSelectionCommands = { + "codeQLLanguageSelection.setSelectedItem": ( + item: LanguageSelectionTreeViewItem, + ) => Promise; +}; + // Commands used for the local databases panel export type LocalDatabasesCommands = { // Command palette commands diff --git a/extensions/ql-vscode/src/extension.ts b/extensions/ql-vscode/src/extension.ts index 236d8426b..0ffacca69 100644 --- a/extensions/ql-vscode/src/extension.ts +++ b/extensions/ql-vscode/src/extension.ts @@ -136,6 +136,7 @@ import { NewQueryRunner, QueryRunner, QueryServerClient } from "./query-server"; import { QueriesModule } from "./queries-panel/queries-module"; import { OpenReferencedFileCodeLensProvider } from "./local-queries/open-referenced-file-code-lens-provider"; import { LanguageContextStore } from "./language-context-store"; +import { LanguageSelectionPanel } from "./language-selection-panel/language-selection-panel"; /** * extension.ts @@ -779,6 +780,10 @@ async function activateWithInstalledDistribution( void extLogger.log("Initializing language context."); const languageContext = new LanguageContextStore(app); + void extLogger.log("Initializing language selector."); + const languageSelectionPanel = new LanguageSelectionPanel(languageContext); + ctx.subscriptions.push(languageSelectionPanel); + void extLogger.log("Initializing database panel."); const databaseUI = new DatabaseUI( app, @@ -1016,6 +1021,7 @@ async function activateWithInstalledDistribution( ...getPackagingCommands({ cliServer, }), + ...languageSelectionPanel.getCommands(), ...modelEditorModule.getCommands(), ...evalLogViewer.getCommands(), ...summaryLanguageSupport.getCommands(), diff --git a/extensions/ql-vscode/src/language-context-store.ts b/extensions/ql-vscode/src/language-context-store.ts index c1fb33a0b..2b69dee27 100644 --- a/extensions/ql-vscode/src/language-context-store.ts +++ b/extensions/ql-vscode/src/language-context-store.ts @@ -46,4 +46,11 @@ export class LanguageContextStore extends DisposableObject { public shouldInclude(language: QueryLanguage | undefined): boolean { return this.languageFilter === "All" || this.languageFilter === language; } + + public selectedLanguage(language: QueryLanguage | undefined): boolean { + return ( + (this.languageFilter === "All" && language === undefined) || + this.languageFilter === language + ); + } } diff --git a/extensions/ql-vscode/src/language-selection-panel/language-selection-data-provider.ts b/extensions/ql-vscode/src/language-selection-panel/language-selection-data-provider.ts new file mode 100644 index 000000000..648e249d4 --- /dev/null +++ b/extensions/ql-vscode/src/language-selection-panel/language-selection-data-provider.ts @@ -0,0 +1,93 @@ +import { DisposableObject } from "../common/disposable-object"; +import { LanguageContextStore } from "../language-context-store"; +import { + Event, + EventEmitter, + ThemeIcon, + TreeDataProvider, + TreeItem, +} from "vscode"; +import { + QueryLanguage, + getLanguageDisplayName, +} from "../common/query-language"; + +const ALL_LANGUAGE_SELECTION_OPTIONS = [ + undefined, // All langauges + QueryLanguage.Cpp, + QueryLanguage.CSharp, + QueryLanguage.Go, + QueryLanguage.Java, + QueryLanguage.Javascript, + QueryLanguage.Python, + QueryLanguage.Ruby, + QueryLanguage.Swift, +]; + +// A tree view items consisting of of a language (or undefined for all languages +// and a boolean indicating whether it is selected or not. +export class LanguageSelectionTreeViewItem extends TreeItem { + constructor( + public readonly language: QueryLanguage | undefined, + public readonly selected: boolean = false, + ) { + const label = language ? getLanguageDisplayName(language) : "All languages"; + super(label); + + this.iconPath = selected ? new ThemeIcon("check") : undefined; + this.contextValue = selected ? undefined : "canBeSelected"; + } +} + +export class LanguageSelectionTreeDataProvider + extends DisposableObject + implements TreeDataProvider +{ + private treeItems: LanguageSelectionTreeViewItem[]; + private readonly onDidChangeTreeDataEmitter = this.push( + new EventEmitter(), + ); + + public constructor(private readonly languageContext: LanguageContextStore) { + super(); + + this.treeItems = this.createTree(languageContext); + + // If the language context changes, we need to update the tree. + this.push( + this.languageContext.onLanguageContextChanged(() => { + this.treeItems = this.createTree(languageContext); + this.onDidChangeTreeDataEmitter.fire(); + }), + ); + } + + public get onDidChangeTreeData(): Event { + return this.onDidChangeTreeDataEmitter.event; + } + + public getTreeItem(item: LanguageSelectionTreeViewItem): TreeItem { + return item; + } + + public getChildren( + item?: LanguageSelectionTreeViewItem, + ): LanguageSelectionTreeViewItem[] { + if (!item) { + return this.treeItems; + } else { + return []; + } + } + + private createTree( + languageContext: LanguageContextStore, + ): LanguageSelectionTreeViewItem[] { + return ALL_LANGUAGE_SELECTION_OPTIONS.map((language) => { + return new LanguageSelectionTreeViewItem( + language, + languageContext.selectedLanguage(language), + ); + }); + } +} diff --git a/extensions/ql-vscode/src/language-selection-panel/language-selection-panel.ts b/extensions/ql-vscode/src/language-selection-panel/language-selection-panel.ts new file mode 100644 index 000000000..bf2f2eb7d --- /dev/null +++ b/extensions/ql-vscode/src/language-selection-panel/language-selection-panel.ts @@ -0,0 +1,40 @@ +import { DisposableObject } from "../common/disposable-object"; +import { window } from "vscode"; +import { + LanguageSelectionTreeDataProvider, + LanguageSelectionTreeViewItem, +} from "./language-selection-data-provider"; +import { LanguageContextStore } from "../language-context-store"; +import { LanguageSelectionCommands } from "../common/commands"; + +// This panel allows the selection of a single language, that will +// then filter all other relevant views (e.g. db panel, query history). +export class LanguageSelectionPanel extends DisposableObject { + constructor(private readonly languageContext: LanguageContextStore) { + super(); + + const dataProvider = new LanguageSelectionTreeDataProvider(languageContext); + + const treeView = window.createTreeView("codeQLLanguageSelection", { + treeDataProvider: dataProvider, + }); + this.push(treeView); + } + + public getCommands(): LanguageSelectionCommands { + return { + "codeQLLanguageSelection.setSelectedItem": + this.handleSetSelectedLanguage.bind(this), + }; + } + + private async handleSetSelectedLanguage( + item: LanguageSelectionTreeViewItem, + ): Promise { + if (item.language) { + await this.languageContext.setLanguageContext(item.language); + } else { + await this.languageContext.clearLanguageContext(); + } + } +} diff --git a/extensions/ql-vscode/test/unit-tests/command-lint.test.ts b/extensions/ql-vscode/test/unit-tests/command-lint.test.ts index 2b289ecdc..5c0936a8e 100644 --- a/extensions/ql-vscode/test/unit-tests/command-lint.test.ts +++ b/extensions/ql-vscode/test/unit-tests/command-lint.test.ts @@ -38,6 +38,7 @@ describe("commands declared in package.json", () => { expect(title).toBeDefined(); commandTitles[command] = title!; } else if ( + command.match(/^codeQLLanguageSelection\./) || command.match(/^codeQLDatabases\./) || command.match(/^codeQLQueries\./) || command.match(/^codeQLVariantAnalysisRepositories\./) || From d2b17e1676b97840ce98c394556bb7dddfc4a946 Mon Sep 17 00:00:00 2001 From: Anders Starcke Henriksen Date: Wed, 11 Oct 2023 11:45:01 +0200 Subject: [PATCH 2/6] Add tests. --- .../language-selection-data-provider.test.ts | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 extensions/ql-vscode/test/vscode-tests/no-workspace/language-selection-panel/language-selection-data-provider.test.ts diff --git a/extensions/ql-vscode/test/vscode-tests/no-workspace/language-selection-panel/language-selection-data-provider.test.ts b/extensions/ql-vscode/test/vscode-tests/no-workspace/language-selection-panel/language-selection-data-provider.test.ts new file mode 100644 index 000000000..c467896fa --- /dev/null +++ b/extensions/ql-vscode/test/vscode-tests/no-workspace/language-selection-panel/language-selection-data-provider.test.ts @@ -0,0 +1,66 @@ +import { + QueryLanguage, + getLanguageDisplayName, +} from "../../../../src/common/query-language"; +import { LanguageContextStore } from "../../../../src/language-context-store"; +import { + LanguageSelectionTreeDataProvider, + LanguageSelectionTreeViewItem, +} from "../../../../src/language-selection-panel/language-selection-data-provider"; +import { createMockApp } from "../../../__mocks__/appMock"; +import { EventEmitter, ThemeIcon } from "vscode"; + +describe("LanguageSelectionTreeDataProvider", () => { + function expectSelected( + items: LanguageSelectionTreeViewItem[], + expected: QueryLanguage | undefined, + ) { + items.forEach((item) => { + if (item.language === expected) { + expect(item.selected).toBe(true); + expect(item.iconPath).toEqual(new ThemeIcon("check")); + } else { + expect(item.selected).toBe(false); + expect(item.iconPath).toBe(undefined); + } + }); + } + + describe("getChildren", () => { + const app = createMockApp({ + createEventEmitter: () => new EventEmitter(), + }); + const languageContext = new LanguageContextStore(app); + const dataProvider = new LanguageSelectionTreeDataProvider(languageContext); + + it("returns list of all languages", async () => { + const expectedLanguageNames = [ + "All languages", + ...Object.values(QueryLanguage).map((language) => { + return getLanguageDisplayName(language); + }), + ]; + // Note that the internal order of C# and C / C++ is different from what is shown in the UI. + [expectedLanguageNames[1], expectedLanguageNames[2]] = [ + expectedLanguageNames[2], + expectedLanguageNames[1], + ]; + const actualLanguagesNames = dataProvider.getChildren().map((item) => { + return item.label; + }); + + expect(actualLanguagesNames).toEqual(expectedLanguageNames); + }); + + it("default selection is All languages", async () => { + const items = dataProvider.getChildren(); + expectSelected(items, undefined); + }); + + it("When language is changed then the selected element change", async () => { + await languageContext.setLanguageContext(QueryLanguage.CSharp); + const items = dataProvider.getChildren(); + expectSelected(items, QueryLanguage.CSharp); + }); + }); +}); From cc3feabe662a2dacd9612b2fdbcba20cf6a40b43 Mon Sep 17 00:00:00 2001 From: Anders Starcke Henriksen Date: Fri, 13 Oct 2023 09:48:28 +0200 Subject: [PATCH 3/6] Apply suggestions from code review Co-authored-by: Koen Vlaswinkel --- .../language-selection-data-provider.ts | 4 ++-- .../language-selection-data-provider.test.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/extensions/ql-vscode/src/language-selection-panel/language-selection-data-provider.ts b/extensions/ql-vscode/src/language-selection-panel/language-selection-data-provider.ts index 648e249d4..f3e9022a0 100644 --- a/extensions/ql-vscode/src/language-selection-panel/language-selection-data-provider.ts +++ b/extensions/ql-vscode/src/language-selection-panel/language-selection-data-provider.ts @@ -13,7 +13,7 @@ import { } from "../common/query-language"; const ALL_LANGUAGE_SELECTION_OPTIONS = [ - undefined, // All langauges + undefined, // All languages QueryLanguage.Cpp, QueryLanguage.CSharp, QueryLanguage.Go, @@ -24,7 +24,7 @@ const ALL_LANGUAGE_SELECTION_OPTIONS = [ QueryLanguage.Swift, ]; -// A tree view items consisting of of a language (or undefined for all languages +// A tree view items consisting of of a language (or undefined for all languages) // and a boolean indicating whether it is selected or not. export class LanguageSelectionTreeViewItem extends TreeItem { constructor( diff --git a/extensions/ql-vscode/test/vscode-tests/no-workspace/language-selection-panel/language-selection-data-provider.test.ts b/extensions/ql-vscode/test/vscode-tests/no-workspace/language-selection-panel/language-selection-data-provider.test.ts index c467896fa..ccf2ed6ce 100644 --- a/extensions/ql-vscode/test/vscode-tests/no-workspace/language-selection-panel/language-selection-data-provider.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/no-workspace/language-selection-panel/language-selection-data-provider.test.ts @@ -57,7 +57,7 @@ describe("LanguageSelectionTreeDataProvider", () => { expectSelected(items, undefined); }); - it("When language is changed then the selected element change", async () => { + it("When language is changed then the selected element changes", async () => { await languageContext.setLanguageContext(QueryLanguage.CSharp); const items = dataProvider.getChildren(); expectSelected(items, QueryLanguage.CSharp); From a03e2c85f1044798c42a7880c17fba3844919b1f Mon Sep 17 00:00:00 2001 From: Anders Starcke Henriksen Date: Fri, 13 Oct 2023 10:02:14 +0200 Subject: [PATCH 4/6] Address comments. --- extensions/ql-vscode/src/common/commands.ts | 1 + extensions/ql-vscode/src/language-context-store.ts | 6 +++++- .../language-selection-data-provider.ts | 10 ++++------ .../language-selection-panel.ts | 1 + .../language-selection-data-provider.test.ts | 9 +++------ 5 files changed, 14 insertions(+), 13 deletions(-) diff --git a/extensions/ql-vscode/src/common/commands.ts b/extensions/ql-vscode/src/common/commands.ts index 4fea925e7..439ad2d20 100644 --- a/extensions/ql-vscode/src/common/commands.ts +++ b/extensions/ql-vscode/src/common/commands.ts @@ -367,6 +367,7 @@ export type AllExtensionCommands = BaseCommands & QueryEditorCommands & ResultsViewCommands & QueryHistoryCommands & + LanguageSelectionCommands & LocalDatabasesCommands & DebuggerCommands & VariantAnalysisCommands & diff --git a/extensions/ql-vscode/src/language-context-store.ts b/extensions/ql-vscode/src/language-context-store.ts index 2b69dee27..2947ff042 100644 --- a/extensions/ql-vscode/src/language-context-store.ts +++ b/extensions/ql-vscode/src/language-context-store.ts @@ -43,11 +43,15 @@ export class LanguageContextStore extends DisposableObject { ); } + // shouldInclude should return true if the given language should be included. + // That means that either the given language is selected or the "All" option is selected. public shouldInclude(language: QueryLanguage | undefined): boolean { return this.languageFilter === "All" || this.languageFilter === language; } - public selectedLanguage(language: QueryLanguage | undefined): boolean { + // isSelectedLanguage returns true if the given language is selected. If no language + // is given then it returns true if the "All" option is selected. + public isSelectedLanguage(language: QueryLanguage | undefined): boolean { return ( (this.languageFilter === "All" && language === undefined) || this.languageFilter === language diff --git a/extensions/ql-vscode/src/language-selection-panel/language-selection-data-provider.ts b/extensions/ql-vscode/src/language-selection-panel/language-selection-data-provider.ts index f3e9022a0..c77e7e020 100644 --- a/extensions/ql-vscode/src/language-selection-panel/language-selection-data-provider.ts +++ b/extensions/ql-vscode/src/language-selection-panel/language-selection-data-provider.ts @@ -51,12 +51,12 @@ export class LanguageSelectionTreeDataProvider public constructor(private readonly languageContext: LanguageContextStore) { super(); - this.treeItems = this.createTree(languageContext); + this.treeItems = this.createTree(); // If the language context changes, we need to update the tree. this.push( this.languageContext.onLanguageContextChanged(() => { - this.treeItems = this.createTree(languageContext); + this.treeItems = this.createTree(); this.onDidChangeTreeDataEmitter.fire(); }), ); @@ -80,13 +80,11 @@ export class LanguageSelectionTreeDataProvider } } - private createTree( - languageContext: LanguageContextStore, - ): LanguageSelectionTreeViewItem[] { + private createTree(): LanguageSelectionTreeViewItem[] { return ALL_LANGUAGE_SELECTION_OPTIONS.map((language) => { return new LanguageSelectionTreeViewItem( language, - languageContext.selectedLanguage(language), + this.languageContext.isSelectedLanguage(language), ); }); } diff --git a/extensions/ql-vscode/src/language-selection-panel/language-selection-panel.ts b/extensions/ql-vscode/src/language-selection-panel/language-selection-panel.ts index bf2f2eb7d..23e7d2cae 100644 --- a/extensions/ql-vscode/src/language-selection-panel/language-selection-panel.ts +++ b/extensions/ql-vscode/src/language-selection-panel/language-selection-panel.ts @@ -14,6 +14,7 @@ export class LanguageSelectionPanel extends DisposableObject { super(); const dataProvider = new LanguageSelectionTreeDataProvider(languageContext); + this.push(dataProvider); const treeView = window.createTreeView("codeQLLanguageSelection", { treeDataProvider: dataProvider, diff --git a/extensions/ql-vscode/test/vscode-tests/no-workspace/language-selection-panel/language-selection-data-provider.test.ts b/extensions/ql-vscode/test/vscode-tests/no-workspace/language-selection-panel/language-selection-data-provider.test.ts index ccf2ed6ce..dc207e319 100644 --- a/extensions/ql-vscode/test/vscode-tests/no-workspace/language-selection-panel/language-selection-data-provider.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/no-workspace/language-selection-panel/language-selection-data-provider.test.ts @@ -40,16 +40,13 @@ describe("LanguageSelectionTreeDataProvider", () => { return getLanguageDisplayName(language); }), ]; - // Note that the internal order of C# and C / C++ is different from what is shown in the UI. - [expectedLanguageNames[1], expectedLanguageNames[2]] = [ - expectedLanguageNames[2], - expectedLanguageNames[1], - ]; const actualLanguagesNames = dataProvider.getChildren().map((item) => { return item.label; }); - expect(actualLanguagesNames).toEqual(expectedLanguageNames); + // Note that the internal order of C# and C / C++ is different from what is shown in the UI. + // So we sort to make sure we can compare the two lists. + expect(actualLanguagesNames.sort()).toEqual(expectedLanguageNames.sort()); }); it("default selection is All languages", async () => { From d5388576b5474a500f622d26365cb179261b13e2 Mon Sep 17 00:00:00 2001 From: Anders Starcke Henriksen Date: Fri, 13 Oct 2023 16:00:17 +0200 Subject: [PATCH 5/6] Apply suggestions from code review Co-authored-by: Koen Vlaswinkel --- .../language-selection-data-provider.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/ql-vscode/test/vscode-tests/no-workspace/language-selection-panel/language-selection-data-provider.test.ts b/extensions/ql-vscode/test/vscode-tests/no-workspace/language-selection-panel/language-selection-data-provider.test.ts index dc207e319..950fd8ae7 100644 --- a/extensions/ql-vscode/test/vscode-tests/no-workspace/language-selection-panel/language-selection-data-provider.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/no-workspace/language-selection-panel/language-selection-data-provider.test.ts @@ -49,12 +49,12 @@ describe("LanguageSelectionTreeDataProvider", () => { expect(actualLanguagesNames.sort()).toEqual(expectedLanguageNames.sort()); }); - it("default selection is All languages", async () => { + it("has a default selection of All languages", async () => { const items = dataProvider.getChildren(); expectSelected(items, undefined); }); - it("When language is changed then the selected element changes", async () => { + it("changes the selected element when the language is changed", async () => { await languageContext.setLanguageContext(QueryLanguage.CSharp); const items = dataProvider.getChildren(); expectSelected(items, QueryLanguage.CSharp); From e8e6c6bbc7953783d8a6820774b33f83eb2d523d Mon Sep 17 00:00:00 2001 From: Anders Starcke Henriksen Date: Fri, 13 Oct 2023 16:28:14 +0200 Subject: [PATCH 6/6] Update comment. --- .../ql-vscode/src/language-context-store.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/extensions/ql-vscode/src/language-context-store.ts b/extensions/ql-vscode/src/language-context-store.ts index 2947ff042..e3a819511 100644 --- a/extensions/ql-vscode/src/language-context-store.ts +++ b/extensions/ql-vscode/src/language-context-store.ts @@ -43,14 +43,24 @@ export class LanguageContextStore extends DisposableObject { ); } - // shouldInclude should return true if the given language should be included. - // That means that either the given language is selected or the "All" option is selected. + /** + * This returns true if the given language should be included. + * + * That means that either the given language is selected or the "All" option is selected. + * + * @param language a query language or undefined if the language is unknown. + */ public shouldInclude(language: QueryLanguage | undefined): boolean { return this.languageFilter === "All" || this.languageFilter === language; } - // isSelectedLanguage returns true if the given language is selected. If no language - // is given then it returns true if the "All" option is selected. + /** + * This returns true if the given language is selected. + * + * If no language is given then it returns true if the "All" option is selected. + * + * @param language a query language or undefined. + */ public isSelectedLanguage(language: QueryLanguage | undefined): boolean { return ( (this.languageFilter === "All" && language === undefined) ||