Merge pull request #3017 from hmac/hmac-model-editor-ruby
Add experimental model editor support for Ruby
This commit is contained in:
@@ -707,12 +707,14 @@ const LLM_GENERATION_BATCH_SIZE = new Setting(
|
||||
MODEL_SETTING,
|
||||
);
|
||||
const EXTENSIONS_DIRECTORY = new Setting("extensionsDirectory", MODEL_SETTING);
|
||||
const ENABLE_RUBY = new Setting("enableRuby", MODEL_SETTING);
|
||||
|
||||
export interface ModelConfig {
|
||||
flowGeneration: boolean;
|
||||
llmGeneration: boolean;
|
||||
getExtensionsDirectory(languageId: string): string | undefined;
|
||||
showMultipleModels: boolean;
|
||||
enableRuby: boolean;
|
||||
}
|
||||
|
||||
export class ModelConfigListener extends ConfigListener implements ModelConfig {
|
||||
@@ -745,4 +747,8 @@ export class ModelConfigListener extends ConfigListener implements ModelConfig {
|
||||
public get showMultipleModels(): boolean {
|
||||
return isCanary();
|
||||
}
|
||||
|
||||
public get enableRuby(): boolean {
|
||||
return !!ENABLE_RUBY.getValue<boolean>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { QueryLanguage } from "../../common/query-language";
|
||||
import { ModelsAsDataLanguage } from "./models-as-data";
|
||||
import { ruby } from "./ruby";
|
||||
import { staticLanguage } from "./static";
|
||||
|
||||
const languages: Partial<Record<QueryLanguage, ModelsAsDataLanguage>> = {
|
||||
[QueryLanguage.CSharp]: staticLanguage,
|
||||
[QueryLanguage.Java]: staticLanguage,
|
||||
[QueryLanguage.Ruby]: ruby,
|
||||
};
|
||||
|
||||
export function getModelsAsDataLanguage(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { MethodDefinition } from "../method";
|
||||
import { ModeledMethod, ModeledMethodType } from "../modeled-method";
|
||||
import { DataTuple } from "../model-extension-file";
|
||||
import { Mode } from "../shared/mode";
|
||||
|
||||
type GenerateMethodDefinition = (method: ModeledMethod) => DataTuple[];
|
||||
type ReadModeledMethod = (row: DataTuple[]) => ModeledMethod;
|
||||
@@ -20,6 +21,11 @@ export type ModelsAsDataLanguagePredicates = Record<
|
||||
>;
|
||||
|
||||
export type ModelsAsDataLanguage = {
|
||||
/**
|
||||
* The modes that are available for this language. If not specified, all
|
||||
* modes are available.
|
||||
*/
|
||||
availableModes?: Mode[];
|
||||
createMethodSignature: (method: MethodDefinition) => string;
|
||||
predicates: ModelsAsDataLanguagePredicates;
|
||||
};
|
||||
|
||||
153
extensions/ql-vscode/src/model-editor/languages/ruby.ts
Normal file
153
extensions/ql-vscode/src/model-editor/languages/ruby.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import { ModelsAsDataLanguage } from "./models-as-data";
|
||||
import { sharedExtensiblePredicates, sharedKinds } from "./shared";
|
||||
import { Mode } from "../shared/mode";
|
||||
|
||||
function parseRubyMethodFromPath(path: string): string {
|
||||
const match = path.match(/Method\[([^\]]+)].*/);
|
||||
if (match) {
|
||||
return match[1];
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function parseRubyAccessPath(path: string): {
|
||||
methodName: string;
|
||||
path: string;
|
||||
} {
|
||||
const match = path.match(/Method\[([^\]]+)]\.(.*)/);
|
||||
if (match) {
|
||||
return { methodName: match[1], path: match[2] };
|
||||
} else {
|
||||
return { methodName: "", path: "" };
|
||||
}
|
||||
}
|
||||
|
||||
function rubyMethodSignature(typeName: string, methodName: string) {
|
||||
return `${typeName}#${methodName}`;
|
||||
}
|
||||
|
||||
export const ruby: ModelsAsDataLanguage = {
|
||||
availableModes: [Mode.Framework],
|
||||
createMethodSignature: ({ typeName, methodName }) =>
|
||||
`${typeName}#${methodName}`,
|
||||
predicates: {
|
||||
source: {
|
||||
extensiblePredicate: sharedExtensiblePredicates.source,
|
||||
supportedKinds: sharedKinds.source,
|
||||
// extensible predicate sourceModel(
|
||||
// string type, string path, string kind
|
||||
// );
|
||||
generateMethodDefinition: (method) => [
|
||||
method.typeName,
|
||||
`Method[${method.methodName}].${method.output}`,
|
||||
method.kind,
|
||||
],
|
||||
readModeledMethod: (row) => {
|
||||
const typeName = row[0] as string;
|
||||
const { methodName, path: output } = parseRubyAccessPath(
|
||||
row[1] as string,
|
||||
);
|
||||
return {
|
||||
type: "source",
|
||||
input: "",
|
||||
output,
|
||||
kind: row[2] as string,
|
||||
provenance: "manual",
|
||||
signature: rubyMethodSignature(typeName, methodName),
|
||||
packageName: "",
|
||||
typeName,
|
||||
methodName,
|
||||
methodParameters: "",
|
||||
};
|
||||
},
|
||||
},
|
||||
sink: {
|
||||
extensiblePredicate: sharedExtensiblePredicates.sink,
|
||||
supportedKinds: sharedKinds.sink,
|
||||
// extensible predicate sinkModel(
|
||||
// string type, string path, string kind
|
||||
// );
|
||||
generateMethodDefinition: (method) => {
|
||||
const path = `Method[${method.methodName}].${method.input}`;
|
||||
return [method.typeName, path, method.kind];
|
||||
},
|
||||
readModeledMethod: (row) => {
|
||||
const typeName = row[0] as string;
|
||||
const { methodName, path: input } = parseRubyAccessPath(
|
||||
row[1] as string,
|
||||
);
|
||||
return {
|
||||
type: "sink",
|
||||
input,
|
||||
output: "",
|
||||
kind: row[2] as string,
|
||||
provenance: "manual",
|
||||
signature: rubyMethodSignature(typeName, methodName),
|
||||
packageName: "",
|
||||
typeName,
|
||||
methodName,
|
||||
methodParameters: "",
|
||||
};
|
||||
},
|
||||
},
|
||||
summary: {
|
||||
extensiblePredicate: sharedExtensiblePredicates.summary,
|
||||
supportedKinds: sharedKinds.summary,
|
||||
// extensible predicate summaryModel(
|
||||
// string type, string path, string input, string output, string kind
|
||||
// );
|
||||
generateMethodDefinition: (method) => [
|
||||
method.typeName,
|
||||
`Method[${method.methodName}]`,
|
||||
method.input,
|
||||
method.output,
|
||||
method.kind,
|
||||
],
|
||||
readModeledMethod: (row) => {
|
||||
const typeName = row[0] as string;
|
||||
const methodName = parseRubyMethodFromPath(row[1] as string);
|
||||
return {
|
||||
type: "summary",
|
||||
input: row[2] as string,
|
||||
output: row[3] as string,
|
||||
kind: row[4] as string,
|
||||
provenance: "manual",
|
||||
signature: rubyMethodSignature(typeName, methodName),
|
||||
packageName: "",
|
||||
typeName,
|
||||
methodName,
|
||||
methodParameters: "",
|
||||
};
|
||||
},
|
||||
},
|
||||
neutral: {
|
||||
extensiblePredicate: sharedExtensiblePredicates.neutral,
|
||||
supportedKinds: sharedKinds.neutral,
|
||||
// extensible predicate neutralModel(
|
||||
// string type, string path, string kind
|
||||
// );
|
||||
generateMethodDefinition: (method) => [
|
||||
method.typeName,
|
||||
`Method[${method.methodName}]`,
|
||||
method.kind,
|
||||
],
|
||||
readModeledMethod: (row) => {
|
||||
const typeName = row[0] as string;
|
||||
const methodName = parseRubyMethodFromPath(row[1] as string);
|
||||
return {
|
||||
type: "neutral",
|
||||
input: "",
|
||||
output: "",
|
||||
kind: row[2] as string,
|
||||
provenance: "manual",
|
||||
signature: rubyMethodSignature(typeName, methodName),
|
||||
packageName: "",
|
||||
typeName,
|
||||
methodName,
|
||||
methodParameters: "",
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -22,8 +22,9 @@ import { showResolvableLocation } from "../databases/local-databases/locations";
|
||||
import { ModelEditorViewTracker } from "./model-editor-view-tracker";
|
||||
import { ModelConfigListener } from "../config";
|
||||
import { ModelingEvents } from "./modeling-events";
|
||||
|
||||
const SUPPORTED_LANGUAGES: string[] = ["java", "csharp"];
|
||||
import { getModelsAsDataLanguage } from "./languages";
|
||||
import { INITIAL_MODE } from "./shared/mode";
|
||||
import { isSupportedLanguage } from "./supported-languages";
|
||||
|
||||
export class ModelEditorModule extends DisposableObject {
|
||||
private readonly queryStorageDir: string;
|
||||
@@ -32,6 +33,7 @@ export class ModelEditorModule extends DisposableObject {
|
||||
private readonly editorViewTracker: ModelEditorViewTracker<ModelEditorView>;
|
||||
private readonly methodsUsagePanel: MethodsUsagePanel;
|
||||
private readonly methodModelingPanel: MethodModelingPanel;
|
||||
private readonly modelConfig: ModelConfigListener;
|
||||
|
||||
private constructor(
|
||||
private readonly app: App,
|
||||
@@ -56,6 +58,7 @@ export class ModelEditorModule extends DisposableObject {
|
||||
this.editorViewTracker,
|
||||
),
|
||||
);
|
||||
this.modelConfig = this.push(new ModelConfigListener());
|
||||
|
||||
this.registerToModelingEvents();
|
||||
}
|
||||
@@ -125,9 +128,10 @@ export class ModelEditorModule extends DisposableObject {
|
||||
}
|
||||
|
||||
const language = db.language;
|
||||
|
||||
if (
|
||||
!SUPPORTED_LANGUAGES.includes(language) ||
|
||||
!isQueryLanguage(language)
|
||||
!isQueryLanguage(language) ||
|
||||
!isSupportedLanguage(language, this.modelConfig)
|
||||
) {
|
||||
void showAndLogErrorMessage(
|
||||
this.app.logger,
|
||||
@@ -136,6 +140,10 @@ export class ModelEditorModule extends DisposableObject {
|
||||
return;
|
||||
}
|
||||
|
||||
const definition = getModelsAsDataLanguage(language);
|
||||
|
||||
const initialMode = definition.availableModes?.[0] ?? INITIAL_MODE;
|
||||
|
||||
const existingView = this.editorViewTracker.getView(
|
||||
db.databaseUri.toString(),
|
||||
);
|
||||
@@ -167,12 +175,10 @@ export class ModelEditorModule extends DisposableObject {
|
||||
return;
|
||||
}
|
||||
|
||||
const modelConfig = this.push(new ModelConfigListener());
|
||||
|
||||
const modelFile = await pickExtensionPack(
|
||||
this.cliServer,
|
||||
db,
|
||||
modelConfig,
|
||||
this.modelConfig,
|
||||
this.app.logger,
|
||||
progress,
|
||||
maxStep,
|
||||
@@ -196,7 +202,7 @@ export class ModelEditorModule extends DisposableObject {
|
||||
this.cliServer,
|
||||
queryDir,
|
||||
language,
|
||||
modelConfig,
|
||||
this.modelConfig,
|
||||
);
|
||||
if (!success) {
|
||||
await cleanupQueryDir();
|
||||
@@ -225,7 +231,7 @@ export class ModelEditorModule extends DisposableObject {
|
||||
this.modelingStore,
|
||||
this.modelingEvents,
|
||||
this.editorViewTracker,
|
||||
modelConfig,
|
||||
this.modelConfig,
|
||||
this.databaseManager,
|
||||
this.cliServer,
|
||||
this.queryRunner,
|
||||
@@ -234,6 +240,7 @@ export class ModelEditorModule extends DisposableObject {
|
||||
db,
|
||||
modelFile,
|
||||
language,
|
||||
initialMode,
|
||||
);
|
||||
|
||||
this.modelingEvents.onDbClosed(async (dbUri) => {
|
||||
|
||||
@@ -10,7 +10,7 @@ import { redactableError } from "../common/errors";
|
||||
import { telemetryListener } from "../common/vscode/telemetry";
|
||||
import { join } from "path";
|
||||
import { Mode } from "./shared/mode";
|
||||
import { writeFile } from "fs-extra";
|
||||
import { outputFile, writeFile } from "fs-extra";
|
||||
import { QueryLanguage } from "../common/query-language";
|
||||
import { fetchExternalApiQueries } from "./queries";
|
||||
import { Method } from "./method";
|
||||
@@ -57,7 +57,7 @@ export async function prepareModelEditorQueries(
|
||||
if (query.dependencies) {
|
||||
for (const [filename, contents] of Object.entries(query.dependencies)) {
|
||||
const dependencyFile = join(queryDir, filename);
|
||||
await writeFile(dependencyFile, contents, "utf8");
|
||||
await outputFile(dependencyFile, contents, "utf8");
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -38,7 +38,7 @@ import { Method } from "./method";
|
||||
import { ModeledMethod } from "./modeled-method";
|
||||
import { ExtensionPack } from "./shared/extension-pack";
|
||||
import { ModelConfigListener } from "../config";
|
||||
import { INITIAL_MODE, Mode } from "./shared/mode";
|
||||
import { Mode } from "./shared/mode";
|
||||
import { loadModeledMethods, saveModeledMethods } from "./modeled-method-fs";
|
||||
import { pickExtensionPack } from "./extension-pack-picker";
|
||||
import {
|
||||
@@ -50,12 +50,14 @@ import { telemetryListener } from "../common/vscode/telemetry";
|
||||
import { ModelingStore } from "./modeling-store";
|
||||
import { ModelEditorViewTracker } from "./model-editor-view-tracker";
|
||||
import { ModelingEvents } from "./modeling-events";
|
||||
import { getModelsAsDataLanguage, ModelsAsDataLanguage } from "./languages";
|
||||
|
||||
export class ModelEditorView extends AbstractWebview<
|
||||
ToModelEditorMessage,
|
||||
FromModelEditorMessage
|
||||
> {
|
||||
private readonly autoModeler: AutoModeler;
|
||||
private readonly languageDefinition: ModelsAsDataLanguage;
|
||||
|
||||
public constructor(
|
||||
protected readonly app: App,
|
||||
@@ -72,7 +74,7 @@ export class ModelEditorView extends AbstractWebview<
|
||||
private readonly extensionPack: ExtensionPack,
|
||||
// The language is equal to databaseItem.language but is properly typed as QueryLanguage
|
||||
private readonly language: QueryLanguage,
|
||||
initialMode: Mode = INITIAL_MODE,
|
||||
initialMode: Mode,
|
||||
) {
|
||||
super(app);
|
||||
|
||||
@@ -95,6 +97,7 @@ export class ModelEditorView extends AbstractWebview<
|
||||
this.addModeledMethods(modeledMethods);
|
||||
},
|
||||
);
|
||||
this.languageDefinition = getModelsAsDataLanguage(language);
|
||||
}
|
||||
|
||||
public async openView() {
|
||||
@@ -376,6 +379,10 @@ export class ModelEditorView extends AbstractWebview<
|
||||
const sourceArchiveAvailable =
|
||||
this.databaseItem.hasSourceArchiveInExplorer();
|
||||
|
||||
const showModeSwitchButton =
|
||||
this.languageDefinition.availableModes === undefined ||
|
||||
this.languageDefinition.availableModes.length > 1;
|
||||
|
||||
await this.postMessage({
|
||||
t: "setModelEditorViewState",
|
||||
viewState: {
|
||||
@@ -385,6 +392,7 @@ export class ModelEditorView extends AbstractWebview<
|
||||
showLlmButton,
|
||||
showMultipleModels: this.modelConfig.showMultipleModels,
|
||||
mode: this.modelingStore.getMode(this.databaseItem),
|
||||
showModeSwitchButton,
|
||||
sourceArchiveAvailable,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Method, Usage } from "./method";
|
||||
import { ModeledMethod } from "./modeled-method";
|
||||
import { ModelingEvents } from "./modeling-events";
|
||||
import { INITIAL_HIDE_MODELED_METHODS_VALUE } from "./shared/hide-modeled-methods";
|
||||
import { INITIAL_MODE, Mode } from "./shared/mode";
|
||||
import { Mode } from "./shared/mode";
|
||||
|
||||
interface InternalDbModelingState {
|
||||
databaseItem: DatabaseItem;
|
||||
@@ -50,10 +50,7 @@ export class ModelingStore extends DisposableObject {
|
||||
this.state = new Map<string, InternalDbModelingState>();
|
||||
}
|
||||
|
||||
public initializeStateForDb(
|
||||
databaseItem: DatabaseItem,
|
||||
mode: Mode = INITIAL_MODE,
|
||||
) {
|
||||
public initializeStateForDb(databaseItem: DatabaseItem, mode: Mode) {
|
||||
const dbUri = databaseItem.databaseUri.toString();
|
||||
this.state.set(dbUri, {
|
||||
databaseItem,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { fetchExternalApisQuery as csharpFetchExternalApisQuery } from "./csharp";
|
||||
import { fetchExternalApisQuery as javaFetchExternalApisQuery } from "./java";
|
||||
import { fetchExternalApisQuery as rubyFetchExternalApisQuery } from "./ruby";
|
||||
import { Query } from "./query";
|
||||
import { QueryLanguage } from "../../common/query-language";
|
||||
|
||||
export const fetchExternalApiQueries: Partial<Record<QueryLanguage, Query>> = {
|
||||
[QueryLanguage.CSharp]: csharpFetchExternalApisQuery,
|
||||
[QueryLanguage.Java]: javaFetchExternalApisQuery,
|
||||
[QueryLanguage.Ruby]: rubyFetchExternalApisQuery,
|
||||
};
|
||||
|
||||
404
extensions/ql-vscode/src/model-editor/queries/ruby.ts
Normal file
404
extensions/ql-vscode/src/model-editor/queries/ruby.ts
Normal file
@@ -0,0 +1,404 @@
|
||||
import { Query } from "./query";
|
||||
|
||||
export const fetchExternalApisQuery: Query = {
|
||||
applicationModeQuery: `/**
|
||||
* @name Fetch endpoints for use in the model editor (application mode)
|
||||
* @description A list of 3rd party endpoints (methods and attributes) used in the codebase. Excludes test and generated code.
|
||||
* @kind table
|
||||
* @id rb/utils/modeleditor/application-mode-endpoints
|
||||
* @tags modeleditor endpoints application-mode
|
||||
*/
|
||||
|
||||
import ruby
|
||||
|
||||
select "todo", "todo", "todo", "todo", "todo", false, "todo", "todo", "todo", "todo"
|
||||
`,
|
||||
frameworkModeQuery: `/**
|
||||
* @name Fetch endpoints for use in the model editor (framework mode)
|
||||
* @description A list of endpoints accessible (methods and attributes) for consumers of the library. Excludes test and generated code.
|
||||
* @kind table
|
||||
* @id rb/utils/modeleditor/framework-mode-endpoints
|
||||
* @tags modeleditor endpoints framework-mode
|
||||
*/
|
||||
|
||||
import ruby
|
||||
import FrameworkModeEndpointsQuery
|
||||
import ModelEditor
|
||||
|
||||
from PublicEndpointFromSource endpoint, boolean supported, string type
|
||||
where
|
||||
supported = isSupported(endpoint) and
|
||||
type = supportedType(endpoint)
|
||||
select endpoint, endpoint.getNamespace(), endpoint.getTypeName(), endpoint.getName(),
|
||||
endpoint.getParameterTypes(), supported, endpoint.getFile().getBaseName(), type
|
||||
`,
|
||||
dependencies: {
|
||||
"FrameworkModeEndpointsQuery.qll": `private import ruby
|
||||
private import ModelEditor
|
||||
private import modeling.internal.Util as Util
|
||||
|
||||
/**
|
||||
* A class of effectively public callables from source code.
|
||||
*/
|
||||
class PublicEndpointFromSource extends Endpoint {
|
||||
PublicEndpointFromSource() {
|
||||
this.getFile() instanceof Util::RelevantFile
|
||||
}
|
||||
|
||||
override predicate isSource() { this instanceof SourceCallable }
|
||||
|
||||
override predicate isSink() { this instanceof SinkCallable }
|
||||
}
|
||||
`,
|
||||
"ModelEditor.qll": `/** Provides classes and predicates related to handling APIs for the VS Code extension. */
|
||||
|
||||
private import ruby
|
||||
private import codeql.ruby.dataflow.FlowSummary
|
||||
private import codeql.ruby.dataflow.internal.DataFlowPrivate
|
||||
private import codeql.ruby.dataflow.internal.FlowSummaryImpl as FlowSummaryImpl
|
||||
private import codeql.ruby.dataflow.internal.FlowSummaryImplSpecific
|
||||
private import modeling.internal.Util as Util
|
||||
private import modeling.internal.Types
|
||||
private import codeql.ruby.frameworks.core.Gem
|
||||
|
||||
/** Holds if the given callable is not worth supporting. */
|
||||
private predicate isUninteresting(DataFlow::MethodNode c) {
|
||||
c.getLocation().getFile().getRelativePath().regexpMatch(".*(test|spec).*")
|
||||
}
|
||||
|
||||
/**
|
||||
* A callable method or accessor from either the Ruby Standard Library, a 3rd party library, or from the source.
|
||||
*/
|
||||
class Endpoint extends DataFlow::MethodNode {
|
||||
Endpoint() {
|
||||
this.isPublic() and not isUninteresting(this)
|
||||
}
|
||||
|
||||
File getFile() { result = this.getLocation().getFile() }
|
||||
|
||||
string getName() { result = this.getMethodName() }
|
||||
|
||||
/**
|
||||
* Gets the namespace of this endpoint.
|
||||
*/
|
||||
bindingset[this]
|
||||
string getNamespace() {
|
||||
// Return the name of any gemspec file in the database.
|
||||
// TODO: make this work for projects with multiple gems (and hence multiple gemspec files)
|
||||
result = any(Gem::GemSpec g).getName()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the unbound type name of this endpoint.
|
||||
*/
|
||||
bindingset[this]
|
||||
string getTypeName() {
|
||||
// result = nestedName(this.getDeclaringType().getUnboundDeclaration())
|
||||
// result = any(DataFlow::ClassNode c | Types::methodReturnsType(this, c) | c).getQualifiedName()
|
||||
result = Util::getAnAccessPathPrefixWithoutSuffix(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the parameter types of this endpoint.
|
||||
*/
|
||||
bindingset[this]
|
||||
string getParameterTypes() {
|
||||
// For now, return the names of postional parameters. We don't always have type information, so we can't return type names.
|
||||
// We don't yet handle keyword params, splat params or block params.
|
||||
// result = "(" + parameterQualifiedTypeNamesToString(this) + ")"
|
||||
result =
|
||||
"(" +
|
||||
concat(DataFlow::ParameterNode p, int i |
|
||||
p = this.asCallable().getParameter(i)
|
||||
|
|
||||
p.getName(), "," order by i
|
||||
) + ")"
|
||||
}
|
||||
|
||||
/** Holds if this API has a supported summary. */
|
||||
pragma[nomagic]
|
||||
predicate hasSummary() {
|
||||
// this instanceof SummarizedCallable
|
||||
none()
|
||||
}
|
||||
|
||||
/** Holds if this API is a known source. */
|
||||
pragma[nomagic]
|
||||
abstract predicate isSource();
|
||||
|
||||
/** Holds if this API is a known sink. */
|
||||
pragma[nomagic]
|
||||
abstract predicate isSink();
|
||||
|
||||
/** Holds if this API is a known neutral. */
|
||||
pragma[nomagic]
|
||||
predicate isNeutral() {
|
||||
// this instanceof FlowSummaryImpl::Public::NeutralCallable
|
||||
none()
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if this API is supported by existing CodeQL libraries, that is, it is either a
|
||||
* recognized source, sink or neutral or it has a flow summary.
|
||||
*/
|
||||
predicate isSupported() {
|
||||
this.hasSummary() or this.isSource() or this.isSink() or this.isNeutral()
|
||||
}
|
||||
}
|
||||
|
||||
boolean isSupported(Endpoint endpoint) {
|
||||
if endpoint.isSupported() then result = true else result = false
|
||||
}
|
||||
|
||||
string supportedType(Endpoint endpoint) {
|
||||
endpoint.isSink() and result = "sink"
|
||||
or
|
||||
endpoint.isSource() and result = "source"
|
||||
or
|
||||
endpoint.hasSummary() and result = "summary"
|
||||
or
|
||||
endpoint.isNeutral() and result = "neutral"
|
||||
or
|
||||
not endpoint.isSupported() and result = ""
|
||||
}
|
||||
|
||||
string methodClassification(Call method) {
|
||||
result = "source"
|
||||
}
|
||||
|
||||
/**
|
||||
* A callable where there exists a MaD sink model that applies to it.
|
||||
*/
|
||||
class SinkCallable extends DataFlow::CallableNode {
|
||||
SinkCallable() { sinkElement(this.asExpr().getExpr(), _, _, _) }
|
||||
}
|
||||
|
||||
/**
|
||||
* A callable where there exists a MaD source model that applies to it.
|
||||
*/
|
||||
class SourceCallable extends DataFlow::CallableNode {
|
||||
SourceCallable() { sourceElement(this.asExpr().getExpr(), _, _, _) }
|
||||
}`,
|
||||
"modeling/internal/Util.qll": `private import ruby
|
||||
|
||||
// \`SomeClass#initialize\` methods are usually called indirectly via
|
||||
// \`SomeClass.new\`, so we need to account for this when generating access paths
|
||||
private string getNormalizedMethodName(DataFlow::MethodNode methodNode) {
|
||||
exists(string actualMethodName | actualMethodName = methodNode.getMethodName() |
|
||||
if actualMethodName = "initialize" then result = "new" else result = actualMethodName
|
||||
)
|
||||
}
|
||||
|
||||
private string getAccessPathSuffix(Ast::MethodBase method) {
|
||||
if method instanceof Ast::SingletonMethod or method.getName() = "initialize"
|
||||
then result = "!"
|
||||
else result = ""
|
||||
}
|
||||
|
||||
string getAnAccessPathPrefix(DataFlow::MethodNode methodNode) {
|
||||
result =
|
||||
getAnAccessPathPrefixWithoutSuffix(methodNode) +
|
||||
getAccessPathSuffix(methodNode.asExpr().getExpr())
|
||||
}
|
||||
|
||||
string getAnAccessPathPrefixWithoutSuffix(DataFlow::MethodNode methodNode) {
|
||||
result =
|
||||
methodNode
|
||||
.asExpr()
|
||||
.getExpr()
|
||||
.getEnclosingModule()
|
||||
.(Ast::ConstantWriteAccess)
|
||||
.getAQualifiedName()
|
||||
}
|
||||
|
||||
class RelevantFile extends File {
|
||||
RelevantFile() { not this.getRelativePath().regexpMatch(".*/?test(case)?s?/.*") }
|
||||
}
|
||||
|
||||
string getMethodPath(DataFlow::MethodNode methodNode) {
|
||||
result = "Method[" + getNormalizedMethodName(methodNode) + "]"
|
||||
}
|
||||
|
||||
private string getParameterPath(DataFlow::ParameterNode paramNode) {
|
||||
exists(Ast::Parameter param, string paramSpec |
|
||||
param = paramNode.asParameter() and
|
||||
(
|
||||
paramSpec = param.getPosition().toString()
|
||||
or
|
||||
paramSpec = param.(Ast::KeywordParameter).getName() + ":"
|
||||
or
|
||||
param instanceof Ast::BlockParameter and
|
||||
paramSpec = "block"
|
||||
)
|
||||
|
|
||||
result = "Parameter[" + paramSpec + "]"
|
||||
)
|
||||
}
|
||||
|
||||
string getMethodParameterPath(DataFlow::MethodNode methodNode, DataFlow::ParameterNode paramNode) {
|
||||
result = getMethodPath(methodNode) + "." + getParameterPath(paramNode)
|
||||
}
|
||||
`,
|
||||
"modeling/internal/Types.qll": `private import ruby
|
||||
private import codeql.ruby.ApiGraphs
|
||||
private import Util as Util
|
||||
|
||||
module Types {
|
||||
private module Config implements DataFlow::ConfigSig {
|
||||
predicate isSource(DataFlow::Node source) {
|
||||
// TODO: construction of type values not using a "new" call
|
||||
source.(DataFlow::CallNode).getMethodName() = "new"
|
||||
}
|
||||
|
||||
predicate isSink(DataFlow::Node sink) { sink = any(DataFlow::MethodNode m).getAReturnNode() }
|
||||
}
|
||||
|
||||
private import DataFlow::Global<Config>
|
||||
|
||||
predicate methodReturnsType(DataFlow::MethodNode methodNode, DataFlow::ClassNode classNode) {
|
||||
// ignore cases of initializing instance of self
|
||||
not methodNode.getMethodName() = "initialize" and
|
||||
exists(DataFlow::CallNode initCall |
|
||||
flow(initCall, methodNode.getAReturnNode()) and
|
||||
classNode.getAnImmediateReference().getAMethodCall() = initCall and
|
||||
// constructed object does not have a type declared in test code
|
||||
/*
|
||||
* TODO: this may be too restrictive, e.g.
|
||||
* - if a type is declared in both production and test code
|
||||
* - if a built-in type is extended in test code
|
||||
*/
|
||||
|
||||
forall(Ast::ModuleBase classDecl | classDecl = classNode.getADeclaration() |
|
||||
classDecl.getLocation().getFile() instanceof Util::RelevantFile
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// \`exprNode\` is an instance of \`classNode\`
|
||||
private predicate exprHasType(DataFlow::ExprNode exprNode, DataFlow::ClassNode classNode) {
|
||||
exists(DataFlow::MethodNode methodNode, DataFlow::CallNode callNode |
|
||||
methodReturnsType(methodNode, classNode) and
|
||||
callNode.getATarget() = methodNode
|
||||
|
|
||||
exprNode.getALocalSource() = callNode
|
||||
)
|
||||
or
|
||||
exists(DataFlow::MethodNode containingMethod |
|
||||
classNode.getInstanceMethod(containingMethod.getMethodName()) = containingMethod
|
||||
|
|
||||
exprNode.getALocalSource() = containingMethod.getSelfParameter()
|
||||
)
|
||||
}
|
||||
|
||||
// extensible predicate typeModel(string type1, string type2, string path);
|
||||
// the method node in type2 constructs an instance of classNode
|
||||
private predicate typeModelReturns(string type1, string type2, string path) {
|
||||
exists(DataFlow::MethodNode methodNode, DataFlow::ClassNode classNode |
|
||||
methodNode.getLocation().getFile() instanceof Util::RelevantFile and
|
||||
methodReturnsType(methodNode, classNode)
|
||||
|
|
||||
type1 = classNode.getQualifiedName() and
|
||||
type2 = Util::getAnAccessPathPrefix(methodNode) and
|
||||
path = Util::getMethodPath(methodNode) + ".ReturnValue"
|
||||
)
|
||||
}
|
||||
|
||||
predicate methodTakesParameterOfType(
|
||||
DataFlow::MethodNode methodNode, DataFlow::ClassNode classNode,
|
||||
DataFlow::ParameterNode parameterNode
|
||||
) {
|
||||
exists(DataFlow::CallNode callToMethodNode, DataFlow::LocalSourceNode argumentNode |
|
||||
callToMethodNode.getATarget() = methodNode and
|
||||
// positional parameter
|
||||
exists(int paramIndex |
|
||||
argumentNode.flowsTo(callToMethodNode.getArgument(paramIndex)) and
|
||||
parameterNode = methodNode.getParameter(paramIndex)
|
||||
)
|
||||
or
|
||||
// keyword parameter
|
||||
exists(string kwName |
|
||||
argumentNode.flowsTo(callToMethodNode.getKeywordArgument(kwName)) and
|
||||
parameterNode = methodNode.getKeywordParameter(kwName)
|
||||
)
|
||||
or
|
||||
// block parameter
|
||||
argumentNode.flowsTo(callToMethodNode.getBlock()) and
|
||||
parameterNode = methodNode.getBlockParameter()
|
||||
|
|
||||
// parameter directly from new call
|
||||
argumentNode.(DataFlow::CallNode).getMethodName() = "new" and
|
||||
classNode.getAnImmediateReference().getAMethodCall() = argumentNode
|
||||
or
|
||||
// parameter from indirect new call
|
||||
exists(DataFlow::ExprNode argExpr |
|
||||
exprHasType(argExpr, classNode) and
|
||||
argumentNode.(DataFlow::CallNode).getATarget() = argExpr
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private predicate typeModelParameters(string type1, string type2, string path) {
|
||||
exists(
|
||||
DataFlow::MethodNode methodNode, DataFlow::ClassNode classNode,
|
||||
DataFlow::ParameterNode parameterNode
|
||||
|
|
||||
methodNode.getLocation().getFile() instanceof Util::RelevantFile and
|
||||
methodTakesParameterOfType(methodNode, classNode, parameterNode)
|
||||
|
|
||||
type1 = classNode.getQualifiedName() and
|
||||
type2 = Util::getAnAccessPathPrefix(methodNode) and
|
||||
path = Util::getMethodParameterPath(methodNode, parameterNode)
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: non-positional params for block arg parameters
|
||||
private predicate methodYieldsType(
|
||||
DataFlow::CallableNode callableNode, int argIdx, DataFlow::ClassNode classNode
|
||||
) {
|
||||
exprHasType(callableNode.getABlockCall().getArgument(argIdx), classNode)
|
||||
}
|
||||
|
||||
/*
|
||||
* e.g. for
|
||||
* \`\`\`rb
|
||||
* class Foo
|
||||
* def initialize
|
||||
* // do some stuff...
|
||||
* if block_given?
|
||||
* yield self
|
||||
* end
|
||||
* end
|
||||
*
|
||||
* def do_something
|
||||
* // do something else
|
||||
* end
|
||||
* end
|
||||
*
|
||||
* Foo.new do |foo| foo.do_something end
|
||||
* \`\`\`
|
||||
*
|
||||
* the parameter foo to the block is an instance of Foo.
|
||||
*/
|
||||
|
||||
private predicate typeModelBlockArgumentParameters(string type1, string type2, string path) {
|
||||
exists(DataFlow::MethodNode methodNode, DataFlow::ClassNode classNode, int argIdx |
|
||||
methodNode.getLocation().getFile() instanceof Util::RelevantFile and
|
||||
methodYieldsType(methodNode, argIdx, classNode)
|
||||
|
|
||||
type1 = classNode.getQualifiedName() and
|
||||
type2 = Util::getAnAccessPathPrefix(methodNode) and
|
||||
path = Util::getMethodPath(methodNode) + ".Argument[block].Parameter[" + argIdx + "]"
|
||||
)
|
||||
}
|
||||
|
||||
predicate typeModel(string type1, string type2, string path) {
|
||||
typeModelReturns(type1, type2, path)
|
||||
or
|
||||
typeModelParameters(type1, type2, path)
|
||||
or
|
||||
typeModelBlockArgumentParameters(type1, type2, path)
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
};
|
||||
@@ -9,6 +9,7 @@ export interface ModelEditorViewState {
|
||||
showLlmButton: boolean;
|
||||
showMultipleModels: boolean;
|
||||
mode: Mode;
|
||||
showModeSwitchButton: boolean;
|
||||
sourceArchiveAvailable: boolean;
|
||||
}
|
||||
|
||||
|
||||
27
extensions/ql-vscode/src/model-editor/supported-languages.ts
Normal file
27
extensions/ql-vscode/src/model-editor/supported-languages.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { QueryLanguage } from "../common/query-language";
|
||||
import { ModelConfig } from "../config";
|
||||
|
||||
/**
|
||||
* Languages that are always supported by the model editor. These languages
|
||||
* do not require a separate config setting to enable them.
|
||||
*/
|
||||
const SUPPORTED_LANGUAGES: QueryLanguage[] = [
|
||||
QueryLanguage.Java,
|
||||
QueryLanguage.CSharp,
|
||||
];
|
||||
|
||||
export function isSupportedLanguage(
|
||||
language: QueryLanguage,
|
||||
modelConfig: ModelConfig,
|
||||
) {
|
||||
if (SUPPORTED_LANGUAGES.includes(language)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (language === QueryLanguage.Ruby) {
|
||||
// Ruby is only enabled when the config setting is set
|
||||
return modelConfig.enableRuby;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -291,12 +291,14 @@ export function ModelEditor({
|
||||
<span slot="start" className="codicon codicon-package"></span>
|
||||
Open extension pack
|
||||
</LinkIconButton>
|
||||
<LinkIconButton onClick={onSwitchModeClick}>
|
||||
<span slot="start" className="codicon codicon-library"></span>
|
||||
{viewState.mode === Mode.Framework
|
||||
? "Model as application"
|
||||
: "Model as dependency"}
|
||||
</LinkIconButton>
|
||||
{viewState.showModeSwitchButton && (
|
||||
<LinkIconButton onClick={onSwitchModeClick}>
|
||||
<span slot="start" className="codicon codicon-library"></span>
|
||||
{viewState.mode === Mode.Framework
|
||||
? "Model as application"
|
||||
: "Model as dependency"}
|
||||
</LinkIconButton>
|
||||
)}
|
||||
</HeaderRow>
|
||||
</HeaderColumn>
|
||||
<HeaderSpacer />
|
||||
|
||||
@@ -12,6 +12,7 @@ export function createMockModelEditorViewState(
|
||||
showFlowGeneration: false,
|
||||
showLlmButton: false,
|
||||
showMultipleModels: false,
|
||||
showModeSwitchButton: true,
|
||||
extensionPack: createMockExtensionPack(),
|
||||
sourceArchiveAvailable: true,
|
||||
...data,
|
||||
|
||||
@@ -40,15 +40,12 @@ describe("setUpPack", () => {
|
||||
await setUpPack(cliServer, queryDir, language, modelConfig);
|
||||
|
||||
const queryFiles = await readdir(queryDir);
|
||||
expect(queryFiles.sort()).toEqual(
|
||||
[
|
||||
expect(queryFiles).toEqual(
|
||||
expect.arrayContaining([
|
||||
"codeql-pack.yml",
|
||||
"ApplicationModeEndpoints.ql",
|
||||
"ApplicationModeEndpointsQuery.qll",
|
||||
"FrameworkModeEndpoints.ql",
|
||||
"FrameworkModeEndpointsQuery.qll",
|
||||
"ModelEditor.qll",
|
||||
].sort(),
|
||||
]),
|
||||
);
|
||||
|
||||
const suiteFileContents = await readFile(
|
||||
|
||||
Reference in New Issue
Block a user