diff --git a/src/rules/plugin-activation-args.ts b/src/rules/plugin-activation-args.ts index 08e8552..e8e75d1 100644 --- a/src/rules/plugin-activation-args.ts +++ b/src/rules/plugin-activation-args.ts @@ -48,20 +48,22 @@ function extractFunctionInfo( | TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression | TSESTree.FunctionDeclaration -): ActivateFunctionInfo { - const params = fn.params.filter( - (param): param is TSESTree.Identifier => param.type === 'Identifier' - ); - - const paramNames = params.map(param => param.name); - - const paramTypes = params.map(param => - param.type === 'Identifier' ? extractParameterType(param) : null - ); - - const paramNodes = params.map(param => - param.type === 'Identifier' ? param : null - ); +): ActivateFunctionInfo | null { + const paramNames: string[] = []; + const paramTypes: (string | null)[] = []; + const paramNodes: (TSESTree.Identifier | null)[] = []; + + for (const param of fn.params) { + const id = param.type === 'AssignmentPattern' ? param.left : param; + if (id.type !== 'Identifier') { + // A destructured parameter has no name to check, and a rest parameter + // has no fixed position, so leave those functions alone. + return null; + } + paramNames.push(id.name); + paramTypes.push(extractParameterType(id)); + paramNodes.push(id); + } return { node, diff --git a/tests/jupyter-plugin-activation-args.test.ts b/tests/jupyter-plugin-activation-args.test.ts index 41ea761..52abc25 100644 --- a/tests/jupyter-plugin-activation-args.test.ts +++ b/tests/jupyter-plugin-activation-args.test.ts @@ -37,7 +37,33 @@ nonTypeAwareTester.run( 'plugin-activation-args (non-type-aware)', pluginActivationArgs, { - valid: [], + valid: [ + { + code: ` + const plugin: JupyterFrontEndPlugin = { + id: 'test-plugin', + optional: [ITranslator], + activate: (app: JupyterFrontEnd, translator: ITranslator | null = null) => {} + }; + ` + }, + { + code: ` + const plugin: JupyterFrontEndPlugin = { + id: 'test-plugin', + activate: ({ commands }: JupyterFrontEnd) => {} + }; + ` + }, + { + code: ` + const plugin: JupyterFrontEndPlugin = { + id: 'test-plugin', + activate: (app: JupyterFrontEnd, ...rest: unknown[]) => {} + }; + ` + } + ], invalid: [ // This same case is in valid cases for the type-aware tester { @@ -650,6 +676,105 @@ ruleTester.run('plugin-activation-args', pluginActivationArgs, { activate: (app: JupyterFrontEnd) => {} }; ` + }, + { + // Optional token with default null value + filename: 'tests/type-aware-fixture.ts', + code: ` + import { ITranslator } from './fixtures/types'; + const plugin: JupyterFrontEndPlugin = { + id: 'test-plugin', + optional: [ITranslator], + activate: ( + app: JupyterFrontEnd, + translator: ITranslator | null = null + ) => {} + }; + ` + }, + { + // Optional and required tokens with default values + filename: 'tests/type-aware-fixture.ts', + code: ` + import { INotebookTracker, ITranslator } from './fixtures/types'; + declare const defaultTracker: INotebookTracker; + const plugin: JupyterFrontEndPlugin = { + id: 'test-plugin', + requires: [INotebookTracker], + optional: [ITranslator], + activate: ( + app: JupyterFrontEnd, + tracker: INotebookTracker = defaultTracker, + translator: ITranslator | null = null + ) => {} + }; + ` + }, + { + // Method shorthand with default parameter + filename: 'tests/type-aware-fixture.ts', + code: ` + import { ITranslator } from './fixtures/types'; + const plugin: JupyterFrontEndPlugin = { + id: 'test-plugin', + optional: [ITranslator], + activate( + app: JupyterFrontEnd, + translator: ITranslator | null = null + ) {} + }; + ` + }, + { + // Referenced activate function with default parameter + filename: 'tests/type-aware-fixture.ts', + code: ` + import { ITranslator } from './fixtures/types'; + function activate( + app: JupyterFrontEnd, + translator: ITranslator | null = null + ): void {} + const plugin: JupyterFrontEndPlugin = { + id: 'test-plugin', + optional: [ITranslator], + activate + }; + ` + }, + { + // Destructured parameter in activate function is skipped + filename: 'tests/type-aware-fixture.ts', + code: ` + const plugin: JupyterFrontEndPlugin = { + id: 'test-plugin', + activate: ({ commands }: JupyterFrontEnd) => {} + }; + ` + }, + { + // Rest parameter in activate function is skipped + filename: 'tests/type-aware-fixture.ts', + code: ` + const plugin: JupyterFrontEndPlugin = { + id: 'test-plugin', + activate: (app: JupyterFrontEnd, ...rest: unknown[]) => {} + }; + ` + }, + { + // Destructured token parameter in activate function is skipped + filename: 'tests/type-aware-fixture.ts', + code: ` + import { INotebookTracker } from './fixtures/types'; + const plugin: JupyterFrontEndPlugin = { + id: 'test-plugin', + requires: [INotebookTracker], + activate: ( + app: JupyterFrontEnd, + { currentWidget }: INotebookTracker + ) => {} + }; + ` } ], @@ -1273,6 +1398,56 @@ ruleTester.run('plugin-activation-args', pluginActivationArgs, { data: { arg: 'invalidApp', allowedNames: '"app", "_app", "_"' } } ] + }, + { + // Optional token with non-nullable default value + filename: 'tests/type-aware-fixture.ts', + code: ` + import { ITranslator } from './fixtures/types'; + declare const fallbackTranslator: ITranslator; + const plugin: JupyterFrontEndPlugin = { + id: 'test-plugin', + optional: [ITranslator], + activate: ( + app: JupyterFrontEnd, + translator: ITranslator = fallbackTranslator + ) => {} + }; + `, + errors: [ + { + messageId: 'optionalNotNullable', + data: { arg: 'translator', type: 'ITranslator' } + } + ] + }, + { + // Tokens in wrong order with default parameters + filename: 'tests/type-aware-fixture.ts', + code: ` + import { INotebookTracker, IRenderMimeRegistry } from './fixtures/types'; + declare const defaultTracker: INotebookTracker; + declare const defaultRenderMime: IRenderMimeRegistry; + const plugin: JupyterFrontEndPlugin = { + id: 'test-plugin', + requires: [INotebookTracker, IRenderMimeRegistry], + activate: ( + app: JupyterFrontEnd, + rendermime: IRenderMimeRegistry = defaultRenderMime, + tracker: INotebookTracker = defaultTracker + ) => {} + }; + `, + errors: [ + { + messageId: 'mismatchedOrder', + data: { arg: 'rendermime' } + }, + { + messageId: 'mismatchedOrder', + data: { arg: 'tracker' } + } + ] } ] });