mirror of
https://github.com/duhanbalci/dexpr.git
synced 2026-08-30 16:01:41 +00:00
0.3.0
This commit is contained in:
@@ -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
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user