Skip to content

Commit

Permalink
feat(detect-non-literal-fs-filename): change to track non-top-level `…
Browse files Browse the repository at this point in the history
…require()` as well (#105)

* feat(detect-non-literal-fs-filename): change to track non-top-level require as well

* move files

* using linter instance for test

* revert
  • Loading branch information
ota-meshi committed Jan 11, 2023
1 parent 7d482c5 commit d3b1543
Show file tree
Hide file tree
Showing 8 changed files with 436 additions and 295 deletions.
11 changes: 10 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,14 @@
],
"eslint-plugin/require-meta-schema": "off", // TODO: enable
"eslint-plugin/require-meta-type": "off" // TODO: enable
}
},
"overrides": [
{
"files": ["test/**/*.js"],
"globals": {
"describe": "readonly",
"it": "readonly"
}
}
]
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"lint:js": "eslint .",
"lint:js:fix": "npm run lint:js -- --fix",
"release": "npx semantic-release",
"test": "mocha test/**/*",
"test": "mocha test/**",
"update:eslint-docs": "eslint-doc-generator"
},
"repository": {
Expand Down
296 changes: 44 additions & 252 deletions rules/detect-non-literal-fs-filename.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@

'use strict';

const fsMetaData = require('./data/fsFunctionData.json');
const fsMetaData = require('../utils/data/fsFunctionData.json');
const funcNames = Object.keys(fsMetaData);
const fsPackageNames = ['fs', 'node:fs', 'fs/promises', 'node:fs/promises', 'fs-extra'];

const { getImportDeclaration, getVariableDeclaration } = require('./utils/import-utils');
const { getImportAccessPath } = require('../utils/import-utils');

//------------------------------------------------------------------------------
// Utils
Expand All @@ -21,189 +21,9 @@ function getIndices(node, argMeta) {

function generateReport({ context, node, packageName, methodName, indices }) {
if (!indices || indices.length === 0) {
return null;
return;
}
return context.report({ node, message: `Found ${methodName} from package "${packageName}" with non literal argument at index ${indices.join(',')}` });
}

/**
* Detects:
* | var something = require('fs').readFile;
* | something(a);
*/
function detectOnRequireWithProperty({ context, methodName, node, program }) {
const declaration = getVariableDeclaration({
condition: (declaration) => declaration.init.parent.id.name === methodName,
hasObject: true,
methodName,
packageNames: fsPackageNames,
program,
});

if (!declaration) {
return null;
}

// we found the require for our method!
const fsFunction = declaration.init.property.name;
const packageName = declaration.init.object.arguments[0].value;
const fnName = declaration.init.property.name;

const indices = getIndices(node, fsMetaData[fsFunction]);

return generateReport({
context,
node,
packageName,
methodName: fnName,
indices,
});
}

/**
* Detects:
* | var something = require('fs');
* | something.readFile(c);
*/
function detectOnMethodCall({ context, node, program, methodName }) {
const declaration = getVariableDeclaration({
packageNames: fsPackageNames,
hasObject: false,
program,
});

if (!declaration) {
return null;
}

const indices = getIndices(node.parent, fsMetaData[methodName]);

return generateReport({
context,
node,
packageName: declaration.init.arguments[0].value,
methodName,
indices,
});
}

/**
* Detects:
* | var { readFile: something } = require('fs')
* | readFile(filename)
*/
function detectOnDestructuredRequire({ context, methodName, node, program }) {
const declaration = getVariableDeclaration({
condition: (declaration) => declaration && declaration.id && declaration.id.properties && declaration.id.properties.some((p) => p.value.name === methodName),
hasObject: false,
methodName,
packageNames: fsPackageNames,
program,
});

if (!declaration) {
return null;
}

const realMethodName = declaration.id.properties.find((p) => p.value.name === methodName).key.name;

const meta = fsMetaData[realMethodName];
const indices = getIndices(node, meta);

return generateReport({
context,
node,
packageName: declaration.init.arguments[0].value,
methodName: realMethodName,
indices,
});
}

/**
* Detects:
* | import { readFile as something } from 'fs';
* | something(filename);
*/
function detectOnDestructuredImport({ context, methodName, node, program }) {
const importDeclaration = getImportDeclaration({ methodName, packageNames: fsPackageNames, program });

const specifier = importDeclaration && importDeclaration.specifiers && importDeclaration.specifiers.find((s) => !!funcNames.includes(s.imported.name));

if (!specifier) {
return null;
}

const fnName = specifier.imported.name;
const meta = fsMetaData[fnName];
const indices = getIndices(node, meta);

return generateReport({
context,
node,
packageName: specifier.parent.source.value,
methodName: fnName,
indices,
});
}

/**
* Detects:
* | import * as something from 'fs';
* | something.readFile(c);
*/
function detectOnDefaultImport({ context, methodName, node, objectName, program }) {
if (!funcNames.includes(methodName)) {
return null;
}

const importDeclaration = getImportDeclaration({ methodName: objectName, packageNames: fsPackageNames, program });

if (!importDeclaration) {
return null;
}

const meta = fsMetaData[methodName];
const indices = getIndices(node.parent, meta);

return generateReport({
context,
node,
packageName: importDeclaration.source.value,
methodName,
indices,
});
}

/**
* Detects:
* | var something = require('fs').promises;
* | something.readFile(filename)
*/
function detectOnPromiseProperty({ context, methodName, node, objectName, program }) {
const declaration = program.body
.filter((entry) => entry.type === 'VariableDeclaration')
.flatMap((entry) => entry.declarations)
.find(
(declaration) =>
declaration.id.name === objectName &&
declaration.init.type === 'MemberExpression' &&
// package name is fs / fs-extra
fsPackageNames.includes(declaration.init.object.arguments[0].value)
);

if (!declaration) {
return null;
}
const meta = fsMetaData[methodName];
const indices = getIndices(node.parent, meta);

return generateReport({
context,
node,
packageName: declaration.init.object.arguments[0].value,
methodName,
indices,
});
context.report({ node, message: `Found ${methodName} from package "${packageName}" with non literal argument at index ${indices.join(',')}` });
}

//------------------------------------------------------------------------------
Expand All @@ -223,87 +43,59 @@ module.exports = {
create: function (context) {
return {
CallExpression: function (node) {
// readFile/open/... (but might be renamed)
const localMethodName = node.callee.name;

// don't check require. If all arguments are Literals, it's surely safe!
if (!localMethodName || localMethodName === 'require' || node.arguments.every((argument) => argument.type === 'Literal')) {
if ((node.callee.type === 'Identifier' && node.callee.name === 'require') || node.arguments.every((argument) => argument.type === 'Literal')) {
return;
}

// this only works, when imports are on top level!
const program = context.getAncestors()[0];

const requireReport = detectOnRequireWithProperty({
context,
methodName: localMethodName,
node,
program,
});
if (requireReport) {
return requireReport;
}

const destructuredRequireReport = detectOnDestructuredRequire({
context,
methodName: localMethodName,
node,
program,
const pathInfo = getImportAccessPath({
node: node.callee,
scope: context.getScope(),
packageNames: fsPackageNames,
});
if (destructuredRequireReport) {
return destructuredRequireReport;
if (!pathInfo) {
return;
}

const importReport = detectOnDestructuredImport({
context,
methodName: localMethodName,
node,
program,
});
if (importReport) {
return importReport;
let fnName;
if (pathInfo.path.length === 1) {
// Check for:
// | var something = require('fs').readFile;
// | something(a);
// ,
// | var something = require('fs');
// | something.readFile(c);
// ,
// | var { readFile: something } = require('fs')
// | readFile(filename);
// ,
// | import { readFile as something } from 'fs';
// | something(filename);
// , or
// | import * as something from 'fs';
// | something.readFile(c);
fnName = pathInfo.path[0];
} else if (pathInfo.path.length === 2) {
// Check for:
// | var something = require('fs').promises;
// | something.readFile(filename)
fnName = pathInfo.path[1];
} else {
return;
}
},
MemberExpression: function (node) {
const realMethodName = node.property.name; // readFile/open/... (not renamed)
const localObjectName = node.object.name; // fs/node:fs/... (but might be renamed)

// this only works, when imports are on top level!
const program = context.getAncestors()[0];

const methodCallReport = detectOnMethodCall({
context,
methodName: realMethodName,
node,
program,
});
if (methodCallReport) {
return methodCallReport;
if (!funcNames.includes(fnName)) {
return false;
}
const packageName = pathInfo.packageName;

const defaultImportReport = detectOnDefaultImport({
program,
objectName: localObjectName,
methodName: realMethodName,
context,
node,
});

if (defaultImportReport) {
return defaultImportReport;
}
const indices = getIndices(node, fsMetaData[fnName]);

const promisePropertyReport = detectOnPromiseProperty({
generateReport({
context,
methodName: realMethodName,
node,
objectName: localObjectName,
program,
packageName,
methodName: fnName,
indices,
});

if (promisePropertyReport) {
return promisePropertyReport;
}
},
};
},
Expand Down

0 comments on commit d3b1543

Please sign in to comment.