react-magnetic-di
Version:
Context driven dependency injection
172 lines (147 loc) • 6.96 kB
JavaScript
function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); }
function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }
function _iterableToArrayLimit(arr, i) { if (typeof Symbol === "undefined" || !(Symbol.iterator in Object(arr))) return; var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; }
function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
var _require = require('../utils'),
getDiIdentifier = _require.getDiIdentifier,
getDiStatements = _require.getDiStatements,
getParentDiStatements = _require.getParentDiStatements,
getDiVars = _require.getDiVars,
isHookName = _require.isHookName,
isComponentName = _require.isComponentName,
isLocalVariable = _require.isLocalVariable;
var getReactIdentifiers = function getReactIdentifiers(node) {
if (node.source.value === 'react') {
return node.specifiers.map(function (s) {
return s.local;
}).filter(function (n) {
return !['useState', 'useContext', 'useReducer'].includes(n.name);
});
}
};
var isDefaultProp = function isDefaultProp(node, diStatement) {
// we assume order rule is enabled, so if the variable is used in an assignment
// defined before our di() statements, then it's probably default props
return node.parent.type === 'AssignmentPattern' && node.range[0] < diStatement.range[0];
};
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'Requires external components/hooks to be marked as injectable',
category: 'Best Practices',
recommended: false
},
fixable: 'code',
schema: [{
type: 'object',
properties: {
ignore: {
type: 'array',
items: {
type: 'string'
}
}
},
additionalProperties: false
}],
messages: {
missingInject: "Dependency '{{name}}' has not being marked as injectable. " + 'Add it to the list of the injectable dependencies'
}
},
create: function create(context) {
var diIdentifier;
var reactVars;
var userOptions = Object.assign({
ignore: []
}, context.options[0]);
var isInjected = function isInjected(vars, n) {
return vars.some(function (v) {
return v.name === n.name;
});
};
var isReactIgnored = function isReactIgnored(n) {
return reactVars.some(function (v) {
return v.name === n.name;
});
};
var isOptionsIgnored = function isOptionsIgnored(n) {
return userOptions.ignore.includes(n.name);
};
var report = function report(node, diStatement) {
return context.report({
node: diStatement,
messageId: 'missingInject',
data: {
name: node.name
},
fix: function fix(fixer) {
var lastArg = diStatement.expression.arguments.slice(-1)[0];
if (!lastArg) {
// if injection without args, let's add the var inside call
var _diStatement$expressi = _slicedToArray(diStatement.expression.callee.range, 2),
start = _diStatement$expressi[0],
end = _diStatement$expressi[1];
return fixer.insertTextAfterRange([start, end + 1], node.name);
}
return fixer.insertTextAfter(lastArg, ", ".concat(node.name));
}
});
};
return {
ImportDeclaration: function ImportDeclaration(node) {
if (!diIdentifier) diIdentifier = getDiIdentifier(node);
if (!reactVars) reactVars = getReactIdentifiers(node);
},
// this is to handle hooks and components recognised as used variables
// it does not cover JSX variables
BlockStatement: function BlockStatement(node) {
if (!diIdentifier) return;
var throughVars = context.getScope().through.map(function (v) {
return v.identifier;
}).filter(function (v) {
return v.name !== diIdentifier.name;
});
var diStatements = getDiStatements(node, diIdentifier); // ignore locations where di was not explicitly set
if (!diStatements.length) return;
var diVars = getDiVars(diStatements);
throughVars.forEach(function (varNode) {
var isInjectable = isHookName(varNode);
if (!isInjectable || isInjected(diVars, varNode) || isReactIgnored(varNode) || isOptionsIgnored(varNode) || isDefaultProp(varNode, diStatements[0])) return;
report(varNode, diStatements[diStatements.length - 1]);
});
},
// as JSX elements are not treated as variables, for each JSX tag
// we check if there is a block with di() above and if that includes it
'JSXOpeningElement:exit': function JSXOpeningElementExit(node) {
if (!diIdentifier) return; // ignore if the component is declared locally
if (isLocalVariable(node.name, context.getScope(), diIdentifier)) return;
var varNode;
switch (node.name.type) {
case 'JSXIdentifier':
{
varNode = node.name;
var isInjectable = isComponentName(varNode);
if (!isInjectable || isReactIgnored(varNode) || isOptionsIgnored(varNode)) return;
break;
}
case 'JSXNamespacedName':
// TODO handle foo:Bar
return;
case 'JSXMemberExpression':
// TODO handle foo.Bar (but ignoring this.Bar)
return;
default:
return;
}
var diStatements = getParentDiStatements(varNode, diIdentifier); // ignore locations where di was not explicitly set
if (!diStatements.length) return;
var diVars = getDiVars(diStatements);
if (isInjected(diVars, varNode)) return;
report(varNode, diStatements[diStatements.length - 1]);
}
};
}
};