Merge pull request #3017 from hmac/hmac-model-editor-ruby

Add experimental model editor support for Ruby
This commit is contained in:
Koen Vlaswinkel
2023-10-30 16:36:15 +01:00
committed by GitHub
15 changed files with 643 additions and 30 deletions

View File

@@ -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>();
}
}

View File

@@ -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(

View File

@@ -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;
};

View 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: "",
};
},
},
},
};

View File

@@ -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) => {

View File

@@ -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;

View File

@@ -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,
},
});

View File

@@ -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,

View File

@@ -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,
};

View 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)
}
}
`,
},
};

View File

@@ -9,6 +9,7 @@ export interface ModelEditorViewState {
showLlmButton: boolean;
showMultipleModels: boolean;
mode: Mode;
showModeSwitchButton: boolean;
sourceArchiveAvailable: boolean;
}

View 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;
}

View File

@@ -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 />

View File

@@ -12,6 +12,7 @@ export function createMockModelEditorViewState(
showFlowGeneration: false,
showLlmButton: false,
showMultipleModels: false,
showModeSwitchButton: true,
extensionPack: createMockExtensionPack(),
sourceArchiveAvailable: true,
...data,

View File

@@ -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(