eslint-plugin-unicorn
Version:
More than 300 powerful ESLint rules
265 lines (225 loc) • 6.32 kB
JavaScript
import {findVariable} from '@eslint-community/eslint-utils';
import {getStaticStringValue, isMethodCall, isNewExpression} from './ast/index.js';
import {getLastTrailingCommentOnSameLine, getVariableIdentifiers} from './utils/index.js';
import {removeStatement} from './fix/index.js';
const MESSAGE_ID = 'no-blob-to-file';
const MESSAGE_ID_SUGGESTION = 'suggestion';
const messages = {
[MESSAGE_ID]: 'Use the original `Blob` instead of converting it to a `File`.',
[MESSAGE_ID_SUGGESTION]: 'Replace the `File` with the original `Blob`.',
};
const isStandaloneConstDeclaration = node =>
node.parent.type === 'VariableDeclaration'
&& node.parent.kind === 'const'
&& node.parent.declarations.length === 1
&& (
node.parent.parent.type === 'Program'
|| node.parent.parent.type === 'BlockStatement'
);
function isGlobalIdentifier(node, context) {
const variable = findVariable(context.sourceCode.getScope(node), node);
return !variable || variable.defs.length === 0;
}
function isSameBindingAtUse(identifier, reference, context) {
if (identifier.type !== 'Identifier') {
return true;
}
const {sourceCode} = context;
return findVariable(sourceCode.getScope(identifier), identifier) === findVariable(sourceCode.getScope(reference), identifier);
}
const isGlobalFormDataConstructor = (node, context) =>
isNewExpression(node, {
name: 'FormData',
argumentsLength: 0,
})
&& isGlobalIdentifier(node.callee, context);
function getConstIdentifierDeclaration(node, context) {
if (node?.type !== 'Identifier') {
return;
}
const variable = findVariable(context.sourceCode.getScope(node), node);
const definition = variable?.defs[0];
const declaration = definition?.node;
if (
definition?.type === 'Variable'
&& definition.kind === 'const'
&& declaration?.type === 'VariableDeclarator'
) {
return declaration;
}
}
function getFileNameNode(newFileExpression) {
const [, fileNameNode] = newFileExpression.arguments;
return fileNameNode;
}
function isBlobIdentifier(node, beforeNode, context) {
const declaration = getConstIdentifierDeclaration(node, context);
if (
!declaration
|| context.sourceCode.getRange(declaration)[0] > context.sourceCode.getRange(beforeNode)[0]
) {
return false;
}
const initializer = declaration.init;
return (
isNewExpression(initializer, {
name: 'Blob',
maximumArguments: 1,
})
|| isNewExpression(initializer, {
name: 'File',
minimumArguments: 2,
maximumArguments: 2,
})
)
&& isGlobalIdentifier(initializer.callee, context);
}
function hasComments(node, context) {
const {sourceCode} = context;
return sourceCode.getCommentsInside(node).length > 0
|| sourceCode.getCommentsBefore(node).some(comment =>
sourceCode.getLoc(comment).end.line === sourceCode.getLoc(node).start.line
|| sourceCode.getLoc(comment).end.line === sourceCode.getLoc(node).start.line - 1)
|| getLastTrailingCommentOnSameLine(context, node);
}
function getBlobIdentifier(newFileExpression, context) {
if (
!isNewExpression(newFileExpression, {
name: 'File',
minimumArguments: 2,
maximumArguments: 2,
})
|| !isGlobalIdentifier(newFileExpression.callee, context)
) {
return;
}
const [fileBits] = newFileExpression.arguments;
const [, fileName] = newFileExpression.arguments;
if (
fileBits.type !== 'ArrayExpression'
|| fileBits.elements.length !== 1
|| fileBits.elements[0]?.type === 'SpreadElement'
|| getStaticStringValue(fileName) === undefined
) {
return;
}
const [blobIdentifier] = fileBits.elements;
if (isBlobIdentifier(blobIdentifier, newFileExpression, context)) {
return blobIdentifier;
}
}
function getSupportedCall(identifier, context) {
const {parent} = identifier;
if (
isMethodCall(parent, {
object: 'URL',
method: 'createObjectURL',
argumentsLength: 1,
optionalCall: false,
optionalMember: false,
})
&& parent.arguments[0] === identifier
&& isGlobalIdentifier(parent.callee.object, context)
) {
return {
call: parent,
kind: 'blobArgument',
};
}
if (!(
isMethodCall(parent, {
methods: ['append', 'set'],
minimumArguments: 2,
maximumArguments: 3,
optionalCall: false,
optionalMember: false,
})
&& parent.arguments[1] === identifier
&& isGlobalFormDataConstructor(getConstIdentifierDeclaration(parent.callee.object, context)?.init, context)
)) {
return;
}
return {
call: parent,
kind: 'formData',
};
}
function getProblem(node, context) {
const {sourceCode} = context;
const {id, init} = node;
if (
id.type !== 'Identifier'
|| !isStandaloneConstDeclaration(node)
) {
return;
}
const blobIdentifier = getBlobIdentifier(init, context);
if (!blobIdentifier) {
return;
}
const variable = findVariable(sourceCode.getScope(id), id);
if (!variable) {
return;
}
const references = getVariableIdentifiers(variable).filter(identifier => identifier !== id);
if (references.length !== 1) {
return;
}
const [reference] = references;
const supportedCall = getSupportedCall(reference, context);
if (!supportedCall) {
return;
}
const fileNameNode = getFileNameNode(init);
if (
!isSameBindingAtUse(blobIdentifier, reference, context)
|| hasComments(node.parent, context)
|| (
supportedCall.kind === 'formData'
&& supportedCall.call.arguments.length === 2
&& !fileNameNode
)
) {
return;
}
return {
node: init,
messageId: MESSAGE_ID,
suggest: [
{
messageId: MESSAGE_ID_SUGGESTION,
* fix(fixer) {
yield removeStatement(node.parent, context, fixer);
yield fixer.replaceText(reference, sourceCode.getText(blobIdentifier));
if (
supportedCall.kind === 'formData'
&& supportedCall.call.arguments.length === 2
) {
yield fixer.insertTextAfter(reference, `, ${sourceCode.getText(fileNameNode)}`);
}
},
},
],
};
}
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
context.on('VariableDeclarator', node => getProblem(node, context));
};
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Disallow unnecessary `Blob` to `File` conversion.',
recommended: 'unopinionated',
},
hasSuggestions: true,
messages,
languages: [
'js/js',
],
},
};
export default config;