This commit is contained in:
2026-04-07 15:16:19 +03:00
parent 953b39d433
commit 0fa388abeb
25 changed files with 1370 additions and 38 deletions
+149
View File
@@ -0,0 +1,149 @@
import { describe, it, expect } from "vitest";
import { inferMethodReturnType, projectedListType } from "./completions";
import type { DexprType } from "./completions";
// ==================== inferMethodReturnType ====================
describe("inferMethodReturnType", () => {
// String methods → String
it.each(["upper", "lower", "trim", "trimStart", "trimEnd", "replace", "charAt", "substring"])(
"%s → String",
(method) => {
expect(inferMethodReturnType(method)).toBe("String");
}
);
// String/List methods → Boolean
it.each(["contains", "startsWith", "endsWith", "isEmpty"])(
"%s → Boolean",
(method) => {
expect(inferMethodReturnType(method)).toBe("Boolean");
}
);
// Methods → Number
it.each(["length", "len", "indexOf", "sum", "avg", "min", "max", "first", "last"])(
"%s → Number",
(method) => {
expect(inferMethodReturnType(method)).toBe("Number");
}
);
// split → StringList
it("split → StringList", () => {
expect(inferMethodReturnType("split")).toBe("StringList");
});
// join → String
it("join → String", () => {
expect(inferMethodReturnType("join")).toBe("String");
});
// filter → List
it("filter → List", () => {
expect(inferMethodReturnType("filter")).toBe("List");
});
// Methods that depend on input type → null
it.each(["reverse", "sort", "slice", "map", "find"])(
"%s → null (context-dependent)",
(method) => {
expect(inferMethodReturnType(method)).toBeNull();
}
);
// Unknown method → null
it("unknown method → null", () => {
expect(inferMethodReturnType("foobar")).toBeNull();
});
});
// ==================== projectedListType ====================
describe("projectedListType", () => {
it("Number field → NumberList", () => {
expect(projectedListType("Number")).toBe("NumberList");
});
it("String field → StringList", () => {
expect(projectedListType("String")).toBe("StringList");
});
it("Boolean field → List", () => {
expect(projectedListType("Boolean")).toBe("List");
});
it("Object field → List", () => {
expect(projectedListType("Object")).toBe("List");
});
it("List field → List", () => {
expect(projectedListType("List")).toBe("List");
});
it("null (unknown) field → List", () => {
expect(projectedListType(null)).toBe("List");
});
it("NumberList field → List (nested lists stay as List)", () => {
expect(projectedListType("NumberList")).toBe("List");
});
it("StringList field → List", () => {
expect(projectedListType("StringList")).toBe("List");
});
});
// ==================== Type flow scenarios ====================
describe("type flow scenarios", () => {
// Simulates what happens in the autocomplete pipeline
it("kalemler.tutar.sum() — List → NumberList → Number", () => {
// Step 1: kalemler is List, tutar field is Number
const projectedType = projectedListType("Number");
expect(projectedType).toBe("NumberList");
// Step 2: .sum() on NumberList returns Number
const resultType = inferMethodReturnType("sum");
expect(resultType).toBe("Number");
});
it("kalemler.adi.join() — List → StringList → String", () => {
const projectedType = projectedListType("String");
expect(projectedType).toBe("StringList");
const resultType = inferMethodReturnType("join");
expect(resultType).toBe("String");
});
it("kalemler.filter().tutar.sum() — List → List → NumberList → Number", () => {
// filter returns List
const afterFilter = inferMethodReturnType("filter");
expect(afterFilter).toBe("List");
// .tutar on List with Number field
const afterProjection = projectedListType("Number");
expect(afterProjection).toBe("NumberList");
// .sum() on NumberList
const result = inferMethodReturnType("sum");
expect(result).toBe("Number");
});
it("kalemler.tutar.max() — projection then aggregate", () => {
const projected = projectedListType("Number");
expect(projected).toBe("NumberList");
const result = inferMethodReturnType("max");
expect(result).toBe("Number");
});
it("kalemler.birim.contains() — StringList method", () => {
const projected = projectedListType("String");
expect(projected).toBe("StringList");
const result = inferMethodReturnType("contains");
expect(result).toBe("Boolean");
});
});
+42 -13
View File
@@ -17,7 +17,8 @@ export type DexprType =
| "Boolean"
| "NumberList"
| "StringList"
| "Object";
| "Object"
| "List";
export interface FunctionInfo {
name: string;
@@ -171,15 +172,15 @@ function inferExprType(
if (objNode.name === "VariableName") {
const varName = doc.sliceString(objNode.from, objNode.to);
const fieldName = doc.sliceString(propNode.from, propNode.to);
// Look up from objectFieldTypes via the global lookup
// (We use knownTypes to check if root is Object, then check field)
const rootType = knownTypes.get(varName);
if (rootType === "Object") {
// Field type needs to come from config — stored as "varName.fieldName" key
// We can't access objectFieldTypes here, so use the convention
// that knownTypes may contain "varName.fieldName" entries
return knownTypes.get(`${varName}.${fieldName}`) ?? null;
}
if (rootType === "List") {
// Property projection: list.field → typed list based on field type
const fieldType = knownTypes.get(`${varName}.${fieldName}`) ?? null;
return projectedListType(fieldType);
}
}
return null;
}
@@ -204,7 +205,7 @@ function findChild(
}
/** Infer return type from known method names */
function inferMethodReturnType(method: string): DexprType | null {
export function inferMethodReturnType(method: string): DexprType | null {
switch (method) {
// String -> String
case "upper":
@@ -245,11 +246,29 @@ function inferMethodReturnType(method: string): DexprType | null {
return null; // depends on input type
case "join":
return "String";
// List methods
case "map":
return null; // depends on field type (NumberList, StringList, or List)
case "filter":
return "List";
case "find":
return null; // returns single element
default:
return null;
}
}
/**
* Given a field type from an Object element within a List,
* return the projected list type after property access.
* e.g. List with Number field "tutar" → kalemler.tutar → NumberList
*/
export function projectedListType(fieldType: DexprType | null): DexprType {
if (fieldType === "Number") return "NumberList";
if (fieldType === "String") return "StringList";
return "List";
}
// --- Autocomplete ---
function dedup(items: Completion[]): Completion[] {
@@ -290,7 +309,7 @@ export function dexprCompletion(info: DexprLanguageInfo): Extension {
// and field completions per Object variable
const objectFieldCompletions = new Map<string, Completion[]>();
for (const v of info.variables ?? []) {
if (v.type === "Object" && v.fields) {
if ((v.type === "Object" || v.type === "List") && v.fields) {
const fieldItems: Completion[] = [];
for (const f of v.fields) {
// Store "customer.name" → "String" in configVarTypes for type inference
@@ -354,13 +373,17 @@ export function dexprCompletion(info: DexprLanguageInfo): Extension {
// e.g. path=["customer","name"] → look up "customer.name" in varTypes
let currentType = rootType;
for (let i = 1; i < path.length; i++) {
if (currentType !== "Object") {
if (currentType === "Object") {
const key = `${path[i - 1]}.${path[i]}`;
currentType = varTypes.get(key) ?? null;
} else if (currentType === "List") {
// Property projection: list.field → typed list
const key = `${path[0]}.${path[i]}`;
const fieldType = varTypes.get(key) ?? null;
currentType = projectedListType(fieldType);
} else {
return { type: currentType, path };
}
// "customer.name" key convention
const key = `${path[i - 1]}.${path[i]}`;
const fieldType = varTypes.get(key) ?? null;
currentType = fieldType;
}
return { type: currentType, path };
@@ -408,6 +431,12 @@ export function dexprCompletion(info: DexprLanguageInfo): Extension {
const fieldItems = objectFieldCompletions.get(rootVarName) ?? [];
const objMethods = methodsByType["Object"] ?? [];
options = [...fieldItems, ...objMethods];
} else if (finalType === "List") {
// Show field names (property projection) + List methods
const rootVarName = path[0];
const fieldItems = objectFieldCompletions.get(rootVarName) ?? [];
const listMethods = methodsByType["List"] ?? [];
options = [...fieldItems, ...listMethods];
} else if (finalType) {
options = methodsByType[finalType] ?? allMethods;
} else {