Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 73 additions & 41 deletions src/extraction/getTypeVariant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,53 +10,85 @@ export enum DataTypeVariant {
NODE
}

export interface TypeVariantResult {
identifier: string;
variant: DataTypeVariant;
}

/**
* Determines the variant of a given TypeScript type string using the TS compiler.
* Determines the variant of a given TypeScript types string or DataType(s) using the TS compiler.
* - If types is a string: returns one result with the string as identifier
* - If types is a DataType: returns one result with DataType.identifier
* - If types is a DataType[]: returns results for each DataType with their identifiers
*/
export const getTypeVariant = (
type?: string,
types?: string | DataType | DataType[],
dataTypes?: DataType[]
): DataTypeVariant => {
): TypeVariantResult[] => {
const typeDefs = getSharedTypeDeclarations(dataTypes);

// We declare a variable with the type to probe it
const sourceCode = `
${typeDefs}
type TargetType = ${type};
const val: TargetType = {} as any;
`;

const fileName = `index.ts`;
const host = createCompilerHost(fileName, sourceCode);
const sourceFile = host.getSourceFile(fileName)!;
const program = host.languageService.getProgram()!;
const checker = program.getTypeChecker();

let discoveredVariant: DataTypeVariant = DataTypeVariant.TYPE;

const visit = (node: ts.Node) => {
if (ts.isVariableDeclaration(node) && node.name.getText() === "val") {
const type = checker.getTypeAtLocation(node);

if (type.getCallSignatures().length > 0) {
discoveredVariant = DataTypeVariant.NODE;
} else if (checker.isArrayType(type)) {
discoveredVariant = DataTypeVariant.ARRAY;
} else if (
type.isStringLiteral() ||
type.isNumberLiteral() ||
(type.getFlags() & (ts.TypeFlags.String | ts.TypeFlags.Number | ts.TypeFlags.Boolean | ts.TypeFlags.EnumLiteral | ts.TypeFlags.BigInt | ts.TypeFlags.ESSymbol)) !== 0
) {
discoveredVariant = DataTypeVariant.PRIMITIVE;
} else if (type.isClassOrInterface() || (type.getFlags() & ts.TypeFlags.Object) !== 0) {
discoveredVariant = DataTypeVariant.OBJECT;
} else {
discoveredVariant = DataTypeVariant.TYPE;
// Determine what we're analyzing
const isStringType = typeof types === "string";
const isArrayType = Array.isArray(types);
const typesToAnalyze = isArrayType ? (types as DataType[]) : isStringType ? [{ identifier: types, type: types }] : [types];
const identifiers = isArrayType
? (types as DataType[]).map(t => t.identifier)
: isStringType
? [types as string]
: [(types as DataType).identifier];

const results: TypeVariantResult[] = [];

for (let i = 0; i < typesToAnalyze.length; i++) {
const currentType = typesToAnalyze[i];
const currentIdentifier = identifiers[i];
const typeString = isStringType ? (types as string) : (currentType as DataType).type;

// We declare a variable with the types to probe it
const sourceCode = `
${typeDefs}
${typeString ? `type TargetType = ${typeString};` : ""}
const val: TargetType = {} as any;
`;

const fileName = `index.ts`;
const host = createCompilerHost(fileName, sourceCode);
const sourceFile = host.getSourceFile(fileName)!;
const program = host.languageService.getProgram()!;
const checker = program.getTypeChecker();

let discoveredVariant: DataTypeVariant = DataTypeVariant.TYPE;

const visit = (node: ts.Node) => {
if (ts.isVariableDeclaration(node) && node.name.getText() === "val") {
const type = checker.getTypeAtLocation(node);

if (type.getCallSignatures().length > 0) {
discoveredVariant = DataTypeVariant.NODE;
} else if (checker.isArrayType(type)) {
discoveredVariant = DataTypeVariant.ARRAY;
} else if (
type.isStringLiteral() ||
type.isNumberLiteral() ||
(type.getFlags() & (ts.TypeFlags.String | ts.TypeFlags.Number | ts.TypeFlags.Boolean | ts.TypeFlags.EnumLiteral | ts.TypeFlags.BigInt | ts.TypeFlags.ESSymbol)) !== 0
) {
discoveredVariant = DataTypeVariant.PRIMITIVE;
} else if (type.isClassOrInterface() || (type.getFlags() & ts.TypeFlags.Object) !== 0) {
discoveredVariant = DataTypeVariant.OBJECT;
} else {
discoveredVariant = DataTypeVariant.TYPE;
}
}
}
ts.forEachChild(node, visit);
};
ts.forEachChild(node, visit);
};

visit(sourceFile);

results.push({
identifier: currentIdentifier!,
variant: discoveredVariant
});
}

visit(sourceFile);
return discoveredVariant;
return results;
};
2 changes: 1 addition & 1 deletion src/suggestion/getNodeSuggestions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const getNodeSuggestions = (

let functionToSuggest = functions

const typeVariant = type ? getTypeVariant(type, dataTypes) : null;
const typeVariant = type ? getTypeVariant(type, dataTypes)[0].variant : null;

if (type && functions && typeVariant !== DataTypeVariant.NODE) {
function getGenericsCount(input: string): number {
Expand Down
2 changes: 1 addition & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ export function generateFlowSourceCode(

if (!isForInference) {
const expectedType = nodeTypes.parameters[index];
const isFunctionType = expectedType ? getTypeVariant(expectedType, dataTypes) === DataTypeVariant.NODE : false;
const isFunctionType = expectedType ? getTypeVariant(expectedType, dataTypes)[0].variant === DataTypeVariant.NODE : false;

if (isFunctionType) {
const lambdaArgName = `p_${sanitizeId(nodeId)}_${index}`;
Expand Down
66 changes: 36 additions & 30 deletions test/getTypeVariant.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,58 +4,64 @@ import {DATA_TYPES, FUNCTION_SIGNATURES} from "./data";
import {getTypesFromNode} from "../src";

describe('getTypeVariant', () => {
it('sollte PRIMITIVE für einfache Typen zurückgeben', () => {
expect(getTypeVariant("string", DATA_TYPES)).toBe(DataTypeVariant.PRIMITIVE);
expect(getTypeVariant("number", DATA_TYPES)).toBe(DataTypeVariant.PRIMITIVE);
expect(getTypeVariant("boolean", DATA_TYPES)).toBe(DataTypeVariant.PRIMITIVE);
it('identifies native TypeScript primitives (string, number, boolean) as PRIMITIVE variant', () => {
expect(getTypeVariant("string", DATA_TYPES)[0].variant).toBe(DataTypeVariant.PRIMITIVE);
expect(getTypeVariant("number", DATA_TYPES)[0].variant).toBe(DataTypeVariant.PRIMITIVE);
expect(getTypeVariant("boolean", DATA_TYPES)[0].variant).toBe(DataTypeVariant.PRIMITIVE);
});

it('sollte ARRAY für Array-Typen zurückgeben', () => {
expect(getTypeVariant("string[]", DATA_TYPES)).toBe(DataTypeVariant.ARRAY);
expect(getTypeVariant("Array<number>", DATA_TYPES)).toBe(DataTypeVariant.ARRAY);
it('recognizes both bracket notation and generic syntax for array types as ARRAY variant', () => {
expect(getTypeVariant("string[]", DATA_TYPES)[0].variant).toBe(DataTypeVariant.ARRAY);
expect(getTypeVariant("Array<number>", DATA_TYPES)[0].variant).toBe(DataTypeVariant.ARRAY);
});

it('sollte OBJECT für Interfaces oder Objekte mit Properties zurückgeben', () => {
expect(getTypeVariant("{ name: string }", DATA_TYPES)).toBe(DataTypeVariant.OBJECT);
expect(getTypeVariant("{}", DATA_TYPES)).toBe(DataTypeVariant.OBJECT);
expect(getTypeVariant("OBJECT<any>", DATA_TYPES)).toBe(DataTypeVariant.OBJECT);
it('classifies object literals and interface-like structures as OBJECT variant', () => {
expect(getTypeVariant("{ name: string }", DATA_TYPES)[0].variant).toBe(DataTypeVariant.OBJECT);
expect(getTypeVariant("{}", DATA_TYPES)[0].variant).toBe(DataTypeVariant.OBJECT);
expect(getTypeVariant("OBJECT<any>", DATA_TYPES)[0].variant).toBe(DataTypeVariant.OBJECT);
});

it('sollte TYPE für einfache Type-Aliase oder void zurückgeben', () => {
expect(getTypeVariant("void", DATA_TYPES)).toBe(DataTypeVariant.TYPE);
expect(getTypeVariant("any", DATA_TYPES)).toBe(DataTypeVariant.TYPE);
it('marks special types like void and any as TYPE variant', () => {
expect(getTypeVariant("void", DATA_TYPES)[0].variant).toBe(DataTypeVariant.TYPE);
expect(getTypeVariant("any", DATA_TYPES)[0].variant).toBe(DataTypeVariant.TYPE);
});

it('sollte LIST (NUMBER) als ARRAY erkennen (wenn in DATA_TYPES definiert)', () => {
// In data.ts ist LIST als T[] definiert
expect(getTypeVariant("LIST<NUMBER>", DATA_TYPES)).toBe(DataTypeVariant.ARRAY);
expect(getTypeVariant("LIST<unknown>", DATA_TYPES)).toBe(DataTypeVariant.ARRAY);
expect(getTypeVariant("LIST<T>", DATA_TYPES)).toBe(DataTypeVariant.ARRAY);
it('resolves generic LIST type aliases to ARRAY variant regardless of type parameter', () => {
// LIST<T> is defined as T[] in data.ts, so all LIST variants should be arrays
expect(getTypeVariant("LIST<NUMBER>", DATA_TYPES)[0].variant).toBe(DataTypeVariant.ARRAY);
expect(getTypeVariant("LIST<unknown>", DATA_TYPES)[0].variant).toBe(DataTypeVariant.ARRAY);
expect(getTypeVariant("LIST<T>", DATA_TYPES)[0].variant).toBe(DataTypeVariant.ARRAY);
});

it('sollte NODE für Funktionstypen wie CONSUMER zurückgeben', () => {
// In data.ts ist CONSUMER als (item:R) => void definiert
expect(getTypeVariant("CONSUMER<NUMBER>", DATA_TYPES)).toBe(DataTypeVariant.NODE);
it('identifies CONSUMER function types with generic parameters as NODE variant', () => {
// CONSUMER<T> represents a callback function (T) => void
expect(getTypeVariant("CONSUMER<NUMBER>", DATA_TYPES)[0].variant).toBe(DataTypeVariant.NODE);
});

it('sollte NODE für Funktionstypen wie RUNNABLE zurückgeben', () => {
// In data.ts ist CONSUMER als (item:R) => void definiert
expect(getTypeVariant("RUNNABLE", DATA_TYPES)).toBe(DataTypeVariant.NODE);
it('identifies parameterless function types like RUNNABLE as NODE variant', () => {
// RUNNABLE represents () => void with no parameters
expect(getTypeVariant("RUNNABLE", DATA_TYPES)[0].variant).toBe(DataTypeVariant.NODE);
});

it('sollte NODE für Funktionstypen wie PREDICATE zurückgeben', () => {
// In data.ts ist CONSUMER als (item:R) => void definiert
expect(getTypeVariant("PREDICATE<NUMBER>", DATA_TYPES)).toBe(DataTypeVariant.NODE);
it('identifies PREDICATE function types with generic parameters as NODE variant', () => {
// PREDICATE<T> represents a boolean-returning function (T) => boolean
expect(getTypeVariant("PREDICATE<NUMBER>", DATA_TYPES)[0].variant).toBe(DataTypeVariant.NODE);
});

it('correctly identifies type variants when retrieved directly from DATA_TYPES registry', () => {
// Verify that types stored in DATA_TYPES are properly classified
expect(getTypeVariant(DATA_TYPES.find(dt => dt.identifier === "LIST"), DATA_TYPES)[0].variant).toBe(DataTypeVariant.ARRAY);
expect(getTypeVariant(DATA_TYPES.find(dt => dt.identifier === "HTTP_METHOD"), DATA_TYPES)[0].variant).toBe(DataTypeVariant.TYPE);
});

it("Check if", () => {
it('recognizes callback parameter types in real function signatures as NODE variant', () => {
// When extracting types from std::list::for_each, the second parameter (consumer) should be NODE
const types = getTypesFromNode({
functionDefinition: {
identifier: "std::list::for_each"
}
}, FUNCTION_SIGNATURES, DATA_TYPES)
expect(getTypeVariant(types.parameters[1], DATA_TYPES)).toBe(DataTypeVariant.NODE);
expect(getTypeVariant(types.parameters[1], DATA_TYPES)[0].variant).toBe(DataTypeVariant.NODE);
});
});