@ui5/linter
Version:
A static code analysis tool for UI5
206 lines (202 loc) • 8.62 kB
JavaScript
/* eslint-disable @typescript-eslint/no-unsafe-enum-comparison */
import ts from "typescript";
import { getLogger } from "@ui5/logger";
import { UnsupportedModuleError } from "./util.js";
const log = getLogger("linter:ui5Types:amdTranspiler:parseModuleDeclaration");
const { SyntaxKind } = ts;
/**
* For a given array of arguments, try to match them to the parameters of the sap.ui.define and sap.ui.predefine
* signatures
*/
export default function parseModuleDeclaration(args, checker) {
/*
Signature: sap.ui.define(sModuleName?, aDependencies?, vFactory, bExport?)
* sModuleName can be omitted, or is string Literal
* aDependencies can be omitted, or is an ArrayExpression (containing string Literals)
* vFactory must be provided and can be anything (typically an (Arrow-)FunctionExpression or an ObjectExpression)
* bExport can be omitted, or is a boolean Literal
Each arguments can also be an Identifier. Whenever an Identifier is used, try to resolve it to a concrete type
before attempting to match it to a parameter. If it can't be resolved, an UnsupportedModuleError is thrown.
Similarly, each argument can also be a CallExpression, in which case we would have to try and resolve its
return type, which is currently out-of-scope.
Signature: sap.ui.predefine(sModuleName, aDependencies?, vFactory, bExport?)
* sModuleName is required
*/
if (args.length > 4) {
throw new UnsupportedModuleError(`Unexpected number of arguments (${args.length}) in sap.ui.define call`);
}
const resolvedArgs = args.map((arg) => {
if (ts.isIdentifier(arg)) {
// Try and resolve the identifier to a more concrete node
const sym = checker.getSymbolAtLocation(arg);
if (sym?.declarations) {
for (const decl of sym.declarations) {
if (ts.isVariableDeclaration(decl)) {
if (decl.initializer) {
return decl.initializer;
}
}
else if (ts.isParameter(decl)) {
// Check for iife
if (decl?.parent.kind === SyntaxKind.FunctionExpression &&
decl.parent.parent?.kind === SyntaxKind.ParenthesizedExpression &&
decl.parent.parent.parent?.kind === SyntaxKind.CallExpression) {
log.verbose(`Found iife`);
const funcExp = decl.parent;
const callExp = decl.parent.parent.parent;
// Determine parameter position
const paramIdx = funcExp.parameters.indexOf(decl);
if (paramIdx === -1) {
throw new Error(`Failed to determine parameter index for iife`);
}
return callExp.arguments[paramIdx];
} // Else: It is's not an iife, we can't resolve the argument value
}
else {
return decl;
}
}
}
}
return arg;
});
return _matchArgumentsToParameters(assertSupportedTypes(resolvedArgs));
}
function assertSupportedTypes(args) {
args.forEach((arg, idx) => {
// We only support a certain set of types. Abort if we encounter anything else
switch (arg.kind) {
case SyntaxKind.StringLiteral:
case SyntaxKind.NumericLiteral:
case SyntaxKind.ArrayLiteralExpression:
case SyntaxKind.ObjectLiteralExpression:
case SyntaxKind.ArrowFunction:
case SyntaxKind.FunctionExpression:
case SyntaxKind.TrueKeyword:
case SyntaxKind.FalseKeyword:
case SyntaxKind.FunctionDeclaration:
case SyntaxKind.ClassDeclaration:
case SyntaxKind.NoSubstitutionTemplateLiteral:
case SyntaxKind.PropertyAccessExpression:
return;
default:
throw new UnsupportedModuleError(`Unsupported type for argument in sap.ui.define call at index ${idx}: ${SyntaxKind[arg.kind]}`);
}
});
return args;
}
export function _matchArgumentsToParameters(args) {
// Create a grid of possible matches between the provided arguments and the expected parameters
const matchGrid = [];
if (args[0]) {
matchGrid.push(permute(args[0], 0 /* Param.ModuleName */));
}
if (args[1]) {
matchGrid.push(permute(args[1], 1 /* Param.Dependencies */));
}
if (args[2]) {
matchGrid.push(permute(args[2], 2 /* Param.Factory */));
}
if (args[3]) {
matchGrid.push(permute(args[3], 3 /* Param.Export */));
}
// This results in a two dimensional array containing values
// for all allowed permutations of the provided arguments
// Now we calculate the best match
for (let i = 0; i < matchGrid.length; i++) {
for (let j = 0; j < matchGrid[i].length; j++) {
if (matchGrid[i][j]) {
// Match
if (i > 0 && j > 0) {
matchGrid[i][j] = matchGrid[i - 1][j - 1] + 1;
}
else {
matchGrid[i][j] = 1;
}
}
else {
// No match
let prevI = 0, prevJ = 0;
if (i > 0) {
prevI = matchGrid[i - 1][j];
}
if (j > 0) {
prevJ = matchGrid[i][j - 1];
}
matchGrid[i][j] = Math.max(prevI, prevJ);
}
}
}
/* Visualization for the resulting "matchGrid":
// Case 1
Input (3 arguments): ArrayLiteralExpression, FunctionExpression, TrueKeyword
┌─────────────────┐
│arg\param│ 0 │ 1 │ 2 │ 3 │
├─────────────────┤
│ 0 │ 0 │ 1 │ 1 │ 1 │
│ 1 │ 0 │ 1 │ 2 │ 2 │
│ 2 │ 0 │ 1 │ 2 │ 3 │
└─────────────────┘
Solution:
arg #0 => dependencies, arg #1 => factory, arg #2 => export flag
(=> moduleName has been omitted)
Legend for the for-loop below:
arg = i
param = j
*/
// Find the highest matches and assign parameters accordingly
const result = {};
let highestMatch = 0;
for (let i = 0; i < matchGrid.length; i++) {
for (let j = 0; j <= 3 /* Param.Export */; j++) {
if (matchGrid[i][j] > highestMatch) {
highestMatch = matchGrid[i][j];
if (j === 0 /* Param.ModuleName */) {
result.moduleName = args[i];
}
else if (j === 1 /* Param.Dependencies */) {
result.dependencies = args[i];
}
else if (j === 2 /* Param.Factory */) {
result.factory = args[i];
}
else if (j === 3 /* Param.Export */) {
result.export = args[i];
}
}
}
}
return result;
}
// Compute which parameters the given argument could match with based on it's type
// For example an ArrayLiteralExpression could only be the dependencies or the factory parameter
function permute(arg, startAt) {
const perm = Array(4).fill(0);
if (startAt <= 0 /* Param.ModuleName */ && canBeModuleName(arg)) {
perm[0] = 1;
}
if (startAt <= 1 /* Param.Dependencies */ && canBeDependencies(arg)) {
perm[1] = 1;
}
if (startAt <= 2 /* Param.Factory */ && canBeFactory(arg)) {
perm[2] = 1;
}
if (startAt <= 3 /* Param.Export */ && canBeExport(arg)) {
perm[3] = 1;
}
return perm;
}
function canBeModuleName(arg) {
return arg.kind === SyntaxKind.StringLiteral ||
arg.kind === SyntaxKind.NoSubstitutionTemplateLiteral;
}
function canBeDependencies(arg) {
return arg.kind === SyntaxKind.ArrayLiteralExpression;
}
function canBeFactory(_arg) {
return true; // All supported types can be used for factory
}
function canBeExport(arg) {
return arg.kind === SyntaxKind.TrueKeyword || arg.kind === SyntaxKind.FalseKeyword;
}
//# sourceMappingURL=parseModuleDeclaration.js.map