Skip to content

Commit 9f7f34a

Browse files
authored
Merge pull request #3653 from github/koesie10/python-use-simplified-paths-and-endpoint-kinds
Use rich type columns and endpoint kinds for Python model editor
2 parents c9e1a64 + 6fd2579 commit 9f7f34a

File tree

7 files changed

+385
-116
lines changed

7 files changed

+385
-116
lines changed

extensions/ql-vscode/src/model-editor/bqrs.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export function decodeBqrsToMethods(
3333
let libraryVersion: string | undefined;
3434
let type: ModeledMethodType;
3535
let classification: CallClassification;
36+
let endpointKindColumn: string | BqrsEntityValue | undefined;
3637
let endpointType: EndpointType | undefined = undefined;
3738

3839
if (mode === Mode.Application) {
@@ -47,6 +48,7 @@ export function decodeBqrsToMethods(
4748
libraryVersion,
4849
type,
4950
classification,
51+
endpointKindColumn,
5052
] = tuple as ApplicationModeTuple;
5153
} else {
5254
[
@@ -58,6 +60,7 @@ export function decodeBqrsToMethods(
5860
supported,
5961
library,
6062
type,
63+
endpointKindColumn,
6164
] = tuple as FrameworkModeTuple;
6265

6366
classification = CallClassification.Unknown;
@@ -68,13 +71,18 @@ export function decodeBqrsToMethods(
6871
}
6972

7073
if (definition.endpointTypeForEndpoint) {
71-
endpointType = definition.endpointTypeForEndpoint({
72-
endpointType,
73-
packageName,
74-
typeName,
75-
methodName,
76-
methodParameters,
77-
});
74+
endpointType = definition.endpointTypeForEndpoint(
75+
{
76+
endpointType,
77+
packageName,
78+
typeName,
79+
methodName,
80+
methodParameters,
81+
},
82+
typeof endpointKindColumn === "object"
83+
? endpointKindColumn.label
84+
: endpointKindColumn,
85+
);
7886
}
7987

8088
if (endpointType === undefined) {

extensions/ql-vscode/src/model-editor/languages/models-as-data.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,11 +174,14 @@ export type ModelsAsDataLanguage = {
174174
* be determined by heuristics.
175175
* @param method The method to get the endpoint type for. The endpoint type can be undefined if the
176176
* query does not return an endpoint type.
177+
* @param endpointKind An optional column that may be provided by the query to help determine the
178+
* endpoint type.
177179
*/
178180
endpointTypeForEndpoint?: (
179181
method: Omit<MethodDefinition, "endpointType"> & {
180182
endpointType: EndpointType | undefined;
181183
},
184+
endpointKind: string | undefined,
182185
) => EndpointType | undefined;
183186
predicates: ModelsAsDataLanguagePredicates;
184187
modelGeneration?: ModelsAsDataLanguageModelGeneration;

extensions/ql-vscode/src/model-editor/languages/python/access-paths.ts

Lines changed: 109 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,26 @@ import { EndpointType } from "../../method";
44

55
const memberTokenRegex = /^Member\[(.+)]$/;
66

7-
export function parsePythonAccessPath(path: string): {
7+
// In Python, the type can contain both the package name and the type name.
8+
export function parsePythonType(type: string) {
9+
// The first part is always the package name. All remaining parts are the type
10+
// name.
11+
12+
const parts = type.split(".");
13+
const packageName = parts.shift() ?? "";
14+
15+
return {
16+
packageName,
17+
typeName: parts.join("."),
18+
};
19+
}
20+
21+
// The type name can also be specified in the type, so this will combine
22+
// the already parsed type name and the type name from the access path.
23+
export function parsePythonAccessPath(
24+
path: string,
25+
shortTypeName: string,
26+
): {
827
typeName: string;
928
methodName: string;
1029
endpointType: EndpointType;
@@ -13,8 +32,12 @@ export function parsePythonAccessPath(path: string): {
1332
const tokens = parseAccessPathTokens(path);
1433

1534
if (tokens.length === 0) {
35+
const typeName = shortTypeName.endsWith("!")
36+
? shortTypeName.slice(0, -1)
37+
: shortTypeName;
38+
1639
return {
17-
typeName: "",
40+
typeName,
1841
methodName: "",
1942
endpointType: EndpointType.Method,
2043
path: "",
@@ -23,6 +46,10 @@ export function parsePythonAccessPath(path: string): {
2346

2447
const typeParts = [];
2548
let endpointType = EndpointType.Function;
49+
// If a short type name was given and it doesn't end in a `!`, then this refers to a method.
50+
if (shortTypeName !== "" && !shortTypeName.endsWith("!")) {
51+
endpointType = EndpointType.Method;
52+
}
2653

2754
let remainingTokens: typeof tokens = [];
2855

@@ -32,6 +59,7 @@ export function parsePythonAccessPath(path: string): {
3259
if (memberMatch) {
3360
typeParts.push(memberMatch[1]);
3461
} else if (token.text === "Instance") {
62+
// Alternative way of specifying that this refers to a method.
3563
endpointType = EndpointType.Method;
3664
} else {
3765
remainingTokens = tokens.slice(i);
@@ -40,9 +68,22 @@ export function parsePythonAccessPath(path: string): {
4068
}
4169

4270
const methodName = typeParts.pop() ?? "";
43-
const typeName = typeParts.join(".");
71+
let typeName = typeParts.join(".");
4472
const remainingPath = remainingTokens.map((token) => token.text).join(".");
4573

74+
if (shortTypeName !== "") {
75+
if (shortTypeName.endsWith("!")) {
76+
// The actual type name is the name without the `!`.
77+
shortTypeName = shortTypeName.slice(0, -1);
78+
}
79+
80+
if (typeName !== "") {
81+
typeName = `${shortTypeName}.${typeName}`;
82+
} else {
83+
typeName = shortTypeName;
84+
}
85+
}
86+
4687
return {
4788
methodName,
4889
typeName,
@@ -51,53 +92,59 @@ export function parsePythonAccessPath(path: string): {
5192
};
5293
}
5394

54-
export function pythonMethodSignature(typeName: string, methodName: string) {
55-
return `${typeName}#${methodName}`;
56-
}
95+
export function parsePythonTypeAndPath(
96+
type: string,
97+
path: string,
98+
): {
99+
packageName: string;
100+
typeName: string;
101+
methodName: string;
102+
endpointType: EndpointType;
103+
path: string;
104+
} {
105+
const { packageName, typeName: shortTypeName } = parsePythonType(type);
106+
const {
107+
typeName,
108+
methodName,
109+
endpointType,
110+
path: remainingPath,
111+
} = parsePythonAccessPath(path, shortTypeName);
57112

58-
function pythonTypePath(typeName: string) {
59-
if (typeName === "") {
60-
return "";
61-
}
113+
return {
114+
packageName,
115+
typeName,
116+
methodName,
117+
endpointType,
118+
path: remainingPath,
119+
};
120+
}
62121

63-
return typeName
64-
.split(".")
65-
.map((part) => `Member[${part}]`)
66-
.join(".");
122+
export function pythonMethodSignature(typeName: string, methodName: string) {
123+
return `${typeName}#${methodName}`;
67124
}
68125

69-
export function pythonMethodPath(
126+
export function pythonType(
127+
packageName: string,
70128
typeName: string,
71-
methodName: string,
72129
endpointType: EndpointType,
73130
) {
74-
if (methodName === "") {
75-
return pythonTypePath(typeName);
131+
if (typeName !== "" && packageName !== "") {
132+
return `${packageName}.${typeName}${endpointType === EndpointType.Function ? "!" : ""}`;
76133
}
77134

78-
const typePath = pythonTypePath(typeName);
79-
80-
let result = typePath;
81-
if (typePath !== "" && endpointType === EndpointType.Method) {
82-
result += ".Instance";
83-
}
135+
return `${packageName}${typeName}`;
136+
}
84137

85-
if (result !== "") {
86-
result += ".";
138+
export function pythonMethodPath(methodName: string) {
139+
if (methodName === "") {
140+
return "";
87141
}
88142

89-
result += `Member[${methodName}]`;
90-
91-
return result;
143+
return `Member[${methodName}]`;
92144
}
93145

94-
export function pythonPath(
95-
typeName: string,
96-
methodName: string,
97-
endpointType: EndpointType,
98-
path: string,
99-
) {
100-
const methodPath = pythonMethodPath(typeName, methodName, endpointType);
146+
export function pythonPath(methodName: string, path: string) {
147+
const methodPath = pythonMethodPath(methodName);
101148
if (methodPath === "") {
102149
return path;
103150
}
@@ -111,7 +158,24 @@ export function pythonPath(
111158

112159
export function pythonEndpointType(
113160
method: Omit<MethodDefinition, "endpointType">,
161+
endpointKind: string | undefined,
114162
): EndpointType {
163+
switch (endpointKind) {
164+
case "Function":
165+
return EndpointType.Function;
166+
case "InstanceMethod":
167+
return EndpointType.Method;
168+
case "ClassMethod":
169+
return EndpointType.ClassMethod;
170+
case "StaticMethod":
171+
return EndpointType.StaticMethod;
172+
case "InitMethod":
173+
return EndpointType.Constructor;
174+
case "Class":
175+
return EndpointType.Class;
176+
}
177+
178+
// Legacy behavior for when the kind column is missing.
115179
if (
116180
method.methodParameters.startsWith("(self,") ||
117181
method.methodParameters === "(self)"
@@ -120,3 +184,12 @@ export function pythonEndpointType(
120184
}
121185
return EndpointType.Function;
122186
}
187+
188+
export function hasPythonSelfArgument(endpointType: EndpointType): boolean {
189+
// Instance methods and class methods both use `Argument[self]` for the first parameter. The first
190+
// parameter after self is called `Argument[0]`.
191+
return (
192+
endpointType === EndpointType.Method ||
193+
endpointType === EndpointType.ClassMethod
194+
);
195+
}

0 commit comments

Comments
 (0)