From dde94f22b831eec0a200197552bb287ddda5cf3f Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Thu, 18 Jul 2024 13:35:04 -0700 Subject: [PATCH] feat(harden-exports): handle TypeScript --- .../eslint-plugin/lib/rules/harden-exports.js | 33 +- .../eslint-plugin/test/harden-exports.test.js | 326 ++++++++++-------- 2 files changed, 205 insertions(+), 154 deletions(-) diff --git a/packages/eslint-plugin/lib/rules/harden-exports.js b/packages/eslint-plugin/lib/rules/harden-exports.js index dc34392e2d..373195029f 100644 --- a/packages/eslint-plugin/lib/rules/harden-exports.js +++ b/packages/eslint-plugin/lib/rules/harden-exports.js @@ -1,3 +1,18 @@ +/** + * @fileoverview Ensure each named export is followed by a call to `harden` function + */ + +'use strict'; + +/** + * @import {Rule} from 'eslint'; + * @import * as ESTree from 'estree'; + */ + +/** + * ESLint rule module for ensuring each named export is followed by a call to `harden` function. + * @type {Rule.RuleModule} + */ module.exports = { meta: { type: 'problem', @@ -10,10 +25,17 @@ module.exports = { fixable: 'code', schema: [], }, - create: function (context) { + /** + * Create function for the rule. + * @param {Rule.RuleContext} context - The rule context. + * @returns {Object} The visitor object. + */ + create(context) { + /** @type {Array} */ let exportNodes = []; return { + /** @param {ESTree.ExportNamedDeclaration & Rule.NodeParentExtension} node */ ExportNamedDeclaration(node) { exportNodes.push(node); }, @@ -21,9 +43,12 @@ module.exports = { const sourceCode = context.getSourceCode(); for (const exportNode of exportNodes) { + /** @type {string[]} */ let exportNames = []; if (exportNode.declaration) { + // @ts-expect-error xxx typedef if (exportNode.declaration.declarations) { + // @ts-expect-error xxx typedef for (const declaration of exportNode.declaration.declarations) { if (declaration.id.type === 'ObjectPattern') { for (const prop of declaration.id.properties) { @@ -33,8 +58,8 @@ module.exports = { exportNames.push(declaration.id.name); } } - } else { - // Handling function exports + } else if (exportNode.declaration.type === 'FunctionDeclaration') { + // @ts-expect-error xxx typedef exportNames.push(exportNode.declaration.id.name); } } else if (exportNode.specifiers) { @@ -49,8 +74,10 @@ module.exports = { return ( statement.type === 'ExpressionStatement' && statement.expression.type === 'CallExpression' && + // @ts-expect-error xxx typedef statement.expression.callee.name === 'harden' && statement.expression.arguments.length === 1 && + // @ts-expect-error xxx typedef statement.expression.arguments[0].name === exportName ); }); diff --git a/packages/eslint-plugin/test/harden-exports.test.js b/packages/eslint-plugin/test/harden-exports.test.js index 308072f0f6..7aa833ea14 100644 --- a/packages/eslint-plugin/test/harden-exports.test.js +++ b/packages/eslint-plugin/test/harden-exports.test.js @@ -1,150 +1,147 @@ const { RuleTester } = require('eslint'); const rule = require('../lib/rules/harden-exports'); -const ruleTester = new RuleTester({ - parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, -}); -ruleTester.run('harden-exports', rule, { - valid: [ - { - code: ` +const jsValid = [ + { + code: ` export const a = 1; harden(a); export const b = 2; harden(b); - `, - }, - { - code: ` + `, + }, + { + code: ` export const a = 1; harden(a); export const b = 2; harden(b); - `, - }, - { - code: ` + `, + }, + { + code: ` export function foo() { - console.log("foo"); - } + console.log("foo"); + } harden(foo); export const a = 1; harden(a); - `, - }, - { - code: ` + `, + }, + { + code: ` export const a = 1; harden(a); export function bar() { - console.log("bar"); - } + console.log("bar"); + } harden(bar); - `, - }, - { - code: ` + `, + }, + { + code: ` export const a = 1; harden(a); export function - multilineFunction() { - console.log("This is a multiline function."); - } + multilineFunction() { + console.log("This is a multiline function."); + } harden(multilineFunction); - `, - }, - { - code: ` + `, + }, + { + code: ` export const { - getEnvironmentOption, - getEnvironmentOptionsList, - environmentOptionsListHas, - } = makeEnvironmentCaptor(); + getEnvironmentOption, + getEnvironmentOptionsList, + environmentOptionsListHas, + } = makeEnvironmentCaptor(); harden(getEnvironmentOption); harden(getEnvironmentOptionsList); harden(environmentOptionsListHas); - `, - }, - ], - invalid: [ - { - code: ` + `, + }, +]; + +const invalid = [ + { + code: ` export const a = 'alreadyHardened'; export const b = 'toHarden'; harden(a); - `, - errors: [ - { - message: - "The named export 'b' should be followed by a call to 'harden'.", - }, - ], - output: ` + `, + errors: [ + { + message: + "The named export 'b' should be followed by a call to 'harden'.", + }, + ], + output: ` export const a = 'alreadyHardened'; export const b = 'toHarden'; harden(b); harden(a); - `, - }, - { - code: ` + `, + }, + { + code: ` export const a = 1; - `, - errors: [ - { - message: - "The named export 'a' should be followed by a call to 'harden'.", - }, - ], - output: ` + `, + errors: [ + { + message: + "The named export 'a' should be followed by a call to 'harden'.", + }, + ], + output: ` export const a = 1; harden(a); - `, - }, - { - code: ` + `, + }, + { + code: ` export function foo() { - console.log("foo"); - } - `, - errors: [ - { - message: - "The named export 'foo' should be followed by a call to 'harden'.", - }, - ], - output: ` + console.log("foo"); + } + `, + errors: [ + { + message: + "The named export 'foo' should be followed by a call to 'harden'.", + }, + ], + output: ` export function foo() { - console.log("foo"); - } + console.log("foo"); + } harden(foo); - `, - }, - { - code: ` + `, + }, + { + code: ` export function - multilineFunction() { - console.log("This is a multiline function."); - } - `, - errors: [ - { - message: - "The named export 'multilineFunction' should be followed by a call to 'harden'.", - }, - ], - output: ` + multilineFunction() { + console.log("This is a multiline function."); + } + `, + errors: [ + { + message: + "The named export 'multilineFunction' should be followed by a call to 'harden'.", + }, + ], + output: ` export function - multilineFunction() { - console.log("This is a multiline function."); - } + multilineFunction() { + console.log("This is a multiline function."); + } harden(multilineFunction); - `, - }, - { - code: ` + `, + }, + { + code: ` export const a = 1; export const b = 2; @@ -152,32 +149,32 @@ export const alreadyHardened = 3; harden(alreadyHardened); export function foo() { - console.log("foo"); - } + console.log("foo"); + } export function - multilineFunction() { - console.log("This is a multiline function."); - } - `, - errors: [ - { - message: - "The named export 'a' should be followed by a call to 'harden'.", - }, - { - message: - "The named export 'b' should be followed by a call to 'harden'.", - }, - { - message: - "The named export 'foo' should be followed by a call to 'harden'.", - }, - { - message: - "The named export 'multilineFunction' should be followed by a call to 'harden'.", - }, - ], - output: ` + multilineFunction() { + console.log("This is a multiline function."); + } + `, + errors: [ + { + message: + "The named export 'a' should be followed by a call to 'harden'.", + }, + { + message: + "The named export 'b' should be followed by a call to 'harden'.", + }, + { + message: + "The named export 'foo' should be followed by a call to 'harden'.", + }, + { + message: + "The named export 'multilineFunction' should be followed by a call to 'harden'.", + }, + ], + output: ` export const a = 1; harden(a); export const b = 2; @@ -187,40 +184,67 @@ export const alreadyHardened = 3; harden(alreadyHardened); export function foo() { - console.log("foo"); - } + console.log("foo"); + } harden(foo); export function - multilineFunction() { - console.log("This is a multiline function."); - } + multilineFunction() { + console.log("This is a multiline function."); + } harden(multilineFunction); - `, - }, - { - code: ` + `, + }, + { + code: ` export const { - getEnvironmentOption, - getEnvironmentOptionsList, - environmentOptionsListHas, +getEnvironmentOption, +getEnvironmentOptionsList, +environmentOptionsListHas, } = makeEnvironmentCaptor(); - `, - errors: [ - { - message: - "The named exports 'getEnvironmentOption, getEnvironmentOptionsList, environmentOptionsListHas' should be followed by a call to 'harden'.", - }, - ], - output: ` + `, + errors: [ + { + message: + "The named exports 'getEnvironmentOption, getEnvironmentOptionsList, environmentOptionsListHas' should be followed by a call to 'harden'.", + }, + ], + output: ` export const { - getEnvironmentOption, - getEnvironmentOptionsList, - environmentOptionsListHas, +getEnvironmentOption, +getEnvironmentOptionsList, +environmentOptionsListHas, } = makeEnvironmentCaptor(); harden(getEnvironmentOption); harden(getEnvironmentOptionsList); harden(environmentOptionsListHas); - `, + `, + }, +]; + +const jsTester = new RuleTester({ + parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, +}); +jsTester.run('harden JS exports', rule, { + valid: jsValid, + invalid, +}); + +const tsTester = new RuleTester({ + parser: require.resolve('@typescript-eslint/parser'), + parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, +}); +tsTester.run('harden TS exports', rule, { + valid: [ + ...jsValid, + { + // harden() on only value exports + code: ` +export type Foo = string; +export interface Bar { + baz: number; +} + `, }, ], + invalid, });