Skip to content

Commit

Permalink
Add imports util to cover all possible cases
Browse files Browse the repository at this point in the history
  • Loading branch information
blazejkustra committed Jun 10, 2024
1 parent f45952b commit 9013404
Show file tree
Hide file tree
Showing 4 changed files with 62 additions and 46 deletions.
4 changes: 2 additions & 2 deletions eslint-plugin-expensify/CONST.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ module.exports = {
USE_PERIODS_ERROR_MESSAGES: 'Use periods at the end of error messages.',
USE_DOUBLE_NEGATION_INSTEAD_OF_BOOLEAN: 'Use !! instead of Boolean().',
NO_ACC_SPREAD_IN_REDUCE: 'Avoid a use of spread (`...`) operator on accumulators in reduce callback. Mutate them directly instead.',
PREFER_TYPE_FEST_TUPLE_TO_UNION: 'Prefer using TupleToUnion from type-fest for converting tuple types to union types.',
PREFER_TYPE_FEST_VALUE_OF: 'Prefer using ValueOf from type-fest to extract the type of the properties of an object.',
PREFER_TYPE_FEST_TUPLE_TO_UNION: 'Prefer using `TupleToUnion` from `type-fest` for converting tuple types to union types.',
PREFER_TYPE_FEST_VALUE_OF: 'Prefer using `ValueOf` from `type-fest` to extract the type of the properties of an object.',
},
};
66 changes: 23 additions & 43 deletions eslint-plugin-expensify/prefer-type-fest.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable rulesdir/prefer-underscore-method */
/* eslint-disable es/no-optional-chaining */
const {AST_NODE_TYPES} = require('@typescript-eslint/utils');
const {PREFER_TYPE_FEST_VALUE_OF, PREFER_TYPE_FEST_TUPLE_TO_UNION} = require('./CONST').MESSAGE;
const {addNamedImport} = require('./utils/imports');

const rule = {
meta: {
Expand All @@ -12,50 +12,14 @@ const rule = {
fixable: 'code',
},
create(context) {
let typeFestImported = false;

function valueOfFixer(node, objectName) {
return (fixer) => {
// Create replacements and add import if necessary
const fixes = [fixer.replaceText(node, `ValueOf<typeof ${objectName}>`)];

if (!typeFestImported) {
fixes.push(
fixer.insertTextBefore(
context.getSourceCode().ast.body[0],
"import type {ValueOf} from 'type-fest';\n"
)
);
}

return fixes;
}
}

function tupleToUnionFixer(node, objectName) {
return (fixer) => {
// Create replacements and add import if necessary
const fixes = [fixer.replaceText(node, `TupleToUnion<typeof ${objectName}>`)];

if (!typeFestImported) {
fixes.push(
fixer.insertTextBefore(
context.getSourceCode().ast.body[0],
"import type {TupleToUnion} from 'type-fest';\n"
)
);
}

return fixes;
}
}
let typeFestImport;

return {
Program(node) {
// Find type-fest import declarations
node.body.forEach(statement => {
if (statement.type === 'ImportDeclaration' && statement.source.value === 'type-fest') {
typeFestImported = true;
typeFestImport = statement;
}
});
},
Expand Down Expand Up @@ -86,7 +50,11 @@ const rule = {
context.report({
node,
message: PREFER_TYPE_FEST_VALUE_OF,
fix: valueOfFixer(node, objectTypeText),
fix: (fixer) => {
const fixes = [fixer.replaceText(node, `ValueOf<typeof ${objectTypeText}>`)];
fixes.push(...addNamedImport(context, fixer, typeFestImport, 'ValueOf', 'type-fest', true));
return fixes;
}
});
}
}
Expand All @@ -96,7 +64,11 @@ const rule = {
context.report({
node,
message: PREFER_TYPE_FEST_TUPLE_TO_UNION,
fix: tupleToUnionFixer(node, objectTypeText),
fix: (fixer) => {
const fixes = [fixer.replaceText(node, `TupleToUnion<typeof ${objectTypeText}>`)];
fixes.push(...addNamedImport(context, fixer, typeFestImport, 'TupleToUnion', 'type-fest', true));
return fixes;
}
});
}
}
Expand All @@ -113,7 +85,11 @@ const rule = {
context.report({
node,
message: PREFER_TYPE_FEST_VALUE_OF,
fix: valueOfFixer(node, objectTypeText),
fix: (fixer) => {
const fixes = [fixer.replaceText(node, `ValueOf<typeof ${objectTypeText}>`)];
fixes.push(...addNamedImport(context, fixer, typeFestImport, 'ValueOf', 'type-fest', true));
return fixes;
}
});
}
}
Expand All @@ -123,7 +99,11 @@ const rule = {
context.report({
node,
message: PREFER_TYPE_FEST_TUPLE_TO_UNION,
fix: tupleToUnionFixer(node, objectTypeText),
fix: (fixer) => {
const fixes = [fixer.replaceText(node, `TupleToUnion<typeof ${objectTypeText}>`)];
fixes.push(...addNamedImport(context, fixer, typeFestImport, 'TupleToUnion', 'type-fest', true));
return fixes;
}
});
}
}
Expand Down
8 changes: 7 additions & 1 deletion eslint-plugin-expensify/tests/prefer-type-fest.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ ruleTester.run('prefer-type-fest', rule, {
parser: require.resolve('@typescript-eslint/parser'),
output: 'import type {TupleToUnion} from \'type-fest\';\nconst TIMEZONES = [\'a\', \'b\'] as const; const test: Record<string, TupleToUnion<typeof TIMEZONES>> = { a: \'a\', b: \'b\' };',
},
{
code: 'import type {Something} from \'type-fest\';\nconst TIMEZONES = [\'a\', \'b\'] as const; const test: Record<string, (typeof TIMEZONES)[number]> = { a: \'a\', b: \'b\' };',
errors: [{message: PREFER_TYPE_FEST_TUPLE_TO_UNION}],
parser: require.resolve('@typescript-eslint/parser'),
output: 'import type {Something, TupleToUnion} from \'type-fest\';\nconst TIMEZONES = [\'a\', \'b\'] as const; const test: Record<string, TupleToUnion<typeof TIMEZONES>> = { a: \'a\', b: \'b\' };',
},
{
code: 'const COLORS = { GREEN: \'green\', BLUE: \'blue\' } as const; type Bad = (typeof COLORS)[keyof COLORS];',
errors: [{message: PREFER_TYPE_FEST_VALUE_OF}],
Expand All @@ -82,7 +88,7 @@ ruleTester.run('prefer-type-fest', rule, {
code: 'import type {TupleToUnion} from \'type-fest\';\nconst COLORS = { GREEN: \'green\', BLUE: \'blue\' } as const; type Bad = (typeof COLORS)[keyof COLORS];',
errors: [{message: PREFER_TYPE_FEST_VALUE_OF}],
parser: require.resolve('@typescript-eslint/parser'),
output: 'import type {TupleToUnion} from \'type-fest\';\nconst COLORS = { GREEN: \'green\', BLUE: \'blue\' } as const; type Bad = ValueOf<typeof COLORS>;',
output: 'import type {TupleToUnion, ValueOf} from \'type-fest\';\nconst COLORS = { GREEN: \'green\', BLUE: \'blue\' } as const; type Bad = ValueOf<typeof COLORS>;',
},
{
code: 'import somethingElse from \'something-else\';\nconst COLORS = { GREEN: \'green\', BLUE: \'blue\' } as const; type Bad = (typeof COLORS)[keyof COLORS];',
Expand Down
30 changes: 30 additions & 0 deletions eslint-plugin-expensify/utils/imports.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
function addNamedImport(context, fixer, importNode, importName, importPath, importAsType = false) {
const fixes = [];

if (importNode) {
const alreadyImported = importNode.specifiers.some(
specifier => specifier.imported.name === importName
);

if(!alreadyImported) {
const lastSpecifier = importNode.specifiers[importNode.specifiers.length - 1];

// Add ValueOf to existing type-fest import
fixes.push(fixer.insertTextAfter(lastSpecifier, `, ${importName}`));
}
} else {
// Add import if it doesn't exist
fixes.push(
fixer.insertTextBefore(
context.getSourceCode().ast.body[0],
`import ${importAsType ? "type " : ""}{${importName}} from '${importPath}';\n`
)
);
}

return fixes;
}

module.exports = {
addNamedImport
};

0 comments on commit 9013404

Please sign in to comment.