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
36 changes: 32 additions & 4 deletions lib/ast/getters/nodeParameter.getters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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)) {
Expand Down
2 changes: 1 addition & 1 deletion lib/ast/identifiers/common.identifiers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
13 changes: 10 additions & 3 deletions lib/ast/identifiers/nodeParameter.identifiers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import {
isArrayPropertyNamed,
isBooleanPropertyNamed,
isObjectPropertyNamed,
isIdentifierPropertyNamed,
isStringPropertyNamed,
} from "./common.identifiers";

Expand Down Expand Up @@ -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"
);
}

Expand Down Expand Up @@ -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 {
Expand Down
5 changes: 4 additions & 1 deletion lib/rules/node-param-default-missing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
20 changes: 13 additions & 7 deletions lib/rules/node-param-default-wrong-for-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

/**
Expand All @@ -53,13 +64,8 @@ export default utils.createRule({
return;
}

// @ts-ignore @TODO
const eligibleOptions = options.value.reduce<unknown[]>(
// @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)) {
Expand Down
2 changes: 2 additions & 0 deletions lib/rules/node-param-multi-options-type-unsorted-items.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions lib/rules/node-param-operation-option-action-miscased.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
2 changes: 2 additions & 0 deletions lib/rules/node-param-operation-option-without-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 2 additions & 0 deletions lib/rules/node-param-options-type-unsorted-items.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions lib/rules/node-param-resource-with-plural-option.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ export default utils.createRule({

if (!options) return;

if (options.hasPropertyPointingToIdentifier) return;

const pluralOption = findPluralOption(options);

if (pluralOption && !isAllowedPlural(pluralOption.value)) {
Expand Down
100 changes: 100 additions & 0 deletions tests/node-param-default-wrong-for-options.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
{
Expand Down