diff --git a/packages/custom-functions-metadata/src/generate.ts b/packages/custom-functions-metadata/src/generate.ts index 3d0da4e50..43a973832 100644 --- a/packages/custom-functions-metadata/src/generate.ts +++ b/packages/custom-functions-metadata/src/generate.ts @@ -1,4 +1,4 @@ -import { IFunction, IGenerateResult, IParseTreeResult, parseTree } from "./parseTree"; +import { IFunction, IEnum, IGenerateResult, IParseTreeResult, parseTree } from "./parseTree"; import { existsSync, readFileSync } from "fs"; /** @@ -7,51 +7,55 @@ import { existsSync, readFileSync } from "fs"; * @param outputFileName - Name of the file to create (i.e functions.json) */ export async function generateCustomFunctionsMetadata( - input: string | string[], - wantConsoleOutput: boolean = false - ): Promise { - const inputFiles: string[] = Array.isArray(input) ? input : [input]; - const functions: IFunction[] = []; - const generateResults: IGenerateResult = { - metadataJson: "", - associate: [], - errors: [], - }; - - if (input && inputFiles.length > 0) { - inputFiles.forEach((inputFile) => { - inputFile = inputFile.trim(); - if (!inputFile) { - // ignore empty strings - } else if (!existsSync(inputFile)) { - throw new Error(`File not found: ${inputFile}`); - } else { - const sourceCode = readFileSync(inputFile, "utf-8"); - const parseTreeResult: IParseTreeResult = parseTree(sourceCode, inputFile); - parseTreeResult.extras.forEach((extra) => extra.errors.forEach((err) => generateResults.errors.push(err))); - - if (generateResults.errors.length > 0) { - if (wantConsoleOutput) { - console.error("Errors in file: " + inputFile); - generateResults.errors.forEach((err) => console.error(err)); - } - } else { - functions.push(...parseTreeResult.functions); - generateResults.associate.push(...parseTreeResult.associate); + input: string | string[], + wantConsoleOutput: boolean = false +): Promise { + const inputFiles: string[] = Array.isArray(input) ? input : [input]; + const functions: IFunction[] = []; + const enums: IEnum[] = []; + const generateResults: IGenerateResult = { + metadataJson: "", + associate: [], + errors: [], + }; + + if (input && inputFiles.length > 0) { + inputFiles.forEach((inputFile) => { + inputFile = inputFile.trim(); + if (!inputFile) { + // ignore empty strings + } else if (!existsSync(inputFile)) { + throw new Error(`File not found: ${inputFile}`); + } else { + const sourceCode = readFileSync(inputFile, "utf-8"); + const parseTreeResult: IParseTreeResult = parseTree(sourceCode, inputFile); + parseTreeResult.extras.forEach((extra) => extra.errors.forEach((err) => generateResults.errors.push(err))); + + if (generateResults.errors.length > 0) { + if (wantConsoleOutput) { + console.error("Errors in file: " + inputFile); + generateResults.errors.forEach((err) => console.error(err)); } + } else { + functions.push(...parseTreeResult.functions); + generateResults.associate.push(...parseTreeResult.associate); + enums.push(...parseTreeResult.enums); } - }); - - if (functions.length > 0) { - const metadata = { - allowCustomDataForDataTypeAny: true, - functions: functions, - } - generateResults.metadataJson = JSON.stringify(metadata, null, 4); } + }); + + if (functions.length > 0) { + const metadata: { allowCustomDataForDataTypeAny: boolean; functions: IFunction[]; enums?: IEnum[] } = { + allowCustomDataForDataTypeAny: true, + functions: functions, + enums: enums, + }; + if (enums.length == 0) { + delete metadata.enums; + } + generateResults.metadataJson = JSON.stringify(metadata, null, 4); } - - return generateResults; } - - \ No newline at end of file + + return generateResults; +} diff --git a/packages/custom-functions-metadata/src/parseTree.ts b/packages/custom-functions-metadata/src/parseTree.ts index 5ef3c35e7..52511f580 100644 --- a/packages/custom-functions-metadata/src/parseTree.ts +++ b/packages/custom-functions-metadata/src/parseTree.ts @@ -40,6 +40,12 @@ export interface IFunctionParameter { repeating?: boolean; } +interface IParameterType { + type: string; + customEnumType?: string; + cellValueType?: string; +} + export interface IFunctionResult { type?: string; dimensionality?: string; @@ -60,6 +66,7 @@ export interface IParseTreeResult { associate: IAssociate[]; extras: IFunctionExtras[]; functions: IFunction[]; + enums: IEnum[]; } export interface IAssociate { @@ -83,7 +90,7 @@ interface IArrayType { } interface IGetParametersArguments { - enumList: string[]; + enums: IEnum[]; extra: IFunctionExtras; jsDocParamInfo: { [key: string]: string }; jsDocParamOptionalInfo: { [key: string]: string }; @@ -97,7 +104,21 @@ interface IJsDocParamType { dimensionality: string; } +export interface IEnum { + id: string; + type: string; + values: IEnumValue[]; +} + +interface IEnumValue { + value: string | number; + description: string; + tooltip: string; +} + const CUSTOM_FUNCTION = "customfunction"; // case insensitive @CustomFunction tag to identify custom functions in JSDoc +const CUSTOM_ENUM = "customenum"; // case insensitive @CustomEnum tag to identify custom Enums in JSDoc +const ENUM = "enum"; const HELPURL_PARAM = "helpurl"; const VOLATILE = "volatile"; const STREAMING = "streaming"; @@ -138,6 +159,20 @@ const TYPE_CUSTOM_FUNCTION_CANCELABLE = { ["customfunctions.cancelablehandler"]: 1, ["customfunctions.cancelableinvocation"]: 2, }; + +const CELLVALUETYPE_TO_BASICTYPE_MAPPINGS = { + cellvalue: "any", + booleancellvalue: "boolean", + doublecellvalue: "number", + entitycellvalue: "any", + errorcellvalue: "any", + formattednumbercellvalue: "number", + linkedentitycellvalue: "any", + localimagecellvalue: "any", + stringcellvalue: "string", + webimagecellvalue: "any", +}; + const TYPE_CUSTOM_FUNCTION_INVOCATION = "customfunctions.invocation"; // These does not work if the developer is using namespace/type alias. To support that, we'd need to @@ -173,191 +208,549 @@ export function parseTree(sourceCode: string, sourceFileName: string): IParseTre const associate: IAssociate[] = []; const functions: IFunction[] = []; const extras: IFunctionExtras[] = []; - const enumList: string[] = []; + const enums: IEnum[] = []; const functionNames: string[] = []; const metadataFunctionNames: string[] = []; - const ids: string[] = []; + const metadataEnumIds: string[] = []; + const metadataFunctionIds: string[] = []; const sourceFile = ts.createSourceFile(sourceFileName, sourceCode, ts.ScriptTarget.Latest, true); buildEnums(sourceFile); - visit(sourceFile); + buildFunctions(sourceFile); const parseTreeResult: IParseTreeResult = { associate, extras, functions, + enums, }; return parseTreeResult; function buildEnums(node: ts.Node) { if (ts.isEnumDeclaration(node)) { - enumList.push(node.name.getText()); + buildSingleEnum(node); } ts.forEachChild(node, buildEnums); } - function visit(node: ts.Node) { + function buildSingleEnum(node: ts.Node) { + if (!ts.isEnumDeclaration(node)) { + return; + } + + if (!node.parent || node.parent.kind !== ts.SyntaxKind.SourceFile) { + return; + } + + const enumDeclaration = node as ts.EnumDeclaration; + + if (!isCustomEnum(enumDeclaration)) { + return; + } + + if (enumDeclaration.members.length === 0) { + return; + } + + const extra: IFunctionExtras = { + errors: [], + javascriptFunctionName: "", + }; + + const id = enumDeclaration.name.text; + validateId(id, getPosition(enumDeclaration), extra); + extras.push(extra); + + if (checkForDuplicate(metadataEnumIds, id)) { + const errorString = `@customenum tag specifies a duplicate name: ${id}`; + extras.push({ errors: [logError(errorString, getPosition(enumDeclaration))], javascriptFunctionName: "" }); + } + + metadataEnumIds.push(id); + + let isNumberEnum = false; + + // Extract JSDoc type from the enum declaration + const jsDocTags = ts.getJSDocTags(enumDeclaration); + const jsDocTypeTag = jsDocTags.find((tag) => tag.tagName.text === ENUM); + + let jsDocType: string | null = null; + if (jsDocTypeTag) { + const typeExpression = (jsDocTypeTag as ts.JSDocPropertyTag).typeExpression; + if (typeExpression && ts.isJSDocTypeExpression(typeExpression)) { + jsDocType = typeExpression.getFullText().trim(); + } + } + + if (!jsDocType) { + const errorString = `Enum ${id} is missing @enum type annotation`; + extras.push({ errors: [logError(errorString, getPosition(enumDeclaration))], javascriptFunctionName: "" }); + return; + } + + const firstMember = enumDeclaration.members[0]; + if (firstMember.initializer) { + const initializerText = firstMember.initializer.getText(); + isNumberEnum = !isNaN(Number(initializerText)); + + const errorString = `Enum type must match the enum type in annotation: ${id}`; + if ((isNumberEnum && jsDocType !== "{number}") || (!isNumberEnum && jsDocType !== "{string}")) { + extras.push({ errors: [logError(errorString, getPosition(enumDeclaration))], javascriptFunctionName: "" }); + return; + } + } + + const values: IEnumValue[] = []; + for (const member of enumDeclaration.members) { + const value = member.initializer ? JSON.parse(member.initializer.getText()) : member.name.getText(); + if ((isNumberEnum && typeof value !== "number") || (!isNumberEnum && typeof value !== "string")) { + const errorString = `Enum value type must be consistent: ${id}`; + extras.push({ errors: [logError(errorString, getPosition(enumDeclaration))], javascriptFunctionName: "" }); + return; + } + const description = member.name.getText(); + const tooltip = + ts + .getLeadingCommentRanges(sourceFile.getFullText(), member.getFullStart()) + ?.map((range) => sourceFile.getFullText().substring(range.pos, range.end).trim()) + .join("\n") + .replace(/^\s*[/*]+\s?/gm, "") // Strip leading slashes, asterisks, and whitespace + .replace(/\s*[/*]+$/gm, "") || ""; // Strip trailing slashes, asterisks, and whitespace + values.push({ value, description, tooltip }); + } + + const enumItem: IEnum = { + id, + type: isNumberEnum ? "number" : "string", + values, + }; + + enums.push(enumItem); + } + + function buildFunctions(node: ts.Node) { if (ts.isFunctionDeclaration(node)) { - if (node.parent && node.parent.kind === ts.SyntaxKind.SourceFile) { - const functionDeclaration = node as ts.FunctionDeclaration; - const position = getPosition(functionDeclaration); - const functionErrors: string[] = []; - const functionName = functionDeclaration.name ? functionDeclaration.name.text : ""; - - if (checkForDuplicate(functionNames, functionName)) { - const errorString = `Duplicate function name: ${functionName}`; - functionErrors.push(logError(errorString, position)); - } + buildSingleFunction(node); + } + ts.forEachChild(node, buildFunctions); + } - functionNames.push(functionName); - - if (isCustomFunction(functionDeclaration)) { - const extra: IFunctionExtras = { - errors: functionErrors, - javascriptFunctionName: functionName, - }; - const idName = getTagComment(functionDeclaration, CUSTOM_FUNCTION); - const idNameArray = idName.split(" "); - const jsDocParamInfo = getJSDocParams(functionDeclaration); - const jsDocParamTypeInfo = getJSDocParamsType(functionDeclaration); - const jsDocParamOptionalInfo = getJSDocParamsOptionalType(functionDeclaration); - - const [lastParameter] = functionDeclaration.parameters.slice(-1); - const isStreamingFunction = hasStreamingInvocationParameter(lastParameter, jsDocParamTypeInfo); - const isCancelableFunction = hasCancelableInvocationParameter(lastParameter, jsDocParamTypeInfo); - const isInvocationFunction = hasInvocationParameter(lastParameter, jsDocParamTypeInfo); - - const parametersToParse = - isStreamingFunction || isCancelableFunction || isInvocationFunction - ? functionDeclaration.parameters.slice(0, functionDeclaration.parameters.length - 1) - : functionDeclaration.parameters.slice(0, functionDeclaration.parameters.length); - - const parameterItems: IGetParametersArguments = { - enumList, - extra, - jsDocParamInfo, - jsDocParamOptionalInfo, - jsDocParamTypeInfo, - parametersToParse, - }; - const parameters = getParameters(parameterItems); - - const description = getDescription(functionDeclaration); - const helpUrl = normalizeLineEndings(getTagComment(functionDeclaration, HELPURL_PARAM)); - - const result = getResults( - functionDeclaration, - isStreamingFunction, - lastParameter, - jsDocParamTypeInfo, - extra, - enumList - ); - - const options = getOptions( - functionDeclaration, - isStreamingFunction, - isCancelableFunction, - isInvocationFunction, - extra - ); - - const funcName: string = functionDeclaration.name ? functionDeclaration.name.text : ""; - const id = normalizeCustomFunctionId(idNameArray[0] || funcName); - const name = idNameArray[1] || id; - - validateId(id, position, extra); - validateName(name, position, extra); - - if (checkForDuplicate(metadataFunctionNames, name)) { - const errorString = `@customfunction tag specifies a duplicate name: ${name}`; - functionErrors.push(logError(errorString, position)); - } + function buildSingleFunction(node: ts.Node) { + if (!ts.isFunctionDeclaration(node)) { + return; + } - metadataFunctionNames.push(name); + if (!node.parent || node.parent.kind !== ts.SyntaxKind.SourceFile) { + return; + } - if (checkForDuplicate(ids, id)) { - const errorString = `@customfunction tag specifies a duplicate id: ${id}`; - functionErrors.push(logError(errorString, position)); - } + const functionDeclaration = node as ts.FunctionDeclaration; - ids.push(id); - associate.push({ sourceFileName, functionName, id }); - - const functionMetadata: IFunction = { - description, - helpUrl, - id, - name, - options, - parameters, - result, - }; - - if ( - !options.cancelable && - !options.requiresAddress && - !options.stream && - !options.volatile && - !options.requiresParameterAddresses && - !options.excludeFromAutoComplete && - !options.linkedEntityDataProvider && - !options.capturesCallingObject - ) { - delete functionMetadata.options; - } else { - if (!options.cancelable) { - delete options.cancelable; - } + if (!isCustomFunction(functionDeclaration)) { + return; + } - if (!options.requiresAddress) { - delete options.requiresAddress; - } + const position = getPosition(functionDeclaration); + const functionErrors: string[] = []; + const functionName = functionDeclaration.name ? functionDeclaration.name.text : ""; - if (!options.stream) { - delete options.stream; - } + if (checkForDuplicate(functionNames, functionName)) { + const errorString = `Duplicate function name: ${functionName}`; + functionErrors.push(logError(errorString, position)); + } - if (!options.volatile) { - delete options.volatile; - } + functionNames.push(functionName); - if (!options.requiresParameterAddresses) { - delete options.requiresParameterAddresses; - } + const extra: IFunctionExtras = { + errors: functionErrors, + javascriptFunctionName: functionName, + }; + const idName = getTagComment(functionDeclaration, CUSTOM_FUNCTION); + const idNameArray = idName.split(" "); + const jsDocParamInfo = getJSDocParams(functionDeclaration); + const jsDocParamTypeInfo = getJSDocParamsType(functionDeclaration); + const jsDocParamOptionalInfo = getJSDocParamsOptionalType(functionDeclaration); + + const [lastParameter] = functionDeclaration.parameters.slice(-1); + const isStreamingFunction = hasStreamingInvocationParameter(lastParameter, jsDocParamTypeInfo); + const isCancelableFunction = hasCancelableInvocationParameter(lastParameter, jsDocParamTypeInfo); + const isInvocationFunction = hasInvocationParameter(lastParameter, jsDocParamTypeInfo); + + const parametersToParse = + isStreamingFunction || isCancelableFunction || isInvocationFunction + ? functionDeclaration.parameters.slice(0, functionDeclaration.parameters.length - 1) + : functionDeclaration.parameters.slice(0, functionDeclaration.parameters.length); + + const parameterItems: IGetParametersArguments = { + enums, + extra, + jsDocParamInfo, + jsDocParamOptionalInfo, + jsDocParamTypeInfo, + parametersToParse, + }; + const parameters = getParameters(parameterItems); - if (!options.excludeFromAutoComplete) { - delete options.excludeFromAutoComplete; - } + const description = getDescription(functionDeclaration); + const helpUrl = normalizeLineEndings(getTagComment(functionDeclaration, HELPURL_PARAM)); - if (!options.linkedEntityDataProvider) { - delete options.linkedEntityDataProvider; - } + const result = getResults(functionDeclaration, isStreamingFunction, lastParameter, jsDocParamTypeInfo, extra); - if (!options.capturesCallingObject) { - delete options.capturesCallingObject; - } - } + const options = getOptions( + functionDeclaration, + isStreamingFunction, + isCancelableFunction, + isInvocationFunction, + extra + ); - if (!functionMetadata.helpUrl) { - delete functionMetadata.helpUrl; - } + const funcName: string = functionDeclaration.name ? functionDeclaration.name.text : ""; + const id = normalizeCustomFunctionId(idNameArray[0] || funcName); + const name = idNameArray[1] || id; - if (!functionMetadata.description) { - delete functionMetadata.description; - } + validateId(id, position, extra); + validateName(name, position, extra); - if (!functionMetadata.result) { - delete functionMetadata.result; - } + if (checkForDuplicate(metadataFunctionNames, name)) { + const errorString = `@customfunction tag specifies a duplicate name: ${name}`; + functionErrors.push(logError(errorString, position)); + } - extras.push(extra); - functions.push(functionMetadata); + metadataFunctionNames.push(name); + + if (checkForDuplicate(metadataFunctionIds, id)) { + const errorString = `@customfunction tag specifies a duplicate id: ${id}`; + functionErrors.push(logError(errorString, position)); + } + + metadataFunctionIds.push(id); + associate.push({ sourceFileName, functionName, id }); + + const functionMetadata: IFunction = { + description, + helpUrl, + id, + name, + options, + parameters, + result, + }; + + if ( + !options.cancelable && + !options.requiresAddress && + !options.stream && + !options.volatile && + !options.requiresParameterAddresses && + !options.excludeFromAutoComplete && + !options.linkedEntityDataProvider && + !options.capturesCallingObject + ) { + delete functionMetadata.options; + } else { + if (!options.cancelable) { + delete options.cancelable; + } + + if (!options.requiresAddress) { + delete options.requiresAddress; + } + + if (!options.stream) { + delete options.stream; + } + + if (!options.volatile) { + delete options.volatile; + } + + if (!options.requiresParameterAddresses) { + delete options.requiresParameterAddresses; + } + + if (!options.excludeFromAutoComplete) { + delete options.excludeFromAutoComplete; + } + + if (!options.linkedEntityDataProvider) { + delete options.linkedEntityDataProvider; + } + + if (!options.capturesCallingObject) { + delete options.capturesCallingObject; + } + } + + if (!functionMetadata.helpUrl) { + delete functionMetadata.helpUrl; + } + + if (!functionMetadata.description) { + delete functionMetadata.description; + } + + if (!functionMetadata.result) { + delete functionMetadata.result; + } + + extras.push(extra); + functions.push(functionMetadata); + } + + /** + * Gets the parameter type of the node + * @param node TypeNode + */ + function getParamType(node: ts.TypeNode, extra: IFunctionExtras): IParameterType { + let type = "any"; + // Only get type for typescript files. js files will return "any" for all types + if (!node) { + return { type: type }; + } + const typePosition = getPosition(node); + + // Get the inner type node if it is an array + if (typeNodeIsArray(node)) { + let arrayType: IArrayType = { + dimensionality: 0, + node, + }; + arrayType = getArrayDimensionalityAndType(node); + node = arrayType.node; + } + + // We currently accept the following types of reference node: enum will be converted to "any", + // Excel.CellValue will be converted accordingly. Anything else is invalid. (Array reference node has already been covered above.) + if (ts.isTypeReferenceNode(node)) { + const typeReferenceNode = node as ts.TypeReferenceNode; + let nodeTypeName = typeReferenceNode.typeName.getText(); + // check enum type + for (const enumItem of enums) { + if (enumItem.id === nodeTypeName) { + return { type: enumItem.type, customEnumType: nodeTypeName }; + } + } + + // @ts-ignore + if (CELLVALUETYPE_MAPPINGS[nodeTypeName]) { + // @ts-ignore + let cellValue = CELLVALUETYPE_MAPPINGS[nodeTypeName]; + if (cellValue === "unsupported") { + const errorString = `Custom function does not support cell value type: ${typeReferenceNode.typeName.getText()}`; + extra.errors.push(logError(errorString, typePosition)); + return { type: type }; } + // @ts-ignore + return { type: CELLVALUETYPE_TO_BASICTYPE_MAPPINGS[cellValue], cellValueType: cellValue }; } + + const errorString = `Custom function does not support type "${typeReferenceNode.typeName.getText()}" as input or return parameter.`; + extra.errors.push(logError(errorString, typePosition)); + return { type: type }; } - ts.forEachChild(node, visit); + // @ts-ignore + type = TYPE_MAPPINGS[node.kind]; + if (!type) { + extra.errors.push(logError("Type doesn't match mappings", typePosition)); + } + + return { type: type }; + } + + /** + * Determines the results parameter for the json + * @param func - Function + * @param isStreaming - Is a streaming function + * @param lastParameter - Last parameter of the function signature + */ + function getResults( + func: ts.FunctionDeclaration, + isStreamingFunction: boolean, + lastParameter: ts.ParameterDeclaration, + jsDocParamTypeInfo: { [key: string]: IJsDocParamType }, + extra: IFunctionExtras + ): IFunctionResult { + let resultType = "any"; + let resultDim = "scalar"; + const defaultResultItem: IFunctionResult = { + dimensionality: resultDim, + type: resultType, + }; + + const lastParameterPosition = getPosition(lastParameter); + + // Try and determine the return type. If one can't be determined we will set to any type + if (isStreamingFunction) { + const lastParameterType = lastParameter.type as ts.TypeReferenceNode; + if (!lastParameterType) { + // Need to get result type from param {type} + const name = (lastParameter.name as ts.Identifier).text; + const ptype = jsDocParamTypeInfo[name]; + // @ts-ignore + resultType = ptype.returnType; + resultDim = ptype.dimensionality; + const paramResultItem: IFunctionResult = { + dimensionality: resultDim, + type: resultType, + }; + + if (paramResultItem.dimensionality === "scalar") { + delete paramResultItem.dimensionality; + } + + return paramResultItem; + } + if (!lastParameterType.typeArguments || lastParameterType.typeArguments.length !== 1) { + const errorString = + "The 'CustomFunctions.StreamingHandler' needs to be passed in a single result type (e.g., 'CustomFunctions.StreamingHandler < number >') :"; + extra.errors.push(logError(errorString, lastParameterPosition)); + return defaultResultItem; + } + const returnType = func.type as ts.TypeReferenceNode; + if (returnType && returnType.getFullText().trim() !== "void") { + const errorString = `A streaming function should return 'void'. Use CustomFunctions.StreamingHandler.setResult() to set results.`; + extra.errors.push(logError(errorString, lastParameterPosition)); + return defaultResultItem; + } + resultType = getParamType(lastParameterType.typeArguments[0], extra).type; + resultDim = getParamDim(lastParameterType.typeArguments[0]); + } else if (func.type) { + if ( + func.type.kind === ts.SyntaxKind.TypeReference && + (func.type as ts.TypeReferenceNode).typeName.getText() === "Promise" && + (func.type as ts.TypeReferenceNode).typeArguments && + // @ts-ignore + (func.type as ts.TypeReferenceNode).typeArguments.length === 1 + ) { + resultType = getParamType( + // @ts-ignore + (func.type as ts.TypeReferenceNode).typeArguments[0], + extra + ).type; + resultDim = getParamDim( + // @ts-ignore + (func.type as ts.TypeReferenceNode).typeArguments[0] + ); + } else { + resultType = getParamType(func.type, extra).type; + resultDim = getParamDim(func.type); + } + } + + // Check the code comments for @return parameter + const returnTypeFromJSDoc = ts.getJSDocReturnType(func); + if (returnTypeFromJSDoc) { + if (func.type && func.type.kind !== returnTypeFromJSDoc.kind) { + const name = (func.name as ts.Identifier).text; + const returnPosition = getPosition(returnTypeFromJSDoc); + const errorString = `Type {${ts.SyntaxKind[func.type.kind]}:${ + ts.SyntaxKind[returnTypeFromJSDoc.kind] + }} doesn't match for return type : ${name}`; + extra.errors.push(logError(errorString, returnPosition)); + } + if ( + returnTypeFromJSDoc.kind === ts.SyntaxKind.TypeReference && + (returnTypeFromJSDoc as ts.TypeReferenceNode).typeName.getText() === "Promise" && + (returnTypeFromJSDoc as ts.TypeReferenceNode).typeArguments && + // @ts-ignore + (returnTypeFromJSDoc as ts.TypeReferenceNode).typeArguments.length === 1 + ) { + resultType = getParamType( + // @ts-ignore + (returnTypeFromJSDoc as ts.TypeReferenceNode).typeArguments[0], + extra + ).type; + resultDim = getParamDim( + // @ts-ignore + (returnTypeFromJSDoc as ts.TypeReferenceNode).typeArguments[0] + ); + } else { + resultType = getParamType(returnTypeFromJSDoc, extra).type; + resultDim = getParamDim(returnTypeFromJSDoc); + } + } + + const resultItem: IFunctionResult = { + dimensionality: resultDim, + type: resultType, + }; + + // Only return dimensionality = matrix. Default assumed scalar + if (resultDim === "scalar") { + delete resultItem.dimensionality; + } + + if (resultType === "any") { + delete resultItem.type; + } + + return resultItem; + } + + /** + * Determines the parameter details for the json + * @param params - Parameters + * @param jsDocParamTypeInfo - jsDocs parameter type info + * @param jsDocParamInfo = jsDocs parameter info + */ + function getParameters(parameterItem: IGetParametersArguments): IFunctionParameter[] { + const parameterMetadata: IFunctionParameter[] = []; + parameterItem.parametersToParse + .map((p: ts.ParameterDeclaration) => { + const parameterPosition = getPosition(p); + // Get type node of parameter from typescript + let typeNode = p.type as ts.TypeNode; + const name = (p.name as ts.Identifier).text; + // Get type node of parameter from jsDocs + const parameterJSDocTypeNode = ts.getJSDocType(p); + if (parameterJSDocTypeNode && typeNode) { + if (parameterJSDocTypeNode.kind !== typeNode.kind) { + const errorString = `Type {${ts.SyntaxKind[parameterJSDocTypeNode.kind]}:${ + ts.SyntaxKind[typeNode.kind] + }} doesn't match for parameter : ${name}`; + parameterItem.extra.errors.push(logError(errorString, parameterPosition)); + } + } + if (!typeNode && parameterJSDocTypeNode) { + typeNode = parameterJSDocTypeNode; + } + const ptype = getParamType(typeNode, parameterItem.extra); + + const pMetadataItem: IFunctionParameter = { + description: parameterItem.jsDocParamInfo[name], + dimensionality: getParamDim(typeNode), + name, + optional: getParamOptional(p, parameterItem.jsDocParamOptionalInfo), + repeating: isRepeatingParameter(typeNode), + ...ptype, + }; + + // Only return dimensionality = matrix. Default assumed scalar + if (pMetadataItem.dimensionality === "scalar") { + delete pMetadataItem.dimensionality; + } + + // only include optional if true + if (!pMetadataItem.optional) { + delete pMetadataItem.optional; + } + + // only include description if it has a value + if (!pMetadataItem.description) { + delete pMetadataItem.description; + } + + // only return repeating if true and allowed + if (!pMetadataItem.repeating) { + delete pMetadataItem.repeating; + } + + parameterMetadata.push(pMetadataItem); + }) + .filter((meta) => meta); + + return parameterMetadata; } } @@ -394,7 +787,7 @@ function areStringsEqual(first: string, second: string, ignoreCase = true): bool * @param node function, parameter, or node */ function getPosition( - node: ts.FunctionDeclaration | ts.ParameterDeclaration | ts.TypeNode, + node: ts.FunctionDeclaration | ts.ParameterDeclaration | ts.EnumDeclaration | ts.TypeNode, position?: number ): ts.LineAndCharacter | null { let positionLocation = null; @@ -416,11 +809,11 @@ function validateId(id: string, position: ts.LineAndCharacter | null, extra: IFu if (!id) { id = "Function name is invalid"; } - const errorString = `The custom function id contains invalid characters. Allowed characters are ('A-Z','a-z','0-9','.','_'):${id}`; + const errorString = `The custom function or enum id contains invalid characters. Allowed characters are ('A-Z','a-z','0-9','.','_'):${id}`; extra.errors.push(logError(errorString, position)); } if (id.length > 128) { - const errorString = `The custom function id exceeds the maximum of 128 characters allowed.`; + const errorString = `The custom function or enum id exceeds the maximum of 128 characters allowed.`; extra.errors.push(logError(errorString, position)); } } @@ -532,209 +925,6 @@ function getOptions( return optionsItem; } -/** - * Determines the results parameter for the json - * @param func - Function - * @param isStreaming - Is a streaming function - * @param lastParameter - Last parameter of the function signature - */ -function getResults( - func: ts.FunctionDeclaration, - isStreamingFunction: boolean, - lastParameter: ts.ParameterDeclaration, - jsDocParamTypeInfo: { [key: string]: IJsDocParamType }, - extra: IFunctionExtras, - enumList: string[] -): IFunctionResult { - let resultType = "any"; - let resultDim = "scalar"; - const defaultResultItem: IFunctionResult = { - dimensionality: resultDim, - type: resultType, - }; - - const lastParameterPosition = getPosition(lastParameter); - - // Try and determine the return type. If one can't be determined we will set to any type - if (isStreamingFunction) { - const lastParameterType = lastParameter.type as ts.TypeReferenceNode; - if (!lastParameterType) { - // Need to get result type from param {type} - const name = (lastParameter.name as ts.Identifier).text; - const ptype = jsDocParamTypeInfo[name]; - // @ts-ignore - resultType = ptype.returnType; - resultDim = ptype.dimensionality; - const paramResultItem: IFunctionResult = { - dimensionality: resultDim, - type: resultType, - }; - - if (paramResultItem.dimensionality === "scalar") { - delete paramResultItem.dimensionality; - } - - return paramResultItem; - } - if (!lastParameterType.typeArguments || lastParameterType.typeArguments.length !== 1) { - const errorString = - "The 'CustomFunctions.StreamingHandler' needs to be passed in a single result type (e.g., 'CustomFunctions.StreamingHandler < number >') :"; - extra.errors.push(logError(errorString, lastParameterPosition)); - return defaultResultItem; - } - const returnType = func.type as ts.TypeReferenceNode; - if (returnType && returnType.getFullText().trim() !== "void") { - const errorString = `A streaming function should return 'void'. Use CustomFunctions.StreamingHandler.setResult() to set results.`; - extra.errors.push(logError(errorString, lastParameterPosition)); - return defaultResultItem; - } - resultType = getParamType(lastParameterType.typeArguments[0], extra, enumList); - resultDim = getParamDim(lastParameterType.typeArguments[0]); - } else if (func.type) { - if ( - func.type.kind === ts.SyntaxKind.TypeReference && - (func.type as ts.TypeReferenceNode).typeName.getText() === "Promise" && - (func.type as ts.TypeReferenceNode).typeArguments && - // @ts-ignore - (func.type as ts.TypeReferenceNode).typeArguments.length === 1 - ) { - resultType = getParamType( - // @ts-ignore - (func.type as ts.TypeReferenceNode).typeArguments[0], - extra, - enumList - ); - resultDim = getParamDim( - // @ts-ignore - (func.type as ts.TypeReferenceNode).typeArguments[0] - ); - } else { - resultType = getParamType(func.type, extra, enumList); - resultDim = getParamDim(func.type); - } - } - - // Check the code comments for @return parameter - const returnTypeFromJSDoc = ts.getJSDocReturnType(func); - if (returnTypeFromJSDoc) { - if (func.type && func.type.kind !== returnTypeFromJSDoc.kind) { - const name = (func.name as ts.Identifier).text; - const returnPosition = getPosition(returnTypeFromJSDoc); - const errorString = `Type {${ts.SyntaxKind[func.type.kind]}:${ - ts.SyntaxKind[returnTypeFromJSDoc.kind] - }} doesn't match for return type : ${name}`; - extra.errors.push(logError(errorString, returnPosition)); - } - if ( - returnTypeFromJSDoc.kind === ts.SyntaxKind.TypeReference && - (returnTypeFromJSDoc as ts.TypeReferenceNode).typeName.getText() === "Promise" && - (returnTypeFromJSDoc as ts.TypeReferenceNode).typeArguments && - // @ts-ignore - (returnTypeFromJSDoc as ts.TypeReferenceNode).typeArguments.length === 1 - ) { - resultType = getParamType( - // @ts-ignore - (returnTypeFromJSDoc as ts.TypeReferenceNode).typeArguments[0], - extra, - enumList - ); - resultDim = getParamDim( - // @ts-ignore - (returnTypeFromJSDoc as ts.TypeReferenceNode).typeArguments[0] - ); - } else { - resultType = getParamType(returnTypeFromJSDoc, extra, enumList); - resultDim = getParamDim(returnTypeFromJSDoc); - } - } - - const resultItem: IFunctionResult = { - dimensionality: resultDim, - type: resultType, - }; - - // We convert cell value types to "any". - if (Object.values(CELLVALUETYPE_MAPPINGS).includes(resultType)) { - resultType = "any"; - } - - // Only return dimensionality = matrix. Default assumed scalar - if (resultDim === "scalar") { - delete resultItem.dimensionality; - } - - if (resultType === "any") { - delete resultItem.type; - } - - return resultItem; -} - -/** - * Determines the parameter details for the json - * @param params - Parameters - * @param jsDocParamTypeInfo - jsDocs parameter type info - * @param jsDocParamInfo = jsDocs parameter info - */ -function getParameters(parameterItem: IGetParametersArguments): IFunctionParameter[] { - const parameterMetadata: IFunctionParameter[] = []; - parameterItem.parametersToParse - .map((p: ts.ParameterDeclaration) => { - const parameterPosition = getPosition(p); - // Get type node of parameter from typescript - let typeNode = p.type as ts.TypeNode; - const name = (p.name as ts.Identifier).text; - // Get type node of parameter from jsDocs - const parameterJSDocTypeNode = ts.getJSDocType(p); - if (parameterJSDocTypeNode && typeNode) { - if (parameterJSDocTypeNode.kind !== typeNode.kind) { - const errorString = `Type {${ts.SyntaxKind[parameterJSDocTypeNode.kind]}:${ - ts.SyntaxKind[typeNode.kind] - }} doesn't match for parameter : ${name}`; - parameterItem.extra.errors.push(logError(errorString, parameterPosition)); - } - } - if (!typeNode && parameterJSDocTypeNode) { - typeNode = parameterJSDocTypeNode; - } - const ptype = getParamType(typeNode, parameterItem.extra, parameterItem.enumList); - - const pMetadataItem: IFunctionParameter = { - description: parameterItem.jsDocParamInfo[name], - dimensionality: getParamDim(typeNode), - name, - optional: getParamOptional(p, parameterItem.jsDocParamOptionalInfo), - repeating: isRepeatingParameter(typeNode), - type: ptype, - }; - - // Only return dimensionality = matrix. Default assumed scalar - if (pMetadataItem.dimensionality === "scalar") { - delete pMetadataItem.dimensionality; - } - - // only include optional if true - if (!pMetadataItem.optional) { - delete pMetadataItem.optional; - } - - // only include description if it has a value - if (!pMetadataItem.description) { - delete pMetadataItem.description; - } - - // only return repeating if true and allowed - if (!pMetadataItem.repeating) { - delete pMetadataItem.repeating; - } - - parameterMetadata.push(pMetadataItem); - }) - .filter((meta) => meta); - - return parameterMetadata; -} - /** * Used to set repeating parameter true for 1d and 3d arrays * @param type Node to check @@ -808,6 +998,14 @@ function isCustomFunction(node: ts.Node): boolean { return hasTag(node, CUSTOM_FUNCTION); } +/** + * Returns true if node is a custom Enum + * @param node - jsDocs node + */ +function isCustomEnum(node: ts.Node): boolean { + return hasTag(node, CUSTOM_ENUM); +} + /** * Returns true if volatile tag found in comments * @param node jsDocs node @@ -1074,63 +1272,6 @@ function hasInvocationParameter( return typeRef.typeName.getText() === "CustomFunctions.Invocation"; } -/** - * Gets the parameter type of the node - * @param node TypeNode - */ -function getParamType(node: ts.TypeNode, extra: IFunctionExtras, enumList: string[]): string { - let type = "any"; - // Only get type for typescript files. js files will return "any" for all types - if (!node) { - return type; - } - const typePosition = getPosition(node); - - // Get the inner type node if it is an array - if (typeNodeIsArray(node)) { - let arrayType: IArrayType = { - dimensionality: 0, - node, - }; - arrayType = getArrayDimensionalityAndType(node); - node = arrayType.node; - } - - // We currently accept the following types of reference node: enum will be converted to "any", - // Excel.CellValue will be converted accordingly. Anything else is invalid. (Array reference node has already been covered above.) - if (ts.isTypeReferenceNode(node)) { - const typeReferenceNode = node as ts.TypeReferenceNode; - let nodeTypeName = typeReferenceNode.typeName.getText(); - if (enumList.indexOf(nodeTypeName) >= 0) { - // Type found in the enumList - return type; - } - // @ts-ignore - if (CELLVALUETYPE_MAPPINGS[nodeTypeName]) { - // @ts-ignore - let cellValue = CELLVALUETYPE_MAPPINGS[nodeTypeName]; - if (cellValue === "unsupported") { - const errorString = `Custom function does not support cell value type: ${typeReferenceNode.typeName.getText()}`; - extra.errors.push(logError(errorString, typePosition)); - return type; - } - return cellValue; - } - - const errorString = `Custom function does not support type "${typeReferenceNode.typeName.getText()}" as input or return parameter.`; - extra.errors.push(logError(errorString, typePosition)); - return type; - } - - // @ts-ignore - type = TYPE_MAPPINGS[node.kind]; - if (!type) { - extra.errors.push(logError("Type doesn't match mappings", typePosition)); - } - - return type; -} - /** * Helper function that checks whether a TypeNode is an array. * There are two cases: Array or sometype[]. diff --git a/packages/custom-functions-metadata/test/cases/enum/expected.json b/packages/custom-functions-metadata/test/cases/enum/expected.json new file mode 100644 index 000000000..2b1dc9e4f --- /dev/null +++ b/packages/custom-functions-metadata/test/cases/enum/expected.json @@ -0,0 +1,121 @@ +{ + "allowCustomDataForDataTypeAny": true, + "functions": [ + { + "description": "Test string enum", + "id": "TESTSTRINGENUM", + "name": "TESTSTRINGENUM", + "parameters": [ + { + "name": "first", + "type": "number" + }, + { + "description": "param of enum type planets", + "name": "second", + "type": "string", + "customEnumType": "PLANETS" + } + ], + "result": {} + }, + { + "description": "Test number enum", + "id": "TESTNUMBERENUM", + "name": "TESTNUMBERENUM", + "parameters": [ + { + "name": "first", + "type": "number" + }, + { + "description": "param of enum type numbers", + "name": "second", + "repeating": true, + "type": "number", + "customEnumType": "NUMBERS" + } + ], + "result": {} + } + ], + "enums": [ + { + "id": "PLANETS", + "type": "string", + "values": [ + { + "value": "mercuryvalue", + "description": "mercury", + "tooltip": "mercury is the first planet from the sun" + }, + { + "value": "venusvalue", + "description": "venus", + "tooltip": "venus is the second planet from the sun" + }, + { + "value": "earthvalue", + "description": "earth", + "tooltip": "earth is the third planet from the sun" + }, + { + "value": "marsvalue", + "description": "mars", + "tooltip": "mars is the fourth planet from the sun" + }, + { + "value": "jupitervalue", + "description": "jupiter", + "tooltip": "jupiter is the fifth planet from the sun" + }, + { + "value": "saturnvalue", + "description": "saturn", + "tooltip": "saturn is the sixth planet from the sun" + }, + { + "value": "uranusvalue", + "description": "uranus", + "tooltip": "uranus is the seventh planet from the sun" + }, + { + "value": "neptunevalue", + "description": "neptune", + "tooltip": "neptune is the eighth planet from the sun" + } + ] + }, + { + "id": "NUMBERS", + "type": "number", + "values": [ + { + "value": 1, + "description": "One", + "tooltip": "One" + }, + { + "value": 2, + "description": "Two", + "tooltip": "Two" + }, + { + "value": 3, + "description": "Three", + "tooltip": "Three" + }, + { + "value": 4, + "description": "Four", + "tooltip": "Four" + }, + { + "value": 5, + "description": "Five", + "tooltip": "Five" + } + ] + } + ] +} \ No newline at end of file diff --git a/packages/custom-functions-metadata/test/cases/enum/functions.ts b/packages/custom-functions-metadata/test/cases/enum/functions.ts new file mode 100644 index 000000000..80a35e21e --- /dev/null +++ b/packages/custom-functions-metadata/test/cases/enum/functions.ts @@ -0,0 +1,64 @@ +/** + * Enum for planets with descriptions and tooltips. + * @customenum + * @enum {string} + */ +enum PLANETS { + /** mercury is the first planet from the sun */ + mercury = "mercuryvalue", + /** venus is the second planet from the sun */ + venus = "venusvalue", + /** earth is the third planet from the sun */ + earth = "earthvalue", + /** mars is the fourth planet from the sun */ + mars = "marsvalue", + /** jupiter is the fifth planet from the sun */ + jupiter = "jupitervalue", + /** saturn is the sixth planet from the sun */ + saturn = "saturnvalue", + /** uranus is the seventh planet from the sun */ + uranus = "uranusvalue", + /** neptune is the eighth planet from the sun */ + neptune = "neptunevalue", +} + +/** + * Test string enum + * @customfunction + * @param first + * @param second param of enum type planets + * @returns + */ +export function testStringEnum(first: number, second: PLANETS): any { + return second; +} + +/** + * Enum for numbers with descriptions and tooltips. + * @customenum + * @enum {number} + */ +enum NUMBERS { + /** One */ + One = 1, + /** Two */ + Two = 2, + /** Three */ + Three = 3, + /** Four */ + Four = 4, + /** Five */ + Five = 5, +} + +/** + * Test number enum + * @customfunction + * @param first + * @param second param of enum type numbers + * @returns + */ +export function testNumberEnum(first: number, second: NUMBERS[]): any { + const sum = second.reduce((acc, num) => acc + num, 0); + return first + sum; +} diff --git a/packages/custom-functions-metadata/test/cases/error-enum-no-type/expected.ts.errors.txt b/packages/custom-functions-metadata/test/cases/error-enum-no-type/expected.ts.errors.txt new file mode 100644 index 000000000..ca69a527b --- /dev/null +++ b/packages/custom-functions-metadata/test/cases/error-enum-no-type/expected.ts.errors.txt @@ -0,0 +1 @@ +Enum PLANETS is missing @enum type annotation (1,1) \ No newline at end of file diff --git a/packages/custom-functions-metadata/test/cases/error-enum-no-type/functions.ts b/packages/custom-functions-metadata/test/cases/error-enum-no-type/functions.ts new file mode 100644 index 000000000..0a961da9c --- /dev/null +++ b/packages/custom-functions-metadata/test/cases/error-enum-no-type/functions.ts @@ -0,0 +1,22 @@ +/** + * Enum for planets with descriptions and tooltips. + * @customenum + */ +enum PLANETS { + /** mercury is the first planet from the sun */ + mercury = "mercuryvalue", + /** venus is the second planet from the sun */ + venus = "venusvalue", + /** earth is the third planet from the sun */ + earth = "earthvalue", + /** mars is the fourth planet from the sun */ + mars = "marsvalue", + /** jupiter is the fifth planet from the sun */ + jupiter = "jupitervalue", + /** saturn is the sixth planet from the sun */ + saturn = "saturnvalue", + /** uranus is the seventh planet from the sun */ + uranus = "uranusvalue", + /** neptune is the eighth planet from the sun */ + neptune = "neptunevalue", +} diff --git a/packages/custom-functions-metadata/test/cases/error-enum-wrong-type/expected.ts.errors.txt b/packages/custom-functions-metadata/test/cases/error-enum-wrong-type/expected.ts.errors.txt new file mode 100644 index 000000000..d29329ef9 --- /dev/null +++ b/packages/custom-functions-metadata/test/cases/error-enum-wrong-type/expected.ts.errors.txt @@ -0,0 +1,2 @@ +Enum type must match the enum type in annotation: PLANETS (1,1) +Enum value type must be consistent: NUMBERS (11,2) \ No newline at end of file diff --git a/packages/custom-functions-metadata/test/cases/error-enum-wrong-type/functions.ts b/packages/custom-functions-metadata/test/cases/error-enum-wrong-type/functions.ts new file mode 100644 index 000000000..9ebd65a16 --- /dev/null +++ b/packages/custom-functions-metadata/test/cases/error-enum-wrong-type/functions.ts @@ -0,0 +1,23 @@ +/** + * Enum for planets with descriptions and tooltips. + * @customenum + * @enum {string} + */ +enum PLANETS { + /** mercury is the first planet from the sun */ + mercury = 0, + /** venus is the second planet from the sun */ + venus = 1, +} + +/** + * Enum for numbers with descriptions and tooltips. + * @customenum + * @enum {number} + */ +enum NUMBERS { + /** One */ + One = 1, + /** Two */ + Two = "two", +} diff --git a/packages/custom-functions-metadata/test/cases/error-name-max-length/expected.js.errors.txt b/packages/custom-functions-metadata/test/cases/error-name-max-length/expected.js.errors.txt index fdcb69a56..c37acc918 100644 --- a/packages/custom-functions-metadata/test/cases/error-name-max-length/expected.js.errors.txt +++ b/packages/custom-functions-metadata/test/cases/error-name-max-length/expected.js.errors.txt @@ -1,2 +1,2 @@ -The custom function id exceeds the maximum of 128 characters allowed. (1,1) +The custom function or enum id exceeds the maximum of 128 characters allowed. (1,1) The custom function name is too long. It must be 128 characters or less. (1,1) \ No newline at end of file diff --git a/packages/custom-functions-metadata/test/cases/error-name-max-length/expected.ts.errors.txt b/packages/custom-functions-metadata/test/cases/error-name-max-length/expected.ts.errors.txt index fdcb69a56..c37acc918 100644 --- a/packages/custom-functions-metadata/test/cases/error-name-max-length/expected.ts.errors.txt +++ b/packages/custom-functions-metadata/test/cases/error-name-max-length/expected.ts.errors.txt @@ -1,2 +1,2 @@ -The custom function id exceeds the maximum of 128 characters allowed. (1,1) +The custom function or enum id exceeds the maximum of 128 characters allowed. (1,1) The custom function name is too long. It must be 128 characters or less. (1,1) \ No newline at end of file diff --git a/packages/custom-functions-metadata/test/cases/input-parameter-types-cellvalue/expected.json b/packages/custom-functions-metadata/test/cases/input-parameter-types-cellvalue/expected.json index 40c3a334f..f496bfcf0 100644 --- a/packages/custom-functions-metadata/test/cases/input-parameter-types-cellvalue/expected.json +++ b/packages/custom-functions-metadata/test/cases/input-parameter-types-cellvalue/expected.json @@ -8,7 +8,8 @@ "parameters": [ { "name": "x", - "type": "cellvalue" + "type": "any", + "cellValueType": "cellvalue" } ], "result": {} @@ -20,7 +21,8 @@ "parameters": [ { "name": "x", - "type": "booleancellvalue" + "type": "boolean", + "cellValueType": "booleancellvalue" } ], "result": {} @@ -32,7 +34,8 @@ "parameters": [ { "name": "x", - "type": "doublecellvalue" + "type": "number", + "cellValueType": "doublecellvalue" } ], "result": {} @@ -44,7 +47,8 @@ "parameters": [ { "name": "x", - "type": "entitycellvalue" + "type": "any", + "cellValueType": "entitycellvalue" } ], "result": {} @@ -56,7 +60,8 @@ "parameters": [ { "name": "x", - "type": "errorcellvalue" + "type": "any", + "cellValueType": "errorcellvalue" } ], "result": {} @@ -68,7 +73,8 @@ "parameters": [ { "name": "x", - "type": "formattednumbercellvalue" + "type": "number", + "cellValueType": "formattednumbercellvalue" } ], "result": {} @@ -80,7 +86,8 @@ "parameters": [ { "name": "x", - "type": "linkedentitycellvalue" + "type": "any", + "cellValueType": "linkedentitycellvalue" } ], "result": {} @@ -92,7 +99,8 @@ "parameters": [ { "name": "x", - "type": "localimagecellvalue" + "type": "any", + "cellValueType": "localimagecellvalue" } ], "result": {} @@ -104,7 +112,8 @@ "parameters": [ { "name": "x", - "type": "stringcellvalue" + "type": "string", + "cellValueType": "stringcellvalue" } ], "result": {} @@ -116,7 +125,8 @@ "parameters": [ { "name": "x", - "type": "webimagecellvalue" + "type": "any", + "cellValueType": "webimagecellvalue" } ], "result": {} @@ -129,7 +139,8 @@ { "name": "x", "repeating": true, - "type": "booleancellvalue" + "type": "boolean", + "cellValueType": "booleancellvalue" } ], "result": {} @@ -142,7 +153,8 @@ { "dimensionality": "matrix", "name": "x", - "type": "booleancellvalue" + "type": "boolean", + "cellValueType": "booleancellvalue" } ], "result": {} @@ -156,7 +168,8 @@ "dimensionality": "matrix", "name": "x", "repeating": true, - "type": "booleancellvalue" + "type": "boolean", + "cellValueType": "booleancellvalue" } ], "result": {} @@ -169,7 +182,8 @@ { "name": "x", "repeating": true, - "type": "booleancellvalue" + "type": "boolean", + "cellValueType": "booleancellvalue" } ], "result": {} @@ -182,7 +196,8 @@ { "dimensionality": "matrix", "name": "x", - "type": "booleancellvalue" + "type": "boolean", + "cellValueType": "booleancellvalue" } ], "result": {} @@ -196,7 +211,8 @@ "dimensionality": "matrix", "name": "x", "repeating": true, - "type": "booleancellvalue" + "type": "boolean", + "cellValueType": "booleancellvalue" } ], "result": {} diff --git a/packages/custom-functions-metadata/test/cases/return-type-cellvalue-array/functions.js b/packages/custom-functions-metadata/test/cases/return-type-cellvalue-array/functions.js index 7ab0ca5a5..691fec139 100644 --- a/packages/custom-functions-metadata/test/cases/return-type-cellvalue-array/functions.js +++ b/packages/custom-functions-metadata/test/cases/return-type-cellvalue-array/functions.js @@ -4,7 +4,7 @@ /** * Cell Value type in return type will convert to "any" (thus omitted). * @customfunction - * @returns {Excel.BooleanCellValue[][]} + * @returns {Excel.EntityCellValue[][]} */ function cellValueMatrixAsReturnType(x){ // empty diff --git a/packages/custom-functions-metadata/test/cases/return-type-cellvalue-array/functions.ts b/packages/custom-functions-metadata/test/cases/return-type-cellvalue-array/functions.ts index 9e1d7f782..d76e2e626 100644 --- a/packages/custom-functions-metadata/test/cases/return-type-cellvalue-array/functions.ts +++ b/packages/custom-functions-metadata/test/cases/return-type-cellvalue-array/functions.ts @@ -5,6 +5,6 @@ * Cell Value type in return type will convert to "any" (thus omitted). * @customfunction */ -function cellValueMatrixAsReturnType(x): Excel.BooleanCellValue[][] { +function cellValueMatrixAsReturnType(x): Excel.EntityCellValue[][] { // empty } \ No newline at end of file diff --git a/packages/custom-functions-metadata/test/cases/return-type-cellvalue/functions.js b/packages/custom-functions-metadata/test/cases/return-type-cellvalue/functions.js index 7e0bcfc6e..6cb5963c3 100644 --- a/packages/custom-functions-metadata/test/cases/return-type-cellvalue/functions.js +++ b/packages/custom-functions-metadata/test/cases/return-type-cellvalue/functions.js @@ -4,7 +4,7 @@ /** * Cell Value type in return type will convert to "any" (thus omitted). * @customfunction - * @returns {Excel.BooleanCellValue} + * @returns {Excel.EntityCellValue} */ function cellValueAsReturnType(x){ // empty diff --git a/packages/custom-functions-metadata/test/cases/return-type-cellvalue/functions.ts b/packages/custom-functions-metadata/test/cases/return-type-cellvalue/functions.ts index 2dac7a4f6..d24479b8c 100644 --- a/packages/custom-functions-metadata/test/cases/return-type-cellvalue/functions.ts +++ b/packages/custom-functions-metadata/test/cases/return-type-cellvalue/functions.ts @@ -5,6 +5,6 @@ * Cell Value type in return type will convert to "any" (thus omitted). * @customfunction */ -function cellValueAsReturnType(x): Excel.BooleanCellValue { +function cellValueAsReturnType(x): Excel.EntityCellValue { // empty } \ No newline at end of file diff --git a/packages/custom-functions-metadata/test/src/test.ts b/packages/custom-functions-metadata/test/src/test.ts index a27f297e2..5c1db696c 100644 --- a/packages/custom-functions-metadata/test/src/test.ts +++ b/packages/custom-functions-metadata/test/src/test.ts @@ -38,8 +38,8 @@ describe("test json output", function() { assert.strictEqual(j.functions[5].result.type, undefined, "void type - result type any not created properly"); assert.strictEqual(j.functions[6].parameters[0].type, "any", "object type - type any not created properly"); assert.strictEqual(j.functions[6].result.type, undefined, "object type - result type any not created properly"); - assert.strictEqual(j.functions[8].parameters[0].type, "any", "enum type - type any not created properly"); - assert.strictEqual(j.functions[8].result.type, undefined, "enum type - result type any not created properly"); + assert.strictEqual(j.functions[8].parameters[0].type, "string", "enum type - type any not created properly"); + assert.strictEqual(j.functions[8].result.type, "string", "enum type - result type any not created properly"); assert.strictEqual(j.functions[9].parameters[0].type, "any", "tuple type - type any not created properly"); assert.strictEqual(j.functions[9].result.type, undefined, "tuple type - result type any not created properly"); assert.strictEqual(j.functions[10].options.stream, true, "CustomFunctions.StreamingHandler - options stream not created properly"); diff --git a/packages/custom-functions-metadata/test/typescript/testfunctions.ts b/packages/custom-functions-metadata/test/typescript/testfunctions.ts index 1f89359b0..75eaabf14 100644 --- a/packages/custom-functions-metadata/test/typescript/testfunctions.ts +++ b/packages/custom-functions-metadata/test/typescript/testfunctions.ts @@ -79,6 +79,10 @@ function testdatetime(d: number): string { return ""; } +/** + * @customenum + * @enum {string} + */ enum Color {Red,Green,Blue}; /**