@specs-feup/lara
Version:
A js port of the popular framework for building source-to-source compilers
459 lines (407 loc) • 11.7 kB
JavaScript
export function convertSpecification(input, baseJoinPointSpec = undefined) {
let typeNameSet = new Set();
let joinpointNameSet = new Set();
let unorderedJoinpoints = [];
let enumNameSet = new Set();
let unorderedEnums = [];
input.children.forEach((child) => {
typeNameSet.add(child.type);
if (child.type === "joinpoint") {
joinpointNameSet.add(child.name);
unorderedJoinpoints.push(child);
}
if (child.type === "enum") {
enumNameSet.add(child.name);
unorderedEnums.push(child);
}
});
let joinpoints = orderJoinpoints(unorderedJoinpoints);
let enums = orderJoinpoints(unorderedEnums);
let output = {
joinpoints: convertJoinpoints(joinpoints, joinpointNameSet, enumNameSet),
enums: convertEnums(enums),
};
if (baseJoinPointSpec !== undefined) {
output.joinpoints[0].extends = baseJoinPointSpec.joinpoints[0].name;
}
deduplicateJoinpoints(output.joinpoints, baseJoinPointSpec);
return output;
}
function orderJoinpoints(unorderedJoinpoints) {
let orderedNameSet = new Set();
let blockedJpSet = new Set();
let joinpoints = [];
unorderedJoinpoints.forEach((jp) => {
if (jp.extends) {
if (orderedNameSet.has(jp.extends)) {
joinpoints.push(jp);
orderedNameSet.add(jp.name);
} else {
blockedJpSet.add(jp);
}
} else {
joinpoints.push(jp);
orderedNameSet.add(jp.name);
}
});
while (blockedJpSet.size > 0) {
blockedJpSet.forEach((jp) => {
if (orderedNameSet.has(jp.extends)) {
joinpoints.push(jp);
orderedNameSet.add(jp.name);
blockedJpSet.delete(jp);
}
});
}
return joinpoints;
}
/**
*
* @param {*} joinpoints
* @param {Set<string>} joinpointNameSet
* @param {Set<string>} enumNameSet
* @returns {ReturnType<convertJoinpoint>[]}
*/
function convertJoinpoints(joinpoints, joinpointNameSet, enumNameSet) {
let convertedJoinpoints = [];
joinpoints.forEach((jp) => {
convertedJoinpoints.push(
convertJoinpoint(jp, joinpointNameSet, enumNameSet)
);
});
return convertedJoinpoints;
}
/**
*
* @param {*} jp
* @param {Set<string>} joinpointNameSet
* @param {Set<string>} enumNameSet
* @returns { { name: string, originalName: string, tooltip: string, extends: string | undefined, attributes: ReturnType<convertJoinpointAttribute>[], actions: { name: string, tooltip: string, returnType: string, parameters: ReturnType<convertJoinpointActionParameter>[], overloads: never[] }[] }, defaultAttribute: string | undefined }
*/
function convertJoinpoint(jp, joinpointNameSet, enumNameSet) {
let attributes = [];
let actions = [];
const actionNameSet = new Set();
jp.children.forEach((child) => {
switch (child.type) {
case "attribute":
if (child.children.length !== 1) {
convertJoinpointAction(
child,
joinpointNameSet,
enumNameSet,
actions,
actionNameSet
);
} else {
attributes.push(
convertJoinpointAttribute(child, joinpointNameSet, enumNameSet)
);
}
break;
case "action":
convertJoinpointAction(
child,
joinpointNameSet,
enumNameSet,
actions,
actionNameSet
);
break;
case "select":
// Do nothing
break;
default:
console.log("Unknown child type:", child.type);
}
});
const jpName = interpretType(jp.name, joinpointNameSet, enumNameSet);
return {
name: jpName,
originalName: jp.name,
tooltip: convertDeprecationNotice(jp.tooltip),
extends: jp.extends
? interpretType(jp.extends, joinpointNameSet, enumNameSet)
: undefined,
attributes: attributes,
actions: actions,
defaultAttribute: jp.defaultAttr,
};
}
/**
*
* @param {string} notice
* @returns {string}
*/
function convertDeprecationNotice(notice) {
if (notice?.includes("DEPRECATED")) {
notice = notice.replace("[DEPRECATED:", "@deprecated");
notice = notice.replace("DEPRECATED:", "@deprecated");
let splitTooltip = notice.split("]");
notice = splitTooltip.slice(1).join("]") + "\n\n" + splitTooltip[0];
}
return capitalizeFirstLetter(notice)?.trim();
}
/**
*
* @param {*} attributeObject
* @param {Set<string>} joinpointNameSet
* @param {Set<string>} enumNameSet
* @returns { { name: string, type: string, default: string } }
*/
function convertJoinpointAttribute(
attributeObject,
joinpointNameSet,
enumNameSet
) {
const attribute = attributeObject.children[0];
return {
name: attribute.name,
type: interpretType(attribute.type, joinpointNameSet, enumNameSet),
tooltip: convertDeprecationNotice(attributeObject.tooltip),
};
}
/**
*
* @param {*} parameterObject
* @param {Set<string>} joinpointNameSet
* @param {Set<string>} enumNameSet
* @returns { { name: string, type: string, default: string } }
*/
function convertJoinpointActionParameter(
parameterObject,
joinpointNameSet,
enumNameSet
) {
const type = interpretType(
parameterObject.type,
joinpointNameSet,
enumNameSet
);
let parameterName = parameterObject.name;
switch (parameterName) {
case "function":
parameterName = "func";
break;
case "else":
parameterName = "elseStatement";
}
let defaultValue = parameterObject.defaultValue;
if (defaultValue === "") {
defaultValue = undefined;
}
return {
name: parameterName,
type: type,
default: JSON.stringify(defaultValue),
};
}
/**
*
* @param {Object} actionObject
* @param {Set<string>} joinpointNameSet
* @param {Set<string>} enumNameSet
* @param {Object[]} actions
* @param {Set<string>} actionNameSet
* @param {string} [overrideName=null]
* @returns
*/
function convertJoinpointAction(
actionObject,
joinpointNameSet,
enumNameSet,
actions,
actionNameSet,
overrideName = null
) {
const action = actionObject.children[0];
const actionName = overrideName ?? action.name;
const convertedAction = {
name: actionName,
tooltip: convertDeprecationNotice(actionObject.tooltip),
returnType: interpretType(action.type, joinpointNameSet, enumNameSet),
parameters: actionObject.children.slice(1).map((parameter) => {
return convertJoinpointActionParameter(
parameter,
joinpointNameSet,
enumNameSet
);
}),
overloads: [],
};
if (actionNameSet.has(convertedAction.name)) {
for (const action of actions) {
if (action.name === convertedAction.name) {
if (action.overloads.length === 0) {
action.overloads.push(structuredClone(action));
let paramCounter = 1;
action.parameters.forEach((param) => {
param.name = `p${paramCounter++}`;
if (param.default !== undefined) {
param.default = '"null"';
}
});
}
action.returnType += " | " + convertedAction.returnType;
for (let i = 0; i < convertedAction.parameters.length; i++) {
if (i >= action.parameters.length) {
action.parameters.push(
structuredClone(convertedAction.parameters[i])
);
action.parameters[i].name = `p${i + 1}`;
action.parameters[i].default = '"null"';
continue;
}
const parameter = convertedAction.parameters[i];
const existingParameter = action.parameters[i];
if (parameter.type !== existingParameter.type) {
existingParameter.type += " | " + parameter.type;
}
}
if (convertedAction.parameters.length < action.parameters.length) {
for (
let i = convertedAction.parameters.length;
i < action.parameters.length;
i++
) {
action.parameters[i].default = '"null"';
}
}
action.overloads.push(convertedAction);
action.returnType = [...new Set(action.returnType.split(" | "))].join(
" | "
);
for (const i in action.parameters) {
action.parameters[i].type = [
...new Set(action.parameters[i].type.split(" | ")),
].join(" | ");
}
break;
}
}
return;
}
actionNameSet.add(actionName);
actions.push(convertedAction);
}
/**
*
* @param {Object[]} enums
* @returns {(ReturnType<convertEnum>)[]}
*/
function convertEnums(enums) {
let convertedEnums = [];
enums.forEach((enumObj) => {
convertedEnums.push(convertEnum(enumObj));
});
return convertedEnums;
}
/**
*
* @param {Object} e
* @returns { {name: string, extends: string, entries: string[]} }
*/
function convertEnum(e) {
return {
name: e.name,
extends: e.extends,
entries: e.children.map((child) => {
return child.value;
}),
};
}
/**
*
* @param {string} string
* @returns {string}
*/
export function capitalizeFirstLetter(string) {
if (!string) return string;
return string.charAt(0).toUpperCase() + string.slice(1);
}
/**
*
* @param {string} typeString
* @param {Set<string>} joinpointNameSet
* @param {Set<string>} enumNameSet
* @returns {string}
*/
function interpretType(typeString, joinpointNameSet, enumNameSet) {
// Detect array types
if (typeString.endsWith("[]")) {
const baseType = typeString.slice(0, -2);
return `${interpretType(baseType, joinpointNameSet, enumNameSet)}[]`;
} else if (typeString.startsWith("[")) {
// Example: [abc | asd] to "abc" | "asd"
const literals = typeString
.slice(1, -1)
.split("|")
.map((literal) => `"${literal.trim()}"`)
.join(" | ");
return literals;
}
if (joinpointNameSet.has(typeString) || enumNameSet.has(typeString)) {
const jpType = capitalizeFirstLetter(typeString);
switch (jpType) {
case "Function":
return "FunctionJp";
case "File":
return "FileJp";
case "Record":
return "RecordJp";
}
return jpType;
}
switch (typeString) {
case "Integer":
case "int":
case "Long":
case "Double":
return "number";
break;
case "Map":
return "Record<string, any>";
default:
return typeString.toLowerCase();
break;
}
}
function deduplicateJoinpoints(joinpoints, baseJoinPointSpec = undefined) {
for (const joinpoint of joinpoints) {
// Find the parent joinpoint
let parentJoinpoint = joinpoints.find(
(jp) => jp.name === joinpoint.extends
);
if (parentJoinpoint === undefined && baseJoinPointSpec !== undefined) {
parentJoinpoint = baseJoinPointSpec.joinpoints.find(
(jp) => jp.name === joinpoint.extends
);
}
while (parentJoinpoint) {
for (const attributeIndex in joinpoint.attributes) {
for (const parentAttribute of parentJoinpoint.attributes) {
if (
JSON.stringify(joinpoint.attributes[attributeIndex]) ===
JSON.stringify(parentAttribute)
) {
joinpoint.attributes.splice(attributeIndex, 1);
}
}
}
for (const actionIndex in joinpoint.actions) {
for (const parentAction of parentJoinpoint.actions) {
if (
JSON.stringify(joinpoint.actions[actionIndex]) ===
JSON.stringify(parentAction)
) {
joinpoint.actions.splice(actionIndex, 1);
}
}
}
parentJoinpoint = joinpoints.find(
(jp) => jp.name === parentJoinpoint.extends
);
}
}
}