Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ Read more at the
| [prefer-object-has-own](./src/rules/prefer-object-has-own.ts) | Prefer `Object.hasOwn()` over `Object.prototype.hasOwnProperty.call()` and `obj.hasOwnProperty()` | ✅ | ✅ | ✖️ |
| [prefer-spread-syntax](./src/rules/prefer-spread-syntax.ts) | Prefer spread syntax over `Array.concat()`, `Array.from()`, `Object.assign({}, ...)`, and `Function.apply()` | ✅ | ✅ | 🔶 |
| [prefer-url-canparse](./src/rules/prefer-url-canparse.ts) | Prefer `URL.canParse()` over try-catch blocks for URL validation | ✅ | 💡 | ✖️ |
| [prefer-get-or-insert](./src/rules/prefer-get-or-insert.ts) | Prefer `Map.prototype.getOrInsert()` over reading a map entry with a default and writing it back | ✖️ | ✅ | ✖️ |

### Module replacements

Expand Down
2 changes: 2 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {preferStringFromCharCode} from './rules/prefer-string-fromcharcode.js';
import {noSpreadInReduce} from './rules/no-spread-in-reduce.js';
import {preferFlatMapOverMapFlat} from './rules/prefer-flatmap-over-map-flat.js';
import {preferStaticCollator} from './rules/prefer-static-collator.js';
import {preferGetOrInsert} from './rules/prefer-get-or-insert.js';

const plugin: ESLint.Plugin = {
meta: {
Expand Down Expand Up @@ -62,6 +63,7 @@ const plugin: ESLint.Plugin = {
'no-spread-in-reduce': noSpreadInReduce,
'prefer-flatmap-over-map-flat': preferFlatMapOverMapFlat,
'prefer-static-collator': preferStaticCollator,
'prefer-get-or-insert': preferGetOrInsert,
'ban-dependencies': banDependencies
}
};
Expand Down
130 changes: 130 additions & 0 deletions src/rules/prefer-get-or-insert.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import {RuleTester} from 'eslint';
import {preferGetOrInsert} from './prefer-get-or-insert.js';

const ruleTester = new RuleTester({
languageOptions: {
ecmaVersion: 2022,
sourceType: 'module'
}
});

ruleTester.run('prefer-get-or-insert', preferGetOrInsert, {
valid: [
// plain get/set, no default
'map.set(k, map.get(k));',

// setting an unrelated value
'map.set(k, other.get(k) ?? def);',
'map.set(k, map.get(other) ?? def);',

// different map between set and get
'map.set(k, other.has(k) ? other.get(k) : def);',

// ternary uses a different key for has vs get
'map.set(k, map.has(k) ? map.get(other) : def);',

// read-with-default that is never written back
'const v = map.get(k) ?? def; use(v);',
'const v = map.has(k) ? map.get(k) : def; use(v);',

// written back to a different map / key
'const v = map.get(k) ?? def; other.set(k, v);',
'const v = map.get(k) ?? def; map.set(other, v);',

// written back, but a different variable
'const v = map.get(k) ?? def; map.set(k, somethingElse);',

// set in a different block than the declaration
'const v = map.get(k) ?? def; if (x) { map.set(k, v); }',

// computed / optional access is out of scope
'map["set"](k, map.get(k) ?? def);',
'map?.set(k, map.get(k) ?? def);',

// logical operators other than ??
'map.set(k, map.get(k) || def);',

// guarded lazy-init that never writes the default back
'let v = map.get(k); if (!v) { v = def; }',

// guarded lazy-init writing back to a different map / key
'let v = map.get(k); if (!v) { v = def; other.set(k, v); }',
'let v = map.get(k); if (!v) { v = def; map.set(other, v); }'
],

invalid: [
// map.set(k, map.get(k) ?? default)
{
code: 'map.set(k, map.get(k) ?? def);',
output: 'map.getOrInsert(k, def);',
errors: [{messageId: 'preferGetOrInsert'}]
},
// member-expression key
{
code: 'cache.set(obj.id, cache.get(obj.id) ?? createDefault());',
output: 'cache.getOrInsert(obj.id, createDefault());',
errors: [{messageId: 'preferGetOrInsert'}]
},
// map.set(k, map.has(k) ? map.get(k) : default)
{
code: 'map.set(k, map.has(k) ? map.get(k) : []);',
output: 'map.getOrInsert(k, []);',
errors: [{messageId: 'preferGetOrInsert'}]
},
// member-expression receiver
{
code: 'this.cache.set(k, this.cache.get(k) ?? def);',
output: 'this.cache.getOrInsert(k, def);',
errors: [{messageId: 'preferGetOrInsert'}]
},
// const v = map.get(k) ?? default; ...; map.set(k, v)
{
code: `
const v = map.get(k) ?? def;
v.push(1);
map.set(k, v);
`,
output: `
const v = map.getOrInsert(k, def);
v.push(1);
`,
errors: [{messageId: 'preferGetOrInsert'}]
},
// const v = map.has(k) ? map.get(k) : default; ...; map.set(k, v)
{
code: `
let v = map.has(k) ? map.get(k) : [];
map.set(k, v);
`,
output: `
let v = map.getOrInsert(k, []);
`,
errors: [{messageId: 'preferGetOrInsert'}]
},
// reassignment instead of declaration
{
code: `
v = map.get(k) ?? def;
map.set(k, v);
`,
output: `
v = map.getOrInsert(k, def);
`,
errors: [{messageId: 'preferGetOrInsert'}]
},
// let v = map.get(k); if (!v) { v = default; map.set(k, v); }
{
code: `
let v = map.get(k);
if (!v) {
v = [];
map.set(k, v);
}
`,
output: `
let v = map.getOrInsert(k, []);
`,
errors: [{messageId: 'preferGetOrInsert'}]
}
]
});
Loading