diff --git a/extensions/ql-vscode/src/common/mutable.ts b/extensions/ql-vscode/src/common/mutable.ts new file mode 100644 index 000000000..d52d7d9c7 --- /dev/null +++ b/extensions/ql-vscode/src/common/mutable.ts @@ -0,0 +1,6 @@ +/** + * Remove all readonly modifiers from a type. + */ +export type Mutable = { + -readonly [P in keyof T]: T[P]; +}; diff --git a/extensions/ql-vscode/src/model-editor/auto-modeler.ts b/extensions/ql-vscode/src/model-editor/auto-modeler.ts index 71a9e0f55..86e2aa55f 100644 --- a/extensions/ql-vscode/src/model-editor/auto-modeler.ts +++ b/extensions/ql-vscode/src/model-editor/auto-modeler.ts @@ -218,8 +218,6 @@ export class AutoModeler { { type: "neutral", kind: "sink", - input: "", - output: "", provenance: "ai-generated", signature: candidate.signature, packageName: candidate.packageName, diff --git a/extensions/ql-vscode/src/model-editor/flow-model-queries.ts b/extensions/ql-vscode/src/model-editor/flow-model-queries.ts index e65dac895..639ee572a 100644 --- a/extensions/ql-vscode/src/model-editor/flow-model-queries.ts +++ b/extensions/ql-vscode/src/model-editor/flow-model-queries.ts @@ -7,10 +7,13 @@ import { NotificationLogger, showAndLogExceptionWithTelemetry, } from "../common/logging"; -import { getModelsAsDataLanguage } from "./languages"; +import { + getModelsAsDataLanguageModel, + ModelsAsDataLanguagePredicates, +} from "./languages"; import { ProgressCallback } from "../common/vscode/progress"; import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders"; -import { ModeledMethod, ModeledMethodType } from "./modeled-method"; +import { ModeledMethod } from "./modeled-method"; import { redactableError } from "../common/errors"; import { telemetryListener } from "../common/vscode/telemetry"; import { runQuery } from "../local-queries/run-query"; @@ -112,7 +115,7 @@ async function resolveFlowQueries( } async function runSingleFlowQuery( - type: Exclude, + type: keyof ModelsAsDataLanguagePredicates, queryPath: string | undefined, queryStep: number, { @@ -158,9 +161,7 @@ async function runSingleFlowQuery( } // Interpret the results - const modelsAsDataLanguage = getModelsAsDataLanguage(language); - - const definition = modelsAsDataLanguage.predicates[type]; + const definition = getModelsAsDataLanguageModel(language, type); const bqrsPath = completedQuery.outputDir.bqrsPath; diff --git a/extensions/ql-vscode/src/model-editor/languages/languages.ts b/extensions/ql-vscode/src/model-editor/languages/languages.ts index 72e2fffaf..e297bc666 100644 --- a/extensions/ql-vscode/src/model-editor/languages/languages.ts +++ b/extensions/ql-vscode/src/model-editor/languages/languages.ts @@ -1,5 +1,8 @@ import { QueryLanguage } from "../../common/query-language"; -import { ModelsAsDataLanguage } from "./models-as-data"; +import { + ModelsAsDataLanguage, + ModelsAsDataLanguagePredicates, +} from "./models-as-data"; import { ruby } from "./ruby"; import { staticLanguage } from "./static"; @@ -18,3 +21,16 @@ export function getModelsAsDataLanguage( } return definition; } + +export function getModelsAsDataLanguageModel< + T extends keyof ModelsAsDataLanguagePredicates, +>( + language: QueryLanguage, + model: T, +): NonNullable { + const definition = getModelsAsDataLanguage(language).predicates[model]; + if (!definition) { + throw new Error(`No models-as-data predicate for ${model}`); + } + return definition; +} diff --git a/extensions/ql-vscode/src/model-editor/languages/models-as-data.ts b/extensions/ql-vscode/src/model-editor/languages/models-as-data.ts index 14ef489c8..383473102 100644 --- a/extensions/ql-vscode/src/model-editor/languages/models-as-data.ts +++ b/extensions/ql-vscode/src/model-editor/languages/models-as-data.ts @@ -1,24 +1,30 @@ import { MethodDefinition } from "../method"; -import { ModeledMethod, ModeledMethodType } from "../modeled-method"; +import { + ModeledMethod, + NeutralModeledMethod, + SinkModeledMethod, + SourceModeledMethod, + SummaryModeledMethod, +} from "../modeled-method"; import { DataTuple } from "../model-extension-file"; import { Mode } from "../shared/mode"; -type GenerateMethodDefinition = (method: ModeledMethod) => DataTuple[]; +type GenerateMethodDefinition = (method: T) => DataTuple[]; type ReadModeledMethod = (row: DataTuple[]) => ModeledMethod; -export type ModelsAsDataLanguageModelType = Exclude; - -export type ModelsAsDataLanguagePredicate = { +export type ModelsAsDataLanguagePredicate = { extensiblePredicate: string; supportedKinds: string[]; - generateMethodDefinition: GenerateMethodDefinition; + generateMethodDefinition: GenerateMethodDefinition; readModeledMethod: ReadModeledMethod; }; -export type ModelsAsDataLanguagePredicates = Record< - ModelsAsDataLanguageModelType, - ModelsAsDataLanguagePredicate ->; +export type ModelsAsDataLanguagePredicates = { + source?: ModelsAsDataLanguagePredicate; + sink?: ModelsAsDataLanguagePredicate; + summary?: ModelsAsDataLanguagePredicate; + neutral?: ModelsAsDataLanguagePredicate; +}; export type ModelsAsDataLanguage = { /** diff --git a/extensions/ql-vscode/src/model-editor/languages/static.ts b/extensions/ql-vscode/src/model-editor/languages/static.ts index 9d9141675..9866c54db 100644 --- a/extensions/ql-vscode/src/model-editor/languages/static.ts +++ b/extensions/ql-vscode/src/model-editor/languages/static.ts @@ -1,5 +1,5 @@ import { ModelsAsDataLanguage } from "./models-as-data"; -import { ModeledMethodType, Provenance } from "../modeled-method"; +import { Provenance } from "../modeled-method"; import { DataTuple } from "../model-extension-file"; import { sharedExtensiblePredicates, sharedKinds } from "./shared"; @@ -34,7 +34,7 @@ export const staticLanguage: ModelsAsDataLanguage = { method.provenance, ], readModeledMethod: (row) => ({ - type: "source" as ModeledMethodType, + type: "source", input: "", output: row[6] as string, kind: row[7] as string, diff --git a/extensions/ql-vscode/src/model-editor/modeled-method-empty.ts b/extensions/ql-vscode/src/model-editor/modeled-method-empty.ts new file mode 100644 index 000000000..316682f9b --- /dev/null +++ b/extensions/ql-vscode/src/model-editor/modeled-method-empty.ts @@ -0,0 +1,88 @@ +import { ModeledMethod, SinkModeledMethod } from "./modeled-method"; +import { MethodSignature } from "./method"; +import { assertNever } from "../common/helpers-pure"; + +export function createEmptyModeledMethod( + type: ModeledMethod["type"], + methodSignature: MethodSignature, +) { + const canonicalMethodSignature: MethodSignature = { + packageName: methodSignature.packageName, + typeName: methodSignature.typeName, + methodName: methodSignature.methodName, + methodParameters: methodSignature.methodParameters, + signature: methodSignature.signature, + }; + + switch (type) { + case "none": + return createEmptyNoneModeledMethod(canonicalMethodSignature); + case "source": + return createEmptySourceModeledMethod(canonicalMethodSignature); + case "sink": + return createEmptySinkModeledMethod(canonicalMethodSignature); + case "summary": + return createEmptySummaryModeledMethod(canonicalMethodSignature); + case "neutral": + return createEmptyNeutralModeledMethod(canonicalMethodSignature); + default: + assertNever(type); + } +} + +function createEmptyNoneModeledMethod( + methodSignature: MethodSignature, +): ModeledMethod { + return { + ...methodSignature, + type: "none", + }; +} + +function createEmptySourceModeledMethod( + methodSignature: MethodSignature, +): ModeledMethod { + return { + ...methodSignature, + type: "source", + output: "", + kind: "", + provenance: "manual", + }; +} + +function createEmptySinkModeledMethod( + methodSignature: MethodSignature, +): SinkModeledMethod { + return { + ...methodSignature, + type: "sink", + input: "", + kind: "", + provenance: "manual", + }; +} + +function createEmptySummaryModeledMethod( + methodSignature: MethodSignature, +): ModeledMethod { + return { + ...methodSignature, + type: "summary", + input: "", + output: "", + kind: "", + provenance: "manual", + }; +} + +function createEmptyNeutralModeledMethod( + methodSignature: MethodSignature, +): ModeledMethod { + return { + ...methodSignature, + type: "neutral", + kind: "", + provenance: "manual", + }; +} diff --git a/extensions/ql-vscode/src/model-editor/modeled-method.ts b/extensions/ql-vscode/src/model-editor/modeled-method.ts index 756cfca7a..b54ba2e61 100644 --- a/extensions/ql-vscode/src/model-editor/modeled-method.ts +++ b/extensions/ql-vscode/src/model-editor/modeled-method.ts @@ -19,12 +19,85 @@ export type Provenance = // Entered by the user in the editor manually | "manual"; -export interface ModeledMethod extends MethodSignature { - readonly type: ModeledMethodType; +export interface NoneModeledMethod extends MethodSignature { + readonly type: "none"; +} + +export interface SourceModeledMethod extends MethodSignature { + readonly type: "source"; + readonly output: string; + readonly kind: ModeledMethodKind; + readonly provenance: Provenance; +} + +export interface SinkModeledMethod extends MethodSignature { + readonly type: "sink"; + readonly input: string; + readonly kind: ModeledMethodKind; + readonly provenance: Provenance; +} + +export interface SummaryModeledMethod extends MethodSignature { + readonly type: "summary"; readonly input: string; readonly output: string; readonly kind: ModeledMethodKind; readonly provenance: Provenance; } +export interface NeutralModeledMethod extends MethodSignature { + readonly type: "neutral"; + readonly kind: ModeledMethodKind; + readonly provenance: Provenance; +} + +export type ModeledMethod = + | NoneModeledMethod + | SourceModeledMethod + | SinkModeledMethod + | SummaryModeledMethod + | NeutralModeledMethod; + export type ModeledMethodKind = string; + +export function modeledMethodSupportsKind( + modeledMethod: ModeledMethod, +): modeledMethod is + | SourceModeledMethod + | SinkModeledMethod + | SummaryModeledMethod + | NeutralModeledMethod { + return ( + modeledMethod.type === "source" || + modeledMethod.type === "sink" || + modeledMethod.type === "summary" || + modeledMethod.type === "neutral" + ); +} + +export function modeledMethodSupportsInput( + modeledMethod: ModeledMethod, +): modeledMethod is SinkModeledMethod | SummaryModeledMethod { + return modeledMethod.type === "sink" || modeledMethod.type === "summary"; +} + +export function modeledMethodSupportsOutput( + modeledMethod: ModeledMethod, +): modeledMethod is SourceModeledMethod | SummaryModeledMethod { + return modeledMethod.type === "source" || modeledMethod.type === "summary"; +} + +export function modeledMethodSupportsProvenance( + modeledMethod: ModeledMethod, +): modeledMethod is + | SourceModeledMethod + | SinkModeledMethod + | SummaryModeledMethod + | NeutralModeledMethod { + return ( + modeledMethod.type === "source" || + modeledMethod.type === "sink" || + modeledMethod.type === "summary" || + modeledMethod.type === "neutral" + ); +} diff --git a/extensions/ql-vscode/src/model-editor/shared/validation.ts b/extensions/ql-vscode/src/model-editor/shared/validation.ts index 8a07f255e..5da83efd1 100644 --- a/extensions/ql-vscode/src/model-editor/shared/validation.ts +++ b/extensions/ql-vscode/src/model-editor/shared/validation.ts @@ -1,4 +1,4 @@ -import { ModeledMethod } from "../modeled-method"; +import { ModeledMethod, NeutralModeledMethod } from "../modeled-method"; import { MethodSignature } from "../method"; import { assertNever } from "../../common/helpers-pure"; @@ -37,16 +37,11 @@ function canonicalizeModeledMethod( return { ...methodSignature, type: "none", - input: "", - output: "", - kind: "", - provenance: "manual", }; case "source": return { ...methodSignature, type: "source", - input: "", output: modeledMethod.output, kind: modeledMethod.kind, provenance: "manual", @@ -56,7 +51,6 @@ function canonicalizeModeledMethod( ...methodSignature, type: "sink", input: modeledMethod.input, - output: "", kind: modeledMethod.kind, provenance: "manual", }; @@ -73,13 +67,11 @@ function canonicalizeModeledMethod( return { ...methodSignature, type: "neutral", - input: "", - output: "", kind: modeledMethod.kind, provenance: "manual", }; default: - assertNever(modeledMethod.type); + assertNever(modeledMethod); } } @@ -118,7 +110,8 @@ export function validateModeledMethods( } const neutralModeledMethods = consideredModeledMethods.filter( - (modeledMethod) => modeledMethod.type === "neutral", + (modeledMethod): modeledMethod is NeutralModeledMethod => + modeledMethod.type === "neutral", ); const neutralModeledMethodsByKind = new Map(); diff --git a/extensions/ql-vscode/src/model-editor/yaml.ts b/extensions/ql-vscode/src/model-editor/yaml.ts index 196e4efad..d9aad3109 100644 --- a/extensions/ql-vscode/src/model-editor/yaml.ts +++ b/extensions/ql-vscode/src/model-editor/yaml.ts @@ -1,10 +1,17 @@ import Ajv from "ajv"; import { Method } from "./method"; -import { ModeledMethod, ModeledMethodType } from "./modeled-method"; +import { + ModeledMethod, + NeutralModeledMethod, + SinkModeledMethod, + SourceModeledMethod, + SummaryModeledMethod, +} from "./modeled-method"; import { getModelsAsDataLanguage, ModelsAsDataLanguagePredicate, + ModelsAsDataLanguagePredicates, } from "./languages"; import * as modelExtensionFileSchema from "./model-extension-file.schema.json"; @@ -16,9 +23,9 @@ import { QueryLanguage } from "../common/query-language"; const ajv = new Ajv({ allErrors: true, allowUnionTypes: true }); const modelExtensionFileSchemaValidate = ajv.compile(modelExtensionFileSchema); -function createDataProperty( - methods: readonly ModeledMethod[], - definition: ModelsAsDataLanguagePredicate, +function createDataProperty( + methods: readonly T[], + definition: ModelsAsDataLanguagePredicate, ) { if (methods.length === 0) { return " []"; @@ -34,38 +41,92 @@ function createDataProperty( .join("\n")}`; } +function createExtensions( + language: QueryLanguage, + methods: readonly T[], + definition: ModelsAsDataLanguagePredicate | undefined, +) { + if (!definition) { + return ""; + } + + return ` - addsTo: + pack: codeql/${language}-all + extensible: ${definition.extensiblePredicate} + data:${createDataProperty(methods, definition)} +`; +} + export function createDataExtensionYaml( language: QueryLanguage, modeledMethods: readonly ModeledMethod[], ) { const modelsAsDataLanguage = getModelsAsDataLanguage(language); - const methodsByType: Record< - Exclude, - ModeledMethod[] - > = { - source: [], - sink: [], - summary: [], - neutral: [], - }; + const methodsByType = { + source: [] as SourceModeledMethod[], + sink: [] as SinkModeledMethod[], + summary: [] as SummaryModeledMethod[], + neutral: [] as NeutralModeledMethod[], + } satisfies Record; for (const modeledMethod of modeledMethods) { - if (modeledMethod?.type && modeledMethod.type !== "none") { - methodsByType[modeledMethod.type].push(modeledMethod); + if (!modeledMethod?.type || modeledMethod.type === "none") { + continue; + } + + switch (modeledMethod.type) { + case "source": + methodsByType.source.push(modeledMethod); + break; + case "sink": + methodsByType.sink.push(modeledMethod); + break; + case "summary": + methodsByType.summary.push(modeledMethod); + break; + case "neutral": + methodsByType.neutral.push(modeledMethod); + break; + default: + assertNever(modeledMethod); } } - const extensions = Object.entries(modelsAsDataLanguage.predicates).map( - ([type, definition]) => ` - addsTo: - pack: codeql/${language}-all - extensible: ${definition.extensiblePredicate} - data:${createDataProperty( - methodsByType[type as Exclude], - definition, - )} -`, - ); + const extensions = Object.keys(methodsByType) + .map((typeKey) => { + const type = typeKey as keyof ModelsAsDataLanguagePredicates; + + switch (type) { + case "source": + return createExtensions( + language, + methodsByType.source, + modelsAsDataLanguage.predicates.source, + ); + case "sink": + return createExtensions( + language, + methodsByType.sink, + modelsAsDataLanguage.predicates.sink, + ); + case "summary": + return createExtensions( + language, + methodsByType.summary, + modelsAsDataLanguage.predicates.summary, + ); + case "neutral": + return createExtensions( + language, + methodsByType.neutral, + modelsAsDataLanguage.predicates.neutral, + ); + default: + assertNever(type); + } + }) + .filter((extensions) => extensions !== ""); return `extensions: ${extensions.join("\n")}`; diff --git a/extensions/ql-vscode/src/stories/method-modeling/MethodModeling.stories.tsx b/extensions/ql-vscode/src/stories/method-modeling/MethodModeling.stories.tsx index 3bf9ad557..80de054e0 100644 --- a/extensions/ql-vscode/src/stories/method-modeling/MethodModeling.stories.tsx +++ b/extensions/ql-vscode/src/stories/method-modeling/MethodModeling.stories.tsx @@ -4,8 +4,13 @@ import { Meta, StoryFn } from "@storybook/react"; import { MethodModeling as MethodModelingComponent } from "../../view/method-modeling/MethodModeling"; import { createMethod } from "../../../test/factories/model-editor/method-factories"; -import { createModeledMethod } from "../../../test/factories/model-editor/modeled-method-factories"; +import { + createNeutralModeledMethod, + createSinkModeledMethod, + createSourceModeledMethod, +} from "../../../test/factories/model-editor/modeled-method-factories"; import { QueryLanguage } from "../../common/query-language"; + export default { title: "Method Modeling/Method Modeling", component: MethodModelingComponent, @@ -55,7 +60,7 @@ export const MultipleModelingsModeledSingle = Template.bind({}); MultipleModelingsModeledSingle.args = { language, method, - modeledMethods: [createModeledMethod(method)], + modeledMethods: [createSinkModeledMethod(method)], showMultipleModels: true, modelingStatus: "saved", }; @@ -65,11 +70,10 @@ MultipleModelingsModeledMultiple.args = { language, method, modeledMethods: [ - createModeledMethod(method), - createModeledMethod({ + createSinkModeledMethod(method), + createSourceModeledMethod({ ...method, type: "source", - input: "", output: "ReturnValue", kind: "remote", }), @@ -83,11 +87,8 @@ MultipleModelingsValidationFailedNeutral.args = { language, method, modeledMethods: [ - createModeledMethod(method), - createModeledMethod({ - ...method, - type: "neutral", - }), + createSinkModeledMethod(method), + createNeutralModeledMethod(method), ], showMultipleModels: true, modelingStatus: "unsaved", @@ -98,15 +99,13 @@ MultipleModelingsValidationFailedDuplicate.args = { language, method, modeledMethods: [ - createModeledMethod(method), - createModeledMethod({ + createSinkModeledMethod(method), + createSourceModeledMethod({ ...method, - type: "source", - input: "", output: "ReturnValue", kind: "remote", }), - createModeledMethod(method), + createSinkModeledMethod(method), ], showMultipleModels: true, modelingStatus: "unsaved", diff --git a/extensions/ql-vscode/src/stories/method-modeling/MethodModelingInputs.stories.tsx b/extensions/ql-vscode/src/stories/method-modeling/MethodModelingInputs.stories.tsx index 784c03a2b..d92cd9ca8 100644 --- a/extensions/ql-vscode/src/stories/method-modeling/MethodModelingInputs.stories.tsx +++ b/extensions/ql-vscode/src/stories/method-modeling/MethodModelingInputs.stories.tsx @@ -4,7 +4,7 @@ import { Meta, StoryFn } from "@storybook/react"; import { MethodModelingInputs as MethodModelingInputsComponent } from "../../view/method-modeling/MethodModelingInputs"; import { createMethod } from "../../../test/factories/model-editor/method-factories"; -import { createModeledMethod } from "../../../test/factories/model-editor/modeled-method-factories"; +import { createSinkModeledMethod } from "../../../test/factories/model-editor/modeled-method-factories"; import { useState } from "react"; import { ModeledMethod } from "../../model-editor/modeled-method"; import { QueryLanguage } from "../../common/query-language"; @@ -41,7 +41,7 @@ const Template: StoryFn = (args) => { }; const method = createMethod(); -const modeledMethod = createModeledMethod(); +const modeledMethod = createSinkModeledMethod(); export const UnmodeledMethod = Template.bind({}); UnmodeledMethod.args = { diff --git a/extensions/ql-vscode/src/stories/method-modeling/MultipleModeledMethodsPanel.stories.tsx b/extensions/ql-vscode/src/stories/method-modeling/MultipleModeledMethodsPanel.stories.tsx index 2aa9126d9..caf565bd3 100644 --- a/extensions/ql-vscode/src/stories/method-modeling/MultipleModeledMethodsPanel.stories.tsx +++ b/extensions/ql-vscode/src/stories/method-modeling/MultipleModeledMethodsPanel.stories.tsx @@ -5,7 +5,10 @@ import { Meta, StoryFn } from "@storybook/react"; import { MultipleModeledMethodsPanel as MultipleModeledMethodsPanelComponent } from "../../view/method-modeling/MultipleModeledMethodsPanel"; import { createMethod } from "../../../test/factories/model-editor/method-factories"; -import { createModeledMethod } from "../../../test/factories/model-editor/modeled-method-factories"; +import { + createSinkModeledMethod, + createSourceModeledMethod, +} from "../../../test/factories/model-editor/modeled-method-factories"; import { ModeledMethod } from "../../model-editor/modeled-method"; import { QueryLanguage } from "../../common/query-language"; @@ -56,7 +59,7 @@ export const Single = Template.bind({}); Single.args = { language, method, - modeledMethods: [createModeledMethod(method)], + modeledMethods: [createSinkModeledMethod(method)], }; export const Multiple = Template.bind({}); @@ -64,11 +67,9 @@ Multiple.args = { language, method, modeledMethods: [ - createModeledMethod(method), - createModeledMethod({ + createSinkModeledMethod(method), + createSourceModeledMethod({ ...method, - type: "source", - input: "", output: "ReturnValue", kind: "remote", }), diff --git a/extensions/ql-vscode/src/stories/model-editor/LibraryRow.stories.tsx b/extensions/ql-vscode/src/stories/model-editor/LibraryRow.stories.tsx index 2ae9487dc..6baf84da2 100644 --- a/extensions/ql-vscode/src/stories/model-editor/LibraryRow.stories.tsx +++ b/extensions/ql-vscode/src/stories/model-editor/LibraryRow.stories.tsx @@ -149,7 +149,6 @@ LibraryRow.args = { { type: "sink", input: "Argument[0]", - output: "", kind: "jndi-injection", provenance: "df-generated", signature: "org.sql2o.Sql2o#Sql2o(String)", @@ -190,9 +189,7 @@ LibraryRow.args = { "org.sql2o.Query#executeScalar(Class)": [ { type: "neutral", - input: "", - output: "", - kind: "", + kind: "summary", provenance: "df-generated", signature: "org.sql2o.Query#executeScalar(Class)", packageName: "org.sql2o", @@ -204,9 +201,7 @@ LibraryRow.args = { "org.sql2o.Sql2o#Sql2o(String,String,String)": [ { type: "neutral", - input: "", - output: "", - kind: "", + kind: "sink", provenance: "df-generated", signature: "org.sql2o.Sql2o#Sql2o(String,String,String)", packageName: "org.sql2o", diff --git a/extensions/ql-vscode/src/stories/model-editor/ModelEditor.stories.tsx b/extensions/ql-vscode/src/stories/model-editor/ModelEditor.stories.tsx index 6812a2392..b58679893 100644 --- a/extensions/ql-vscode/src/stories/model-editor/ModelEditor.stories.tsx +++ b/extensions/ql-vscode/src/stories/model-editor/ModelEditor.stories.tsx @@ -219,7 +219,6 @@ ModelEditor.args = { { type: "sink", input: "Argument[0]", - output: "", kind: "jndi-injection", provenance: "df-generated", signature: "org.sql2o.Sql2o#Sql2o(String)", @@ -260,9 +259,7 @@ ModelEditor.args = { "org.sql2o.Query#executeScalar(Class)": [ { type: "neutral", - input: "", - output: "", - kind: "", + kind: "sink", provenance: "df-generated", signature: "org.sql2o.Query#executeScalar(Class)", packageName: "org.sql2o", @@ -274,9 +271,7 @@ ModelEditor.args = { "org.sql2o.Sql2o#Sql2o(String,String,String)": [ { type: "neutral", - input: "", - output: "", - kind: "", + kind: "sink", provenance: "df-generated", signature: "org.sql2o.Sql2o#Sql2o(String,String,String)", packageName: "org.sql2o", diff --git a/extensions/ql-vscode/src/view/method-modeling/MultipleModeledMethodsPanel.tsx b/extensions/ql-vscode/src/view/method-modeling/MultipleModeledMethodsPanel.tsx index b6e92a541..20f441bc4 100644 --- a/extensions/ql-vscode/src/view/method-modeling/MultipleModeledMethodsPanel.tsx +++ b/extensions/ql-vscode/src/view/method-modeling/MultipleModeledMethodsPanel.tsx @@ -13,6 +13,7 @@ import { Codicon } from "../common"; import { validateModeledMethods } from "../../model-editor/shared/validation"; import { ModeledMethodAlert } from "./ModeledMethodAlert"; import { QueryLanguage } from "../../common/query-language"; +import { createEmptyModeledMethod } from "../../model-editor/modeled-method-empty"; import { sendTelemetry } from "../common/telemetry"; export type MultipleModeledMethodsPanelProps = { @@ -95,18 +96,10 @@ export const MultipleModeledMethodsPanel = ({ ); const handleAddClick = useCallback(() => { - const newModeledMethod: ModeledMethod = { - type: "none", - input: "", - output: "", - kind: "", - provenance: "manual", - signature: method.signature, - packageName: method.packageName, - typeName: method.typeName, - methodName: method.methodName, - methodParameters: method.methodParameters, - }; + const newModeledMethod: ModeledMethod = createEmptyModeledMethod( + "none", + method, + ); const newModeledMethods = [...modeledMethods, newModeledMethod]; diff --git a/extensions/ql-vscode/src/view/method-modeling/__tests__/MethodModeling.spec.tsx b/extensions/ql-vscode/src/view/method-modeling/__tests__/MethodModeling.spec.tsx index ebfe4da3c..7b7b3e6f2 100644 --- a/extensions/ql-vscode/src/view/method-modeling/__tests__/MethodModeling.spec.tsx +++ b/extensions/ql-vscode/src/view/method-modeling/__tests__/MethodModeling.spec.tsx @@ -2,7 +2,7 @@ import * as React from "react"; import { render as reactRender, screen } from "@testing-library/react"; import { MethodModeling, MethodModelingProps } from "../MethodModeling"; import { createMethod } from "../../../../test/factories/model-editor/method-factories"; -import { createModeledMethod } from "../../../../test/factories/model-editor/modeled-method-factories"; +import { createSinkModeledMethod } from "../../../../test/factories/model-editor/modeled-method-factories"; import { QueryLanguage } from "../../../common/query-language"; describe(MethodModeling.name, () => { @@ -11,7 +11,7 @@ describe(MethodModeling.name, () => { it("renders method modeling panel", () => { const method = createMethod(); - const modeledMethod = createModeledMethod(); + const modeledMethod = createSinkModeledMethod(); const isModelingInProgress = false; const onChange = jest.fn(); diff --git a/extensions/ql-vscode/src/view/method-modeling/__tests__/MethodModelingInputs.spec.tsx b/extensions/ql-vscode/src/view/method-modeling/__tests__/MethodModelingInputs.spec.tsx index 99dd9fc69..d362dcb0c 100644 --- a/extensions/ql-vscode/src/view/method-modeling/__tests__/MethodModelingInputs.spec.tsx +++ b/extensions/ql-vscode/src/view/method-modeling/__tests__/MethodModelingInputs.spec.tsx @@ -6,8 +6,12 @@ import { MethodModelingInputsProps, } from "../MethodModelingInputs"; import { createMethod } from "../../../../test/factories/model-editor/method-factories"; -import { createModeledMethod } from "../../../../test/factories/model-editor/modeled-method-factories"; +import { + createMethodSignature, + createSinkModeledMethod, +} from "../../../../test/factories/model-editor/modeled-method-factories"; import { QueryLanguage } from "../../../common/query-language"; +import { createEmptyModeledMethod } from "../../../model-editor/modeled-method-empty"; describe(MethodModelingInputs.name, () => { const render = (props: MethodModelingInputsProps) => @@ -15,7 +19,7 @@ describe(MethodModelingInputs.name, () => { const language = QueryLanguage.Java; const method = createMethod(); - const modeledMethod = createModeledMethod(); + const modeledMethod = createSinkModeledMethod(); const isModelingInProgress = false; const onChange = jest.fn(); @@ -76,9 +80,10 @@ describe(MethodModelingInputs.name, () => { onChange, }); - const updatedModeledMethod = createModeledMethod({ - type: "source", - }); + const updatedModeledMethod = createEmptyModeledMethod( + "source", + createMethodSignature(), + ); rerender( { const language = QueryLanguage.Java; const method = createMethod(); - const modeledMethods = [createModeledMethod(), createModeledMethod()]; + const modeledMethods = [createSinkModeledMethod(), createSinkModeledMethod()]; const isModelingInProgress = false; const onChange = jest.fn(); diff --git a/extensions/ql-vscode/src/view/method-modeling/__tests__/MultipleModeledMethodsPanel.spec.tsx b/extensions/ql-vscode/src/view/method-modeling/__tests__/MultipleModeledMethodsPanel.spec.tsx index 01d636434..e884251c6 100644 --- a/extensions/ql-vscode/src/view/method-modeling/__tests__/MultipleModeledMethodsPanel.spec.tsx +++ b/extensions/ql-vscode/src/view/method-modeling/__tests__/MultipleModeledMethodsPanel.spec.tsx @@ -1,7 +1,11 @@ import * as React from "react"; import { render as reactRender, screen, waitFor } from "@testing-library/react"; import { createMethod } from "../../../../test/factories/model-editor/method-factories"; -import { createModeledMethod } from "../../../../test/factories/model-editor/modeled-method-factories"; +import { + createNoneModeledMethod, + createSinkModeledMethod, + createSourceModeledMethod, +} from "../../../../test/factories/model-editor/modeled-method-factories"; import { MultipleModeledMethodsPanel, MultipleModeledMethodsPanelProps, @@ -82,11 +86,10 @@ describe(MultipleModeledMethodsPanel.name, () => { describe("with one modeled method", () => { const modeledMethods = [ - createModeledMethod({ + createSinkModeledMethod({ ...method, type: "sink", input: "Argument[this]", - output: "", kind: "path-injection", }), ]; @@ -164,10 +167,6 @@ describe(MultipleModeledMethodsPanel.name, () => { methodName: method.methodName, methodParameters: method.methodParameters, type: "none", - input: "", - output: "", - kind: "", - provenance: "manual", }, ]); }); @@ -201,19 +200,11 @@ describe(MultipleModeledMethodsPanel.name, () => { describe("with two modeled methods", () => { const modeledMethods = [ - createModeledMethod({ + createSinkModeledMethod({ ...method, - type: "sink", - input: "Argument[this]", - output: "", - kind: "path-injection", }), - createModeledMethod({ + createSourceModeledMethod({ ...method, - type: "source", - input: "", - output: "ReturnValue", - kind: "remote", }), ]; @@ -367,7 +358,6 @@ describe(MultipleModeledMethodsPanel.name, () => { methodName: method.methodName, methodParameters: method.methodParameters, type: "source", - input: "Argument[this]", output: "ReturnValue", kind: "value", provenance: "manual", @@ -403,7 +393,6 @@ describe(MultipleModeledMethodsPanel.name, () => { methodParameters: method.methodParameters, type: "sink", input: "Argument[this]", - output: "ReturnValue", kind: "value", provenance: "manual", }, @@ -447,10 +436,6 @@ describe(MultipleModeledMethodsPanel.name, () => { methodName: method.methodName, methodParameters: method.methodParameters, type: "none", - input: "", - output: "", - kind: "", - provenance: "manual", }, ]); }); @@ -553,24 +538,18 @@ describe(MultipleModeledMethodsPanel.name, () => { describe("with three modeled methods", () => { const modeledMethods = [ - createModeledMethod({ + createSinkModeledMethod({ ...method, - type: "sink", input: "Argument[this]", - output: "", kind: "path-injection", }), - createModeledMethod({ + createSourceModeledMethod({ ...method, - type: "source", - input: "", output: "ReturnValue", kind: "remote", }), - createModeledMethod({ + createSourceModeledMethod({ ...method, - type: "source", - input: "", output: "ReturnValue", kind: "local", }), @@ -719,19 +698,14 @@ describe(MultipleModeledMethodsPanel.name, () => { describe("with 1 modeled and 1 unmodeled method", () => { const modeledMethods = [ - createModeledMethod({ + createSinkModeledMethod({ ...method, type: "sink", input: "Argument[this]", - output: "", kind: "path-injection", }), - createModeledMethod({ + createNoneModeledMethod({ ...method, - type: "none", - input: "", - output: "", - kind: "", }), ]; @@ -823,10 +797,6 @@ describe(MultipleModeledMethodsPanel.name, () => { methodName: method.methodName, methodParameters: method.methodParameters, type: "none", - input: "", - output: "", - kind: "", - provenance: "manual", }, ]); }); @@ -834,10 +804,10 @@ describe(MultipleModeledMethodsPanel.name, () => { describe("with duplicate modeled methods", () => { const modeledMethods = [ - createModeledMethod({ + createSinkModeledMethod({ ...method, }), - createModeledMethod({ + createSinkModeledMethod({ ...method, }), ]; diff --git a/extensions/ql-vscode/src/view/model-editor/MethodRow.tsx b/extensions/ql-vscode/src/view/model-editor/MethodRow.tsx index fbc303f49..e60cef7e7 100644 --- a/extensions/ql-vscode/src/view/model-editor/MethodRow.tsx +++ b/extensions/ql-vscode/src/view/model-editor/MethodRow.tsx @@ -33,6 +33,7 @@ import { canAddNewModeledMethod } from "../../model-editor/shared/multiple-model import { DataGridCell, DataGridRow } from "../common/DataGrid"; import { validateModeledMethods } from "../../model-editor/shared/validation"; import { ModeledMethodAlert } from "../method-modeling/ModeledMethodAlert"; +import { createEmptyModeledMethod } from "../../model-editor/modeled-method-empty"; const ApiOrMethodRow = styled.div` min-height: calc(var(--input-height) * 1px); @@ -165,18 +166,10 @@ const ModelableMethodRow = forwardRef( ); const handleAddModelClick = useCallback(() => { - const newModeledMethod: ModeledMethod = { - type: "none", - input: "", - output: "", - kind: "", - provenance: "manual", - signature: method.signature, - packageName: method.packageName, - typeName: method.typeName, - methodName: method.methodName, - methodParameters: method.methodParameters, - }; + const newModeledMethod: ModeledMethod = createEmptyModeledMethod( + "none", + method, + ); const newModeledMethods = [...modeledMethods, newModeledMethod]; onChange(method.signature, newModeledMethods); }, [method, modeledMethods, onChange]); @@ -357,20 +350,7 @@ function modeledMethodsToDisplay( viewState: ModelEditorViewState, ): ModeledMethod[] { if (modeledMethods.length === 0) { - return [ - { - type: "none", - input: "", - output: "", - kind: "", - provenance: "manual", - signature: method.signature, - packageName: method.packageName, - typeName: method.typeName, - methodName: method.methodName, - methodParameters: method.methodParameters, - }, - ]; + return [createEmptyModeledMethod("none", method)]; } if (viewState.showMultipleModels) { diff --git a/extensions/ql-vscode/src/view/model-editor/ModelInputDropdown.tsx b/extensions/ql-vscode/src/view/model-editor/ModelInputDropdown.tsx index 959448e2b..9b0e60c26 100644 --- a/extensions/ql-vscode/src/view/model-editor/ModelInputDropdown.tsx +++ b/extensions/ql-vscode/src/view/model-editor/ModelInputDropdown.tsx @@ -1,7 +1,10 @@ import * as React from "react"; import { ChangeEvent, useCallback, useMemo } from "react"; import { Dropdown } from "../common/Dropdown"; -import { ModeledMethod } from "../../model-editor/modeled-method"; +import { + ModeledMethod, + modeledMethodSupportsInput, +} from "../../model-editor/modeled-method"; import { Method, getArgumentsList } from "../../model-editor/method"; type Props = { @@ -32,14 +35,13 @@ export const ModelInputDropdown = ({ ); const enabled = useMemo( - () => - modeledMethod?.type && ["sink", "summary"].includes(modeledMethod?.type), - [modeledMethod?.type], + () => modeledMethod && modeledMethodSupportsInput(modeledMethod), + [modeledMethod], ); const handleChange = useCallback( (e: ChangeEvent) => { - if (!modeledMethod) { + if (!modeledMethod || !modeledMethodSupportsInput(modeledMethod)) { return; } @@ -53,9 +55,14 @@ export const ModelInputDropdown = ({ [onChange, modeledMethod], ); + const value = + modeledMethod && modeledMethodSupportsInput(modeledMethod) + ? modeledMethod.input + : undefined; + return ( { - if (!modeledMethod) { + if (!modeledMethod || !modeledMethodSupportsKind(modeledMethod)) { return; } @@ -63,19 +64,26 @@ export const ModelKindDropdown = ({ [onChangeKind], ); + const value = + modeledMethod && modeledMethodSupportsKind(modeledMethod) + ? modeledMethod.kind + : undefined; + useEffect(() => { - const value = modeledMethod?.kind ?? ""; + if (!modeledMethod || !modeledMethodSupportsKind(modeledMethod)) { + return; + } if (kinds.length === 0 && value !== "") { onChangeKind(""); - } else if (kinds.length > 0 && !kinds.includes(value)) { + } else if (kinds.length > 0 && !kinds.includes(value ?? "")) { onChangeKind(kinds[0]); } - }, [modeledMethod?.kind, kinds, onChangeKind]); + }, [modeledMethod, value, kinds, onChangeKind]); return ( - modeledMethod?.type && - ["source", "summary"].includes(modeledMethod?.type), - [modeledMethod?.type], + () => modeledMethod && modeledMethodSupportsOutput(modeledMethod), + [modeledMethod], ); const handleChange = useCallback( (e: ChangeEvent) => { - if (!modeledMethod) { + if (!modeledMethod || !modeledMethodSupportsOutput(modeledMethod)) { return; } @@ -55,9 +56,14 @@ export const ModelOutputDropdown = ({ [onChange, modeledMethod], ); + const value = + modeledMethod && modeledMethodSupportsOutput(modeledMethod) + ? modeledMethod.output + : undefined; + return ( = [ { value: "none", label: "Unmodeled" }, @@ -35,25 +38,36 @@ export const ModelTypeDropdown = ({ const handleChange = useCallback( (e: ChangeEvent) => { let newProvenance: Provenance = "manual"; - if (modeledMethod?.provenance === "df-generated") { - newProvenance = "df-manual"; - } else if (modeledMethod?.provenance === "ai-generated") { - newProvenance = "ai-manual"; + if (modeledMethod && modeledMethodSupportsProvenance(modeledMethod)) { + if (modeledMethod.provenance === "df-generated") { + newProvenance = "df-manual"; + } else if (modeledMethod.provenance === "ai-generated") { + newProvenance = "ai-manual"; + } } - const updatedModeledMethod: ModeledMethod = { - // If there are no arguments, we will default to "Argument[this]" - input: argumentsList.length === 0 ? "Argument[this]" : "Argument[0]", - output: "ReturnValue", - kind: "value", - type: e.target.value as ModeledMethodType, - provenance: newProvenance, - signature: method.signature, - packageName: method.packageName, - typeName: method.typeName, - methodName: method.methodName, - methodParameters: method.methodParameters, + const emptyModeledMethod = createEmptyModeledMethod( + e.target.value as ModeledMethodType, + method, + ); + const updatedModeledMethod: Mutable = { + ...emptyModeledMethod, }; + if ("input" in updatedModeledMethod) { + // If there are no arguments, we will default to "Argument[this]" + updatedModeledMethod.input = + argumentsList.length === 0 ? "Argument[this]" : "Argument[0]"; + } + if ("output" in updatedModeledMethod) { + updatedModeledMethod.output = "ReturnValue"; + } + if ("provenance" in updatedModeledMethod) { + updatedModeledMethod.provenance = newProvenance; + } + if ("kind" in updatedModeledMethod) { + updatedModeledMethod.kind = "value"; + } + onChange(updatedModeledMethod); }, [onChange, method, modeledMethod, argumentsList], diff --git a/extensions/ql-vscode/src/view/model-editor/__tests__/LibraryRow.spec.tsx b/extensions/ql-vscode/src/view/model-editor/__tests__/LibraryRow.spec.tsx index 0dff8795d..ab9d1485f 100644 --- a/extensions/ql-vscode/src/view/model-editor/__tests__/LibraryRow.spec.tsx +++ b/extensions/ql-vscode/src/view/model-editor/__tests__/LibraryRow.spec.tsx @@ -27,7 +27,6 @@ describe(LibraryRow.name, () => { ...method, type: "sink", input: "Argument[0]", - output: "", kind: "jndi-injection", provenance: "df-generated", }, diff --git a/extensions/ql-vscode/src/view/model-editor/__tests__/MethodRow.spec.tsx b/extensions/ql-vscode/src/view/model-editor/__tests__/MethodRow.spec.tsx index fea123c51..bdd303e98 100644 --- a/extensions/ql-vscode/src/view/model-editor/__tests__/MethodRow.spec.tsx +++ b/extensions/ql-vscode/src/view/model-editor/__tests__/MethodRow.spec.tsx @@ -78,7 +78,6 @@ describe(MethodRow.name, () => { expect(onChange).toHaveBeenCalledWith(method.signature, [ { type: "source", - input: "Argument[0]", output: "ReturnValue", kind: "value", provenance: "manual", @@ -367,10 +366,6 @@ describe(MethodRow.name, () => { modeledMethod, { type: "none", - input: "", - output: "", - kind: "", - provenance: "manual", signature: method.signature, packageName: method.packageName, typeName: method.typeName, diff --git a/extensions/ql-vscode/src/view/model-editor/__tests__/ModelKindDropdown.spec.tsx b/extensions/ql-vscode/src/view/model-editor/__tests__/ModelKindDropdown.spec.tsx index 6a6cf024f..6591a9080 100644 --- a/extensions/ql-vscode/src/view/model-editor/__tests__/ModelKindDropdown.spec.tsx +++ b/extensions/ql-vscode/src/view/model-editor/__tests__/ModelKindDropdown.spec.tsx @@ -2,7 +2,11 @@ import * as React from "react"; import { render, screen } from "@testing-library/react"; import { ModelKindDropdown } from "../ModelKindDropdown"; import userEvent from "@testing-library/user-event"; -import { createModeledMethod } from "../../../../test/factories/model-editor/modeled-method-factories"; +import { + createNoneModeledMethod, + createSinkModeledMethod, + createSourceModeledMethod, +} from "../../../../test/factories/model-editor/modeled-method-factories"; import { QueryLanguage } from "../../../common/query-language"; describe(ModelKindDropdown.name, () => { @@ -13,8 +17,7 @@ describe(ModelKindDropdown.name, () => { }); it("allows changing the kind", async () => { - const modeledMethod = createModeledMethod({ - type: "source", + const modeledMethod = createSourceModeledMethod({ kind: "local", }); @@ -36,8 +39,7 @@ describe(ModelKindDropdown.name, () => { }); it("resets the kind when changing the supported kinds", () => { - const modeledMethod = createModeledMethod({ - type: "source", + const modeledMethod = createSourceModeledMethod({ kind: "local", }); @@ -53,8 +55,7 @@ describe(ModelKindDropdown.name, () => { expect(onChange).not.toHaveBeenCalled(); // Changing the type to sink should update the supported kinds - const updatedModeledMethod = createModeledMethod({ - type: "sink", + const updatedModeledMethod = createSinkModeledMethod({ kind: "local", }); @@ -70,8 +71,9 @@ describe(ModelKindDropdown.name, () => { }); it("sets the kind when value is undefined", () => { - const modeledMethod = createModeledMethod({ + const modeledMethod = createSourceModeledMethod({ type: "source", + kind: undefined, }); render( @@ -91,10 +93,7 @@ describe(ModelKindDropdown.name, () => { }); it("does not call onChange when unmodeled and the kind is valid", () => { - const modeledMethod = createModeledMethod({ - type: "none", - kind: "", - }); + const modeledMethod = createNoneModeledMethod(); render( { expect(onChange).not.toHaveBeenCalled(); }); - - it("calls onChange when unmodeled and the kind is valid", () => { - const modeledMethod = createModeledMethod({ - type: "none", - kind: "local", - }); - - render( - , - ); - - expect(onChange).toHaveBeenCalledTimes(1); - expect(onChange).toHaveBeenCalledWith( - expect.objectContaining({ - kind: "", - }), - ); - }); }); diff --git a/extensions/ql-vscode/src/view/model-editor/__tests__/ModeledMethodDataGrid.spec.tsx b/extensions/ql-vscode/src/view/model-editor/__tests__/ModeledMethodDataGrid.spec.tsx index e3724fadc..1bb1529ba 100644 --- a/extensions/ql-vscode/src/view/model-editor/__tests__/ModeledMethodDataGrid.spec.tsx +++ b/extensions/ql-vscode/src/view/model-editor/__tests__/ModeledMethodDataGrid.spec.tsx @@ -53,7 +53,6 @@ describe(ModeledMethodDataGrid.name, () => { ...method1, type: "sink", input: "Argument[0]", - output: "", kind: "jndi-injection", provenance: "df-generated", }, diff --git a/extensions/ql-vscode/src/view/model-editor/__tests__/ModeledMethodsList.spec.tsx b/extensions/ql-vscode/src/view/model-editor/__tests__/ModeledMethodsList.spec.tsx index ba3a324e2..08b946369 100644 --- a/extensions/ql-vscode/src/view/model-editor/__tests__/ModeledMethodsList.spec.tsx +++ b/extensions/ql-vscode/src/view/model-editor/__tests__/ModeledMethodsList.spec.tsx @@ -53,7 +53,6 @@ describe(ModeledMethodsList.name, () => { ...method1, type: "sink", input: "Argument[0]", - output: "", kind: "jndi-injection", provenance: "df-generated", }, diff --git a/extensions/ql-vscode/test/factories/model-editor/modeled-method-factories.ts b/extensions/ql-vscode/test/factories/model-editor/modeled-method-factories.ts index 9c8b1207b..9706701f8 100644 --- a/extensions/ql-vscode/test/factories/model-editor/modeled-method-factories.ts +++ b/extensions/ql-vscode/test/factories/model-editor/modeled-method-factories.ts @@ -1,8 +1,15 @@ -import { ModeledMethod } from "../../../src/model-editor/modeled-method"; +import { + NeutralModeledMethod, + NoneModeledMethod, + SinkModeledMethod, + SourceModeledMethod, + SummaryModeledMethod, +} from "../../../src/model-editor/modeled-method"; +import { MethodSignature } from "../../../src/model-editor/method"; -export function createModeledMethod( - data: Partial = {}, -): ModeledMethod { +export function createMethodSignature( + data: Partial = {}, +): MethodSignature { return { libraryVersion: "1.6.0", signature: "org.sql2o.Connection#createQuery(String)", @@ -10,11 +17,68 @@ export function createModeledMethod( typeName: "Connection", methodName: "createQuery", methodParameters: "(String)", + ...data, + }; +} + +export function createNoneModeledMethod( + data: Partial = {}, +): NoneModeledMethod { + return { + ...createMethodSignature(), + type: "none", + ...data, + }; +} + +export function createSinkModeledMethod( + data: Partial = {}, +): SinkModeledMethod { + return { + ...createMethodSignature(), type: "sink", input: "Argument[0]", - output: "", kind: "path-injection", provenance: "manual", ...data, }; } + +export function createSourceModeledMethod( + data: Partial = {}, +): SourceModeledMethod { + return { + ...createMethodSignature(), + type: "source", + output: "ReturnValue", + kind: "remote", + provenance: "manual", + ...data, + }; +} + +export function createSummaryModeledMethod( + data: Partial = {}, +): SummaryModeledMethod { + return { + ...createMethodSignature(), + type: "summary", + input: "Argument[this]", + output: "ReturnValue", + kind: "taint", + provenance: "manual", + ...data, + }; +} + +export function createNeutralModeledMethod( + data: Partial = {}, +): NeutralModeledMethod { + return { + ...createMethodSignature(), + type: "neutral", + kind: "summary", + provenance: "manual", + ...data, + }; +} diff --git a/extensions/ql-vscode/test/unit-tests/model-editor/auto-model.test.ts b/extensions/ql-vscode/test/unit-tests/model-editor/auto-model.test.ts index 7389ff8f6..536d01b51 100644 --- a/extensions/ql-vscode/test/unit-tests/model-editor/auto-model.test.ts +++ b/extensions/ql-vscode/test/unit-tests/model-editor/auto-model.test.ts @@ -103,9 +103,7 @@ describe("getCandidates", () => { "org.my.A#x()": [ { type: "neutral", - kind: "", - input: "", - output: "", + kind: "sink", provenance: "manual", signature: "org.my.A#x()", packageName: "org.my", diff --git a/extensions/ql-vscode/test/unit-tests/model-editor/modeled-method.test.ts b/extensions/ql-vscode/test/unit-tests/model-editor/modeled-method.test.ts new file mode 100644 index 000000000..67a37f6c5 --- /dev/null +++ b/extensions/ql-vscode/test/unit-tests/model-editor/modeled-method.test.ts @@ -0,0 +1,52 @@ +import { createNoneModeledMethod } from "../../factories/model-editor/modeled-method-factories"; +import { + ModeledMethod, + modeledMethodSupportsInput, + modeledMethodSupportsKind, + modeledMethodSupportsOutput, + modeledMethodSupportsProvenance, +} from "../../../src/model-editor/modeled-method"; + +describe("modeledMethodSupportsKind", () => { + const modeledMethod = createNoneModeledMethod() as ModeledMethod; + + it("can access the kind property", () => { + // These are more type tests than unit tests, but they're still useful. + if (modeledMethodSupportsKind(modeledMethod)) { + expect(modeledMethod.kind).not.toBeUndefined(); // never hit + } + }); +}); + +describe("modeledMethodSupportsInput", () => { + const modeledMethod = createNoneModeledMethod() as ModeledMethod; + + it("can access the input property", () => { + // These are more type tests than unit tests, but they're still useful. + if (modeledMethodSupportsInput(modeledMethod)) { + expect(modeledMethod.input).not.toBeUndefined(); // never hit + } + }); +}); + +describe("modeledMethodSupportsOutput", () => { + const modeledMethod = createNoneModeledMethod() as ModeledMethod; + + it("can access the output property", () => { + // These are more type tests than unit tests, but they're still useful. + if (modeledMethodSupportsOutput(modeledMethod)) { + expect(modeledMethod.output).not.toBeUndefined(); // never hit + } + }); +}); + +describe("modeledMethodSupportsProvenance", () => { + const modeledMethod = createNoneModeledMethod() as ModeledMethod; + + it("can access the provenance property", () => { + // These are more type tests than unit tests, but they're still useful. + if (modeledMethodSupportsProvenance(modeledMethod)) { + expect(modeledMethod.provenance).not.toBeUndefined(); + } + }); +}); diff --git a/extensions/ql-vscode/test/unit-tests/model-editor/shared/validation.test.ts b/extensions/ql-vscode/test/unit-tests/model-editor/shared/validation.test.ts index 51a8d8183..572cbe5b7 100644 --- a/extensions/ql-vscode/test/unit-tests/model-editor/shared/validation.test.ts +++ b/extensions/ql-vscode/test/unit-tests/model-editor/shared/validation.test.ts @@ -1,15 +1,19 @@ import { validateModeledMethods } from "../../../../src/model-editor/shared/validation"; -import { createModeledMethod } from "../../../factories/model-editor/modeled-method-factories"; +import { + createNeutralModeledMethod, + createNoneModeledMethod, + createSinkModeledMethod, + createSourceModeledMethod, + createSummaryModeledMethod, +} from "../../../factories/model-editor/modeled-method-factories"; describe(validateModeledMethods.name, () => { it("should not give an error with valid modeled methods", () => { const modeledMethods = [ - createModeledMethod({ - type: "source", + createSourceModeledMethod({ output: "ReturnValue", }), - createModeledMethod({ - type: "sink", + createSinkModeledMethod({ input: "Argument[this]", }), ]; @@ -21,17 +25,13 @@ describe(validateModeledMethods.name, () => { it("should not give an error with valid modeled methods and an unmodeled method", () => { const modeledMethods = [ - createModeledMethod({ - type: "source", + createSourceModeledMethod({ output: "ReturnValue", }), - createModeledMethod({ - type: "sink", + createSinkModeledMethod({ input: "Argument[this]", }), - createModeledMethod({ - type: "none", - }), + createNoneModeledMethod(), ]; const errors = validateModeledMethods(modeledMethods); @@ -41,20 +41,14 @@ describe(validateModeledMethods.name, () => { it("should not give an error with valid modeled methods and multiple unmodeled methods", () => { const modeledMethods = [ - createModeledMethod({ - type: "none", - }), - createModeledMethod({ - type: "source", + createNoneModeledMethod(), + createSourceModeledMethod({ output: "ReturnValue", }), - createModeledMethod({ - type: "sink", + createSinkModeledMethod({ input: "Argument[this]", }), - createModeledMethod({ - type: "none", - }), + createNoneModeledMethod(), ]; const errors = validateModeledMethods(modeledMethods); @@ -63,11 +57,7 @@ describe(validateModeledMethods.name, () => { }); it("should not give an error with a single neutral model", () => { - const modeledMethods = [ - createModeledMethod({ - type: "neutral", - }), - ]; + const modeledMethods = [createNeutralModeledMethod()]; const errors = validateModeledMethods(modeledMethods); @@ -76,12 +66,8 @@ describe(validateModeledMethods.name, () => { it("should not give an error with a neutral model and an unmodeled method", () => { const modeledMethods = [ - createModeledMethod({ - type: "neutral", - }), - createModeledMethod({ - type: "none", - }), + createNeutralModeledMethod(), + createNoneModeledMethod(), ]; const errors = validateModeledMethods(modeledMethods); @@ -90,7 +76,10 @@ describe(validateModeledMethods.name, () => { }); it("should give an error with exact duplicate modeled methods", () => { - const modeledMethods = [createModeledMethod(), createModeledMethod()]; + const modeledMethods = [ + createSinkModeledMethod(), + createSinkModeledMethod(), + ]; const errors = validateModeledMethods(modeledMethods); @@ -106,10 +95,10 @@ describe(validateModeledMethods.name, () => { it("should give an error with duplicate modeled methods with different provenance", () => { const modeledMethods = [ - createModeledMethod({ + createSinkModeledMethod({ provenance: "df-generated", }), - createModeledMethod({ + createSinkModeledMethod({ provenance: "manual", }), ]; @@ -127,18 +116,21 @@ describe(validateModeledMethods.name, () => { }); it("should give an error with duplicate modeled methods with different source unused fields", () => { - const modeledMethods = [ - createModeledMethod({ - type: "source", + const modeledMethod1 = createSourceModeledMethod({ + output: "ReturnValue", + ...{ input: "Argument[this]", - output: "ReturnValue", - }), - createModeledMethod({ - type: "source", + }, + }); + + const modeledMethod2 = createSourceModeledMethod({ + output: "ReturnValue", + ...{ input: "Argument[1]", - output: "ReturnValue", - }), - ]; + }, + }); + + const modeledMethods = [modeledMethod1, modeledMethod2]; const errors = validateModeledMethods(modeledMethods); @@ -153,18 +145,22 @@ describe(validateModeledMethods.name, () => { }); it("should give an error with duplicate modeled methods with different sink unused fields", () => { - const modeledMethods = [ - createModeledMethod({ - type: "sink", - input: "Argument[this]", + const modeledMethod1 = createSinkModeledMethod({ + type: "sink", + input: "Argument[this]", + ...{ output: "ReturnValue", - }), - createModeledMethod({ - type: "sink", - input: "Argument[this]", + }, + }); + const modeledMethod2 = createSinkModeledMethod({ + type: "sink", + input: "Argument[this]", + ...{ output: "Argument[this]", - }), - ]; + }, + }); + + const modeledMethods = [modeledMethod1, modeledMethod2]; const errors = validateModeledMethods(modeledMethods); @@ -187,16 +183,14 @@ describe(validateModeledMethods.name, () => { }; const modeledMethods = [ - createModeledMethod({ - type: "sink", + createSummaryModeledMethod({ input: "Argument[this]", output: "ReturnValue", ...supportedTrue, }), - createModeledMethod({ - type: "sink", + createSummaryModeledMethod({ input: "Argument[this]", - output: "Argument[this]", + output: "ReturnValue", ...supportedFalse, }), ]; @@ -215,15 +209,19 @@ describe(validateModeledMethods.name, () => { it("should give an error with duplicate modeled methods with different neutral unused fields", () => { const modeledMethods = [ - createModeledMethod({ + createNeutralModeledMethod({ type: "neutral", - input: "Argument[this]", - output: "ReturnValue", + ...{ + input: "Argument[this]", + output: "ReturnValue", + }, }), - createModeledMethod({ + createNeutralModeledMethod({ type: "neutral", - input: "Argument[1]", - output: "Argument[this]", + ...{ + input: "Argument[1]", + output: "Argument[this]", + }, }), ]; @@ -241,11 +239,8 @@ describe(validateModeledMethods.name, () => { it("should give an error with neutral combined with other models", () => { const modeledMethods = [ - createModeledMethod({ - type: "sink", - }), - createModeledMethod({ - type: "neutral", + createSinkModeledMethod(), + createNeutralModeledMethod({ kind: "sink", }), ]; @@ -264,11 +259,8 @@ describe(validateModeledMethods.name, () => { it("should not give an error with other neutral combined with other models", () => { const modeledMethods = [ - createModeledMethod({ - type: "sink", - }), - createModeledMethod({ - type: "neutral", + createSinkModeledMethod(), + createNeutralModeledMethod({ kind: "summary", }), ]; @@ -280,15 +272,11 @@ describe(validateModeledMethods.name, () => { it("should give an error with duplicate neutral combined with other models", () => { const modeledMethods = [ - createModeledMethod({ - type: "neutral", + createNeutralModeledMethod({ kind: "summary", }), - createModeledMethod({ - type: "summary", - }), - createModeledMethod({ - type: "neutral", + createSummaryModeledMethod(), + createNeutralModeledMethod({ kind: "summary", }), ]; @@ -313,18 +301,12 @@ describe(validateModeledMethods.name, () => { it("should include unmodeled methods in the index", () => { const modeledMethods = [ - createModeledMethod({ - type: "none", - }), - createModeledMethod({ - type: "neutral", + createNoneModeledMethod(), + createNeutralModeledMethod({ kind: "sink", }), - createModeledMethod({ - type: "sink", - }), - createModeledMethod({ - type: "neutral", + createSinkModeledMethod(), + createNeutralModeledMethod({ kind: "sink", }), ]; diff --git a/extensions/ql-vscode/test/unit-tests/model-editor/yaml.test.ts b/extensions/ql-vscode/test/unit-tests/model-editor/yaml.test.ts index 91a06419a..f6dbf9e0b 100644 --- a/extensions/ql-vscode/test/unit-tests/model-editor/yaml.test.ts +++ b/extensions/ql-vscode/test/unit-tests/model-editor/yaml.test.ts @@ -15,7 +15,6 @@ describe("createDataExtensionYaml", () => { { type: "sink", input: "Argument[0]", - output: "", kind: "sql", provenance: "df-generated", signature: "org.sql2o.Connection#createQuery(String)", @@ -230,7 +229,6 @@ describe("createDataExtensionYamlsForApplicationMode", () => { { type: "sink", input: "Argument[0]", - output: "", kind: "sql", provenance: "df-generated", signature: "org.sql2o.Connection#createQuery(String)", @@ -243,8 +241,6 @@ describe("createDataExtensionYamlsForApplicationMode", () => { "org.springframework.boot.SpringApplication#run(Class,String[])": [ { type: "neutral", - input: "", - output: "", kind: "summary", provenance: "manual", signature: @@ -259,7 +255,6 @@ describe("createDataExtensionYamlsForApplicationMode", () => { { type: "sink", input: "Argument[0]", - output: "", kind: "jndi", provenance: "manual", signature: "org.sql2o.Sql2o#Sql2o(String,String,String)", @@ -474,7 +469,6 @@ describe("createDataExtensionYamlsForApplicationMode", () => { { type: "sink", input: "Argument[0]", - output: "", kind: "sql", provenance: "df-generated", signature: "org.sql2o.Connection#createQuery(String)", @@ -487,8 +481,6 @@ describe("createDataExtensionYamlsForApplicationMode", () => { "org.springframework.boot.SpringApplication#run(Class,String[])": [ { type: "neutral", - input: "", - output: "", kind: "summary", provenance: "manual", signature: @@ -503,7 +495,6 @@ describe("createDataExtensionYamlsForApplicationMode", () => { { type: "sink", input: "Argument[0]", - output: "", kind: "jndi", provenance: "manual", signature: "org.sql2o.Sql2o#Sql2o(String,String,String)", @@ -519,8 +510,6 @@ describe("createDataExtensionYamlsForApplicationMode", () => { "org.sql2o.Connection#createQuery(String)": [ { type: "neutral", - input: "", - output: "", kind: "summary", provenance: "manual", signature: "org.sql2o.Connection#createQuery(String)", @@ -533,8 +522,6 @@ describe("createDataExtensionYamlsForApplicationMode", () => { "org.sql2o.Query#executeScalar(Class)": [ { type: "neutral", - input: "", - output: "", kind: "summary", provenance: "manual", signature: "org.sql2o.Query#executeScalar(Class)", @@ -718,7 +705,6 @@ describe("createDataExtensionYamlsForFrameworkMode", () => { { type: "sink", input: "Argument[0]", - output: "", kind: "sql", provenance: "df-generated", signature: "org.sql2o.Connection#createQuery(String)", @@ -732,7 +718,6 @@ describe("createDataExtensionYamlsForFrameworkMode", () => { { type: "sink", input: "Argument[0]", - output: "", kind: "jndi", provenance: "manual", signature: "org.sql2o.Sql2o#Sql2o(String,String,String)", @@ -874,7 +859,6 @@ describe("createDataExtensionYamlsForFrameworkMode", () => { { type: "sink", input: "Argument[0]", - output: "", kind: "sql", provenance: "df-generated", signature: "org.sql2o.Connection#createQuery(String)", @@ -888,7 +872,6 @@ describe("createDataExtensionYamlsForFrameworkMode", () => { { type: "sink", input: "Argument[0]", - output: "", kind: "jndi", provenance: "manual", signature: "org.sql2o.Sql2o#Sql2o(String,String,String)", @@ -904,8 +887,6 @@ describe("createDataExtensionYamlsForFrameworkMode", () => { "org.sql2o.Connection#createQuery(String)": [ { type: "neutral", - input: "", - output: "", kind: "summary", provenance: "manual", signature: "org.sql2o.Connection#createQuery(String)", @@ -918,8 +899,6 @@ describe("createDataExtensionYamlsForFrameworkMode", () => { "org.sql2o.Query#executeScalar(Class)": [ { type: "neutral", - input: "", - output: "", kind: "summary", provenance: "manual", signature: "org.sql2o.Query#executeScalar(Class)", diff --git a/extensions/ql-vscode/test/vscode-tests/no-workspace/model-editor/methods-usage/methods-usage-data-provider.test.ts b/extensions/ql-vscode/test/vscode-tests/no-workspace/model-editor/methods-usage/methods-usage-data-provider.test.ts index 701a24f06..9ee644c70 100644 --- a/extensions/ql-vscode/test/vscode-tests/no-workspace/model-editor/methods-usage/methods-usage-data-provider.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/no-workspace/model-editor/methods-usage/methods-usage-data-provider.test.ts @@ -15,7 +15,7 @@ import { import { mockedObject } from "../../../utils/mocking.helpers"; import { ModeledMethod } from "../../../../../src/model-editor/modeled-method"; import { Mode } from "../../../../../src/model-editor/shared/mode"; -import { createModeledMethod } from "../../../../factories/model-editor/modeled-method-factories"; +import { createSinkModeledMethod } from "../../../../factories/model-editor/modeled-method-factories"; describe("MethodsUsageDataProvider", () => { const mockCliServer = mockedObject({}); @@ -252,9 +252,11 @@ describe("MethodsUsageDataProvider", () => { unsupportedModeledMethod, ]; const modeledMethods: Record = {}; - modeledMethods[supportedModeledMethod.signature] = [createModeledMethod()]; + modeledMethods[supportedModeledMethod.signature] = [ + createSinkModeledMethod(), + ]; modeledMethods[unsupportedModeledMethod.signature] = [ - createModeledMethod(), + createSinkModeledMethod(), ]; const modifiedMethodSignatures: Set = new Set();