diff --git a/lib/ast/getters/nodeParameter.getters.ts b/lib/ast/getters/nodeParameter.getters.ts index 50482f4..bde0615 100644 --- a/lib/ast/getters/nodeParameter.getters.ts +++ b/lib/ast/getters/nodeParameter.getters.ts @@ -119,7 +119,11 @@ export function getOptions(nodeParam: TSESTree.ObjectExpression) { if (!found) return null; - if (!found.value.elements) { + // Only process ArrayExpression as literal options, treat everything else as non-literal + if ( + found.value.type !== AST_NODE_TYPES.ArrayExpression || + !found.value.elements + ) { return { ast: found, value: [{ name: "", value: "", description: "", action: "" }], // unused placeholder @@ -128,10 +132,17 @@ export function getOptions(nodeParam: TSESTree.ObjectExpression) { } const elements = found.value.elements.filter( - (i) => i.type === "ObjectExpression" + (i) => i?.type === "ObjectExpression" ); - if (!elements.length) return null; + if (!elements.length) { + // Array exists but contains non-object elements (e.g., variables), treat as non-literal + return { + ast: found, + value: [{ name: "", value: "", description: "", action: "" }], // unused placeholder + hasPropertyPointingToIdentifier: true, + }; + } if (hasMemberExpression(elements)) { return { @@ -162,7 +173,7 @@ export function getCollectionOptions(nodeParam: TSESTree.ObjectExpression) { } const elements = found.value.elements.filter( - (i) => i.type === "ObjectExpression" + (i) => i?.type === "ObjectExpression" ); if (!elements.length) return null; @@ -250,6 +261,23 @@ export function getLoadOptionsMethod(nodeParam: TSESTree.ObjectExpression) { }; } +export function getLoadOptions(nodeParam: TSESTree.ObjectExpression) { + const typeOptions = getTypeOptions(nodeParam); + + if (!typeOptions) return null; + + const { properties } = typeOptions.ast.value; + + const found = properties.find(id.nodeParam.isLoadOptions); + + if (!found) return null; + + return { + ast: found, + value: found.value, // Object value, not string like loadOptionsMethod + }; +} + export function getDescription(nodeParam: TSESTree.ObjectExpression) { for (const property of nodeParam.properties) { if (id.nodeParam.isDescription(property)) { diff --git a/lib/ast/identifiers/common.identifiers.ts b/lib/ast/identifiers/common.identifiers.ts index 2194acb..9bdcddc 100644 --- a/lib/ast/identifiers/common.identifiers.ts +++ b/lib/ast/identifiers/common.identifiers.ts @@ -87,7 +87,7 @@ export function isBooleanPropertyNamed( * Check whether the property has a specific key name and points to an `ObjectExpression`. */ export function isObjectPropertyNamed( - keyName: "displayOptions" | "typeOptions" | "show" | "default" | "defaults", + keyName: "displayOptions" | "typeOptions" | "show" | "default" | "defaults" | "loadOptions", property: TSESTree.ObjectLiteralElement ) { return isTargetProperty({ keyName, valueType: "object" }, property); diff --git a/lib/ast/identifiers/nodeParameter.identifiers.ts b/lib/ast/identifiers/nodeParameter.identifiers.ts index c626515..9bb4aa8 100644 --- a/lib/ast/identifiers/nodeParameter.identifiers.ts +++ b/lib/ast/identifiers/nodeParameter.identifiers.ts @@ -15,7 +15,6 @@ import { isArrayPropertyNamed, isBooleanPropertyNamed, isObjectPropertyNamed, - isIdentifierPropertyNamed, isStringPropertyNamed, } from "./common.identifiers"; @@ -263,8 +262,10 @@ export function isOptions( property: TSESTree.ObjectLiteralElement ): property is OptionsProperty { return ( - isArrayPropertyNamed("options", property) || - isIdentifierPropertyNamed("options", property) + property.type === AST_NODE_TYPES.Property && + property.computed === false && + property.key.type === AST_NODE_TYPES.Identifier && + property.key.name === "options" ); } @@ -309,6 +310,12 @@ export function isLoadOptionsMethod( return isStringPropertyNamed("loadOptionsMethod", property); } +export function isLoadOptions( + property: TSESTree.ObjectLiteralElement +): property is ObjectProperty { + return isObjectPropertyNamed("loadOptions", property); +} + export function isDescription( property: TSESTree.ObjectLiteralElement ): property is StringProperty { diff --git a/lib/rules/node-param-default-missing.ts b/lib/rules/node-param-default-missing.ts index 4171312..0560f3e 100644 --- a/lib/rules/node-param-default-missing.ts +++ b/lib/rules/node-param-default-missing.ts @@ -87,5 +87,8 @@ function getDefaultForOptionsTypeParam(node: TSESTree.ObjectExpression) { function getZerothOption(nodeParamArg: TSESTree.ObjectExpression) { if (!id.nodeParam.isOptionsType(nodeParamArg)) return null; - return getters.nodeParam.getOptions(nodeParamArg)?.value[0] ?? null; + const options = getters.nodeParam.getOptions(nodeParamArg); + if (!options || options.hasPropertyPointingToIdentifier) return null; + + return options.value[0] ?? null; } diff --git a/lib/rules/node-param-default-wrong-for-options.ts b/lib/rules/node-param-default-wrong-for-options.ts index 2910a60..3f5bb2f 100644 --- a/lib/rules/node-param-default-wrong-for-options.ts +++ b/lib/rules/node-param-default-wrong-for-options.ts @@ -30,6 +30,17 @@ export default utils.createRule({ if (!_default) return; + // If typeOptions.loadOptionsMethod or typeOptions.loadOptions is present, + // options are loaded dynamically and any default should be allowed + const loadOptionsMethod = getters.nodeParam.getLoadOptionsMethod(node); + const loadOptions = getters.nodeParam.getLoadOptions(node); + if (loadOptionsMethod || loadOptions) return; + + // If default is a variable or expression (not a literal), allow it + if (_default.isUnparseable) { + return; + } + const options = getters.nodeParam.getOptions(node); /** @@ -53,13 +64,8 @@ export default utils.createRule({ return; } - // @ts-ignore @TODO - const eligibleOptions = options.value.reduce( - // @ts-ignore @TODO - (acc, option) => { - return acc.push(option.value), acc; - }, - [] + const eligibleOptions: unknown[] = options.value.map( + (option) => option.value ); if (!eligibleOptions.includes(_default.value)) { diff --git a/lib/rules/node-param-multi-options-type-unsorted-items.ts b/lib/rules/node-param-multi-options-type-unsorted-items.ts index 37d7b13..e0befbc 100644 --- a/lib/rules/node-param-multi-options-type-unsorted-items.ts +++ b/lib/rules/node-param-multi-options-type-unsorted-items.ts @@ -33,6 +33,8 @@ export default utils.createRule({ if (!optionsNode) return; + if (optionsNode.hasPropertyPointingToIdentifier) return; + if (optionsNode.value.length < MIN_ITEMS_TO_ALPHABETIZE) return; if (/^\d+$/.test(optionsNode.value[0].value)) return; // do not sort numeric strings diff --git a/lib/rules/node-param-operation-option-action-miscased.ts b/lib/rules/node-param-operation-option-action-miscased.ts index 0d9bd11..2e43dcd 100644 --- a/lib/rules/node-param-operation-option-action-miscased.ts +++ b/lib/rules/node-param-operation-option-action-miscased.ts @@ -33,6 +33,8 @@ export default utils.createRule({ if (!options) return; + if (options.hasPropertyPointingToIdentifier) return; + // skip `options: [...].sort()`, see EditImage.node.ts if (!Array.isArray(options.ast.value.elements)) return; diff --git a/lib/rules/node-param-operation-option-action-wrong-for-get-many.ts b/lib/rules/node-param-operation-option-action-wrong-for-get-many.ts index 15ae8d3..49bfeae 100644 --- a/lib/rules/node-param-operation-option-action-wrong-for-get-many.ts +++ b/lib/rules/node-param-operation-option-action-wrong-for-get-many.ts @@ -30,6 +30,8 @@ export default utils.createRule({ if (!options) return; + if (options.hasPropertyPointingToIdentifier) return; + // skip `options: [...].sort()`, see EditImage.node.ts if (!Array.isArray(options.ast.value.elements)) return; diff --git a/lib/rules/node-param-operation-option-description-wrong-for-get-many.ts b/lib/rules/node-param-operation-option-description-wrong-for-get-many.ts index 882b022..22cdd8b 100644 --- a/lib/rules/node-param-operation-option-description-wrong-for-get-many.ts +++ b/lib/rules/node-param-operation-option-description-wrong-for-get-many.ts @@ -28,6 +28,8 @@ export default utils.createRule({ if (!options) return; + if (options.hasPropertyPointingToIdentifier) return; + // skip `options: [...].sort()`, see EditImage.node.ts if (!Array.isArray(options.ast.value.elements)) return; diff --git a/lib/rules/node-param-operation-option-without-action.ts b/lib/rules/node-param-operation-option-without-action.ts index a49353b..25c3b7c 100644 --- a/lib/rules/node-param-operation-option-without-action.ts +++ b/lib/rules/node-param-operation-option-without-action.ts @@ -34,6 +34,8 @@ export default utils.createRule({ if (!options) return; + if (options.hasPropertyPointingToIdentifier) return; + if (allOptionsHaveActions(options)) return; // quick check via `value` for (const option of options.ast.value.elements) { diff --git a/lib/rules/node-param-options-type-unsorted-items.ts b/lib/rules/node-param-options-type-unsorted-items.ts index f6ccd11..3deb9b8 100644 --- a/lib/rules/node-param-options-type-unsorted-items.ts +++ b/lib/rules/node-param-options-type-unsorted-items.ts @@ -32,6 +32,8 @@ export default utils.createRule({ if (!optionsNode) return; + if (optionsNode.hasPropertyPointingToIdentifier) return; + if (optionsNode.value.length < MIN_ITEMS_TO_ALPHABETIZE) return; if (/^\d+$/.test(optionsNode.value[0].value)) return; // do not sort numeric strings diff --git a/lib/rules/node-param-resource-with-plural-option.ts b/lib/rules/node-param-resource-with-plural-option.ts index 1f5ca0b..ad32201 100644 --- a/lib/rules/node-param-resource-with-plural-option.ts +++ b/lib/rules/node-param-resource-with-plural-option.ts @@ -32,6 +32,8 @@ export default utils.createRule({ if (!options) return; + if (options.hasPropertyPointingToIdentifier) return; + const pluralOption = findPluralOption(options); if (pluralOption && !isAllowedPlural(pluralOption.value)) { diff --git a/tests/node-param-default-wrong-for-options.test.ts b/tests/node-param-default-wrong-for-options.test.ts index 6bf8eb1..9790a71 100644 --- a/tests/node-param-default-wrong-for-options.test.ts +++ b/tests/node-param-default-wrong-for-options.test.ts @@ -55,6 +55,106 @@ ruleTester().run(getRuleName(module), rule, { ], };`, }, + { + code: outdent` + const test = { + displayName: "Test", + name: "test", + type: "options", + default: "any_value", + typeOptions: { + loadOptionsMethod: "getFields", + }, + };`, + }, + { + code: outdent` + const test = { + displayName: "Model", + name: "model", + type: "options", + default: "gpt-3.5-turbo-1106", + typeOptions: { + loadOptions: { + routing: { + request: { + method: "GET", + url: "=/v1/models", + }, + }, + }, + }, + };`, + }, + { + code: outdent` + const myVariable = "first"; + const test = { + displayName: "Test", + name: "test", + type: "options", + default: myVariable, + options: [ + { + name: 'First', + value: 'first', + }, + { + name: 'Second', + value: 'second', + }, + ], + };`, + }, + { + code: outdent` + const test = { + displayName: "Test", + name: "test", + type: "options", + default: myObject.getValue(), + typeOptions: { + loadOptionsMethod: "getFields", + }, + };`, + }, + { + code: outdent` + const currencies = [{name: 'USD', value: 'USD'}, {name: 'EUR', value: 'EUR'}]; + const test = { + displayName: 'Currency', + name: 'currency', + type: 'options', + default: 'USD', + options: currencies.sort((a, b) => a.name.localeCompare(b.name)), + };`, + }, + { + code: outdent` + const config = { + currencyOptions: [{name: 'USD', value: 'USD'}, {name: 'EUR', value: 'EUR'}] + }; + const test = { + displayName: 'Currency', + name: 'currency', + type: 'options', + default: 'EUR', + options: config.currencyOptions, + };`, + }, + { + code: outdent` + const lastNodeResponseMode = {name: 'Last Node', value: 'lastNode'}; + const respondToWebhookResponseMode = {name: 'Respond to Webhook', value: 'respondToWebhook'}; + const test = { + displayName: 'Response Mode', + name: 'responseMode', + type: 'options', + options: [lastNodeResponseMode, respondToWebhookResponseMode], + default: 'lastNode', + description: 'When and how to respond to the webhook', + };`, + }, ], invalid: [ {