-
Notifications
You must be signed in to change notification settings - Fork 71
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
harden-exports rule for eslint plugin (#2369)
Refs: Agoric/agoric-sdk#9726 ## Description A new lint rule, `harden-exports`, to support Agoric/agoric-sdk#9726 Includes an autofixer ### Security Considerations Could enhance security ### Scaling Considerations n/a ### Documentation Considerations We don't yet document provided rules: https://endojs.github.io/endo/modules/_endo_eslint_plugin.html I think that's okay for now. If that is requested I'd file it as a separate issue, out of scope of this one. ### Testing Considerations I temporarily enabled this in the **recommended** config and ran `lint-fix` on the repo. All the changes looked correct. However `harden()` isn't always available so I don't think we should enable it in any of Endo's shared configs. ### Compatibility Considerations n/a ### Upgrade Considerations n/a
- Loading branch information
Showing
4 changed files
with
546 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
/** | ||
* @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', | ||
docs: { | ||
description: | ||
'Ensure each named export is followed by a call to `harden` function', | ||
category: 'Possible Errors', | ||
recommended: false, | ||
}, | ||
fixable: 'code', | ||
schema: [], | ||
}, | ||
/** | ||
* Create function for the rule. | ||
* @param {Rule.RuleContext} context - The rule context. | ||
* @returns {Object} The visitor object. | ||
*/ | ||
create(context) { | ||
/** @type {Array<ESTree.ExportNamedDeclaration & Rule.NodeParentExtension>} */ | ||
let exportNodes = []; | ||
|
||
return { | ||
/** @param {ESTree.ExportNamedDeclaration & Rule.NodeParentExtension} node */ | ||
ExportNamedDeclaration(node) { | ||
exportNodes.push(node); | ||
}, | ||
'Program:exit'() { | ||
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) { | ||
exportNames.push(prop.key.name); | ||
} | ||
} else { | ||
exportNames.push(declaration.id.name); | ||
} | ||
} | ||
} else if (exportNode.declaration.type === 'FunctionDeclaration') { | ||
// @ts-expect-error xxx typedef | ||
exportNames.push(exportNode.declaration.id.name); | ||
} | ||
} else if (exportNode.specifiers) { | ||
for (const spec of exportNode.specifiers) { | ||
exportNames.push(spec.exported.name); | ||
} | ||
} | ||
|
||
const missingHardenCalls = []; | ||
for (const exportName of exportNames) { | ||
const hasHardenCall = sourceCode.ast.body.some(statement => { | ||
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 | ||
); | ||
}); | ||
|
||
if (!hasHardenCall) { | ||
missingHardenCalls.push(exportName); | ||
} | ||
} | ||
|
||
if (missingHardenCalls.length > 0) { | ||
const noun = missingHardenCalls.length === 1 ? 'export' : 'exports'; | ||
context.report({ | ||
node: exportNode, | ||
message: `The named ${noun} '${missingHardenCalls.join(', ')}' should be followed by a call to 'harden'.`, | ||
fix: function (fixer) { | ||
const hardenCalls = missingHardenCalls | ||
.map(name => `harden(${name});`) | ||
.join('\n'); | ||
return fixer.insertTextAfter(exportNode, `\n${hardenCalls}`); | ||
}, | ||
}); | ||
} | ||
} | ||
}, | ||
}; | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,250 @@ | ||
const { RuleTester } = require('eslint'); | ||
const rule = require('../lib/rules/harden-exports'); | ||
|
||
const jsValid = [ | ||
{ | ||
code: ` | ||
export const a = 1; | ||
harden(a); | ||
export const b = 2; | ||
harden(b); | ||
`, | ||
}, | ||
{ | ||
code: ` | ||
export const a = 1; | ||
harden(a); | ||
export const b = 2; | ||
harden(b); | ||
`, | ||
}, | ||
{ | ||
code: ` | ||
export function foo() { | ||
console.log("foo"); | ||
} | ||
harden(foo); | ||
export const a = 1; | ||
harden(a); | ||
`, | ||
}, | ||
{ | ||
code: ` | ||
export const a = 1; | ||
harden(a); | ||
export function bar() { | ||
console.log("bar"); | ||
} | ||
harden(bar); | ||
`, | ||
}, | ||
{ | ||
code: ` | ||
export const a = 1; | ||
harden(a); | ||
export function | ||
multilineFunction() { | ||
console.log("This is a multiline function."); | ||
} | ||
harden(multilineFunction); | ||
`, | ||
}, | ||
{ | ||
code: ` | ||
export const { | ||
getEnvironmentOption, | ||
getEnvironmentOptionsList, | ||
environmentOptionsListHas, | ||
} = makeEnvironmentCaptor(); | ||
harden(getEnvironmentOption); | ||
harden(getEnvironmentOptionsList); | ||
harden(environmentOptionsListHas); | ||
`, | ||
}, | ||
]; | ||
|
||
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: ` | ||
export const a = 'alreadyHardened'; | ||
export const b = 'toHarden'; | ||
harden(b); | ||
harden(a); | ||
`, | ||
}, | ||
{ | ||
code: ` | ||
export const a = 1; | ||
`, | ||
errors: [ | ||
{ | ||
message: | ||
"The named export 'a' should be followed by a call to 'harden'.", | ||
}, | ||
], | ||
output: ` | ||
export const a = 1; | ||
harden(a); | ||
`, | ||
}, | ||
{ | ||
code: ` | ||
export function foo() { | ||
console.log("foo"); | ||
} | ||
`, | ||
errors: [ | ||
{ | ||
message: | ||
"The named export 'foo' should be followed by a call to 'harden'.", | ||
}, | ||
], | ||
output: ` | ||
export function foo() { | ||
console.log("foo"); | ||
} | ||
harden(foo); | ||
`, | ||
}, | ||
{ | ||
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: ` | ||
export function | ||
multilineFunction() { | ||
console.log("This is a multiline function."); | ||
} | ||
harden(multilineFunction); | ||
`, | ||
}, | ||
{ | ||
code: ` | ||
export const a = 1; | ||
export const b = 2; | ||
export const alreadyHardened = 3; | ||
harden(alreadyHardened); | ||
export function 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: ` | ||
export const a = 1; | ||
harden(a); | ||
export const b = 2; | ||
harden(b); | ||
export const alreadyHardened = 3; | ||
harden(alreadyHardened); | ||
export function foo() { | ||
console.log("foo"); | ||
} | ||
harden(foo); | ||
export function | ||
multilineFunction() { | ||
console.log("This is a multiline function."); | ||
} | ||
harden(multilineFunction); | ||
`, | ||
}, | ||
{ | ||
code: ` | ||
export const { | ||
getEnvironmentOption, | ||
getEnvironmentOptionsList, | ||
environmentOptionsListHas, | ||
} = makeEnvironmentCaptor(); | ||
`, | ||
errors: [ | ||
{ | ||
message: | ||
"The named exports 'getEnvironmentOption, getEnvironmentOptionsList, environmentOptionsListHas' should be followed by a call to 'harden'.", | ||
}, | ||
], | ||
output: ` | ||
export const { | ||
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, | ||
}); |
Oops, something went wrong.