eslint-plugin-paired-calls
v1.0.0
Published
Enforce “paired” API calls (open/close, acquire/release, show/hide, start/stop) with optional safety requirements when explicit exceptions may occur between the pair.
Maintainers
Readme
eslint-plugin-paired-calls
Enforce “paired” API calls (open/close, acquire/release, show/hide, start/stop) with optional safety requirements when explicit exceptions may occur between the pair.
Features
- Configurable pairs (
open/close) - Supports
try/catch/finallyand Promise chains (.then/.catch/.finally) - No
try/finallyrequirement when there is no explicit exception between the pair - If an explicit exception exists between the pair:
- sync/async-await style: require
try/finallyOR close in bothtryandcatch - Promise style: require
.finally(...)OR close in boththenandcatch
- sync/async-await style: require
- Simple inter-procedural “call stack” closing detection (local functions), with configurable max depth
- Resource-style configuration via
"<resource>"placeholder (Java-like “FileStream must be closed” feel)
Install
pnpm add -D eslint eslint-plugin-paired-callsUsage
ESLint v9+ (flat config, eslint.config.js)
const pairedCalls = require("eslint-plugin-paired-calls");
module.exports = [
{
files: ["**/*.{js,ts}"],
plugins: { "paired-calls": pairedCalls },
rules: {
"paired-calls/enforce-paired-calls": [
"error",
{
pairs: [
{ open: "uni.showLoading", close: "uni.hideLoading" },
{ open: "lock.acquire", close: "lock.release", resource: "Lock" },
{
open: "fs.createReadStream",
close: "<resource>.close",
resource: "FileStream",
},
],
callGraph: { maxDepth: 8 },
},
],
},
},
];ESLint v8 (legacy .eslintrc.*)
module.exports = {
plugins: ["paired-calls"],
rules: {
"paired-calls/enforce-paired-calls": [
"error",
{
pairs: [{ open: "uni.showLoading", close: "uni.hideLoading" }],
},
],
},
};Rule: paired-calls/enforce-paired-calls
Options
type Options = {
pairs: Array<{
open: string; // e.g. "lock.acquire" or "uni.showLoading"
close: string; // e.g. "lock.release" or "<resource>.close"
resource?: string; // label used in diagnostics
}>;
callGraph?: {
maxDepth?: number; // default: 8
};
};Resource placeholder: "<resource>"
If close contains "<resource>", the rule will try to bind it to a variable assigned from the open call:
const s = fs.createReadStream("a.txt");
s.close(); // matches "<resource>.close"Exception model (explicit only)
This rule only treats the following as “explicit exceptions”:
throw ...Promise.reject(...)
If none of these appear between open/close in the same list of statements, the rule does not require try/finally / .finally.
Promise chains
When an explicit exception may occur between open/close inside a Promise chain, use either:
.finally(() => close())- close in both
.then(...)and.catch(...)(or.then(onFulfilled, onRejected)), so both success and error paths close the resource
try/catch/finally
When an explicit exception may occur between open/close in sync/async-await style code, use either:
try { ... } finally { close() }- close in both
tryandcatch
Inter-procedural (call stack) closing
If you call a local function and pass the resource variable as an argument, the rule will try to resolve the function body and see whether it (or nested calls) closes the resource, up to callGraph.maxDepth:
function cleanup(r) { r.close(); }
function f() {
const s = fs.createReadStream("a.txt");
cleanup(s); // treated as a close
}Limitations
- Best-effort static analysis; it does not build a full TypeScript type graph
- Inter-procedural analysis only follows locally declared functions (function declarations and function expressions assigned to identifiers)
- Explicit exceptions are currently limited to
throwandPromise.reject
License
MIT
