eslint prefer-template
labelled safe
Analysis Nemotron 3 Super
The rule flags uses of binary '+' where at least one operand is a string literal.
Fix: Replaces the concatenation expression with a template literal where string literals become raw text and non-literal operands become ${expression} placeholders, preserving original grouping and operator precedence via implicit left-associativity.
11 hazards
- If any operand is a getter with side effects, rewriting may change the number of times the getter is accessed due to altered evaluation order.
- If an operand contains a comma operator, the template literal may group expressions differently, changing which value is used.
- If an operand is a function call, the fix may evaluate it in a different temporal order relative to other side effects in the expression.
- If the concatenation appears in a context expecting exactly two operands (e.g., tagged template with custom parsing), the rewrite breaks the expected argument structure.
- If an operand is undefined or null, String() conversion in the template may produce 'undefined'/'null' while the original concatenation could throw if the operand was a Symbol (though Symbols are rare in practice).
- If the code relies on the left-to-right evaluation order of the '+' operator for side effects (e.g., "a" + (b, console.log("side"), c)), the fix may reorder those side effects.
- If the concatenation is inside a larger expression where operator precedence matters (e.g., "a" + b * c), the fix does not add parentheses, potentially changing meaning if the template literal is misinterpreted (though template literals have high precedence, this is generally safe).
- If the string literal contains backticks, dollar signs, or curly braces, the fix does not escape them, potentially altering the template literal's meaning (e.g., "`${x}${"}" becomes `` `${x}${}` `` which is invalid or changes interpolation).
- If the expression includes decorators or metadata comments attached to specific tokens, the rewrite may discard or misplace those comments.
- If the concatenation is part of a sequence expression where the result is ignored (e.g., 0, "a" + b), removing the '+' operator may drop a deliberate no-op if the linter or build step depends on the syntactic form.
- If an operand is a class field access that depends on runtime 'this' binding, the fix does not alter binding but may change when 'this' is evaluated if the operand is moved relative to other side-effecting expressions.
Programs
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | fix ok | Precedence in Larger Expression | If the concatenation is inside a larger expression where operator precedence matters, the fix does not add parentheses, potentially changing meaning. |
| r1-p002 | fix ok | This Binding: Arrow vs Function Method Extraction | this binding: arrow vs function, methods extracted from objects, call/apply/bind |
| r1-p003 | fix ok | Function Declarations Inside Blocks | function declarations inside blocks, switch cases, labeled statements |
| r1-p004 | fix ok | Numeric/String Coercion Differences | numeric/string coercion: + vs template literal, objects with toString/valueOf, Symbol, BigInt |
| r1-p005 | fix ok prediction off | Undefined/null vs Symbol Conversion | If an operand is undefined or null, String() conversion in the template may produce 'undefined'/'null' while the original concatenation could throw if the operand was a Symbol |
| r1-p006 | fix ok | Comments Attached to Removed/Moved Code | comments attached to the removed or moved code |
| r1-p007 | fix ok | Getter Side Effects and Evaluation Order | If any operand is a getter with side effects, rewriting may change the number of times the getter is accessed due to altered evaluation order. |
| r1-p008 | fix ok | Temporal Dead Zone After Hoisting/Reordering | temporal dead zone after code is hoisted or reordered |
| r2-p001 | fix ok | Comma Operator Grouping Changed | If an operand contains a comma operator, the template literal may group expressions differently, changing which value is used. |
| r2-p002 | invalid | Label/Break Control Flow Interference | label/break/continue control flow, finally blocks, early returns in nested blocks |
| r2-p003 | invalid | Left-to-Right Evaluation Order Side Effects | If the code relies on the left-to-right evaluation order of the '+' operator for side effects (e.g., "a" + (b, console.log("side"), c)), the fix may reorder those side effects. |
| r2-p004 | silent prediction off | Precedence in Larger Expression | If the concatenation is inside a larger expression where operator precedence matters (e.g., "a" + b * c), the fix does not add parentheses, potentially changing meaning if the template literal is misi |
| r2-p005 | fix ok | Var Hoisting and Re-declaration | var hoisting and re-declaration inside loops or after use |
| r2-p006 | fix ok | Order of Evaluation - Side Effects Run Twice/Never | order of evaluation when expressions are merged, duplicated or deleted (side effects run twice or never) |
| r2-p007 | silent | Sequence Expression with Ignored Result | If the concatenation is part of a sequence expression where the result is ignored (e.g., 0, "a" + b), removing the '+' operator may drop a deliberate no-op if the linter or build step depends on the s |
| r2-p008 | fix ok | Getter Side Effects and Evaluation Order | getters, setters and Object.defineProperty side effects |
Sources
r1-p001 · Precedence in Larger Expression
1function f() {
2 return "result:" + 2 * 3;
3}
4console.log(f());
--- orig.js 2026-10-22 14:31:24.400152237 -0400 +++ fixed.js 2026-10-22 14:31:28.327171922 -0400 @@ -1,4 +1,4 @@ function f() { - return "result:" + 2 * 3; + return `result:${ 2 * 3}`; } console.log(f());
result:6
r1-p002 · This Binding: Arrow vs Function Method Extraction
1const obj = {
2 value: 42,
3 getValue: function() { return this.value; }
4};
5const fn = obj.getValue;
6console.log("Value is " + fn());
--- orig.js 2026-10-22 14:31:24.401152242 -0400 +++ fixed.js 2026-10-22 14:31:27.619168299 -0400 @@ -3,4 +3,4 @@ getValue: function() { return this.value; } }; const fn = obj.getValue; -console.log("Value is " + fn()); +console.log(`Value is ${ fn()}`);
Value is undefined
r1-p003 · Function Declarations Inside Blocks
1if (true) {
2 function helper() { return "ok"; }
3 console.log("Status: " + helper());
4}
--- orig.js 2026-10-22 14:31:24.402152247 -0400 +++ fixed.js 2026-10-22 14:31:27.067165491 -0400 @@ -1,4 +1,4 @@ if (true) { function helper() { return "ok"; } - console.log("Status: " + helper()); + console.log(`Status: ${ helper()}`); }
Status: ok
r1-p004 · Numeric/String Coercion Differences
1const obj = {
2 toString() { return "obj"; }
3};
4console.log("Value: " + obj);
--- orig.js 2026-10-22 14:31:24.403152252 -0400 +++ fixed.js 2026-10-22 14:31:27.602168213 -0400 @@ -1,4 +1,4 @@ const obj = { toString() { return "obj"; } }; -console.log("Value: " + obj); +console.log(`Value: ${ obj}`);
Value: obj
r1-p005 · Undefined/null vs Symbol Conversion
1let x = undefined;
2try {
3 console.log("Result: " + x);
4} catch (e) {
5 console.log("Caught: " + e.name + ": " + e.message);
6}
--- orig.js 2026-10-22 14:31:24.403152252 -0400 +++ fixed.js 2026-10-22 14:31:27.601168208 -0400 @@ -1,6 +1,6 @@ let x = undefined; try { - console.log("Result: " + x); + console.log(`Result: ${ x}`); } catch (e) { - console.log("Caught: " + e.name + ": " + e.message); + console.log(`Caught: ${ e.name }: ${ e.message}`); }
Result: undefined
r1-p006 · Comments Attached to Removed/Moved Code
1let msg = "Hello" // comment
2 + world;
3console.log(msg);
--- orig.js 2026-10-22 14:31:24.404152256 -0400 +++ fixed.js 2026-10-22 14:31:28.293171747 -0400 @@ -1,3 +1,3 @@ -let msg = "Hello" // comment - + world; +let msg = `Hello${ // comment + world}`; console.log(msg);
r1-p007 · Getter Side Effects and Evaluation Order
1let count = 0;
2const obj = {
3 get value() {
4 count++;
5 return "val";
6 }
7};
8console.log("Start" + obj.value + "End");
9console.log("Getter called " + count + " times");
--- orig.js 2026-10-22 14:31:28.737174035 -0400 +++ fixed.js 2026-10-22 14:31:31.942190926 -0400 @@ -5,5 +5,5 @@ return "val"; } }; -console.log("Start" + obj.value + "End"); -console.log("Getter called " + count + " times"); +console.log(`Start${ obj.value }End`); +console.log(`Getter called ${ count } times`);
StartvalEnd Getter called 1 times
r1-p008 · Temporal Dead Zone After Hoisting/Reordering
1console.log("Value: " + tmp);
2let tmp = "initialized";
--- orig.js 2026-10-22 14:31:29.227176574 -0400 +++ fixed.js 2026-10-22 14:31:32.465193753 -0400 @@ -1,2 +1,2 @@ -console.log("Value: " + tmp); +console.log(`Value: ${ tmp}`); let tmp = "initialized";
r2-p001 · Comma Operator Grouping Changed
1let r = '';
2const obj = { toString: () => { r += 'x'; return 42; } };
3console.log(('start' + (obj, obj)) === ('start' + `${obj}${obj}`));
--- orig.js 2026-10-22 14:31:40.059236679 -0400 +++ fixed.js 2026-10-22 14:31:42.143249108 -0400 @@ -1,3 +1,3 @@ let r = ''; const obj = { toString: () => { r += 'x'; return 42; } }; -console.log(('start' + (obj, obj)) === ('start' + `${obj}${obj}`)); +console.log((`start${ obj, obj}`) === ('start' + `${obj}${obj}`));
false
r2-p002 · Label/Break Control Flow Interference (generated program does not parse: /home/sun/dev/diaghunt/runs/2026-10-22T18-30-3)
1outer: {
2 try {
3 if (true) break outer;
4 console.log('unreachable');
5 } finally {
6 console.log('finally ran');
7 }
8}
9console.log('done' + (() => { break outer; })());
r2-p003 · Left-to-Right Evaluation Order Side Effects (generated program does not parse: /home/sun/dev/diaghunt/runs/2026-10-22T18-30-3)
1let log = '';
2const getA = () => { log += 'a'; return 'A'; };
3const getB = () => { log += 'b'; return 'B'; };
4console.log(getA() + (getB(), log += 'side', getB()) + '': log);
r2-p004 · Precedence in Larger Expression
1console.log(1 + 2 * '3' + 4); // Before: 1+(2*'3')+4 = 1+6+4=11
2 // After fix would be same due to precedence, but we test coercion:
3console.log(typeof (1 + 2 * '3' + 4));
11 number
r2-p005 · Var Hoisting and Re-declaration
1for (var i = 0; i < 2; i++) {
2 var msg = 'i:' + i;
3 console.log(msg);
4 if (i === 0) var msg = 'redeclared'; // hoisted, but block-scoped effect
5}
6console.log('final i:', i);
--- orig.js 2026-10-22 14:31:40.062236697 -0400 +++ fixed.js 2026-10-22 14:31:44.524263645 -0400 @@ -1,5 +1,5 @@ for (var i = 0; i < 2; i++) { - var msg = 'i:' + i; + var msg = `i:${ i}`; console.log(msg); if (i === 0) var msg = 'redeclared'; // hoisted, but block-scoped effect }
i:0 i:1 final i: 2
r2-p006 · Order of Evaluation - Side Effects Run Twice/Never
1let count = 0;
2const inc = () => ++count;
3console.log(('a' + inc()) + ('b' + inc()), 'count=' + count);
--- orig.js 2026-10-22 14:31:40.062236697 -0400 +++ fixed.js 2026-10-22 14:31:44.456263225 -0400 @@ -1,3 +1,3 @@ let count = 0; const inc = () => ++count; -console.log(('a' + inc()) + ('b' + inc()), 'count=' + count); +console.log(`a${ inc() }b${ inc()}`, `count=${ count}`);
a1b2 count=2
r2-p007 · Sequence Expression with Ignored Result
1let used = 0;
20, used = ('x' + 'y'), console.log(used); // Sequence: assigns then logs
3// Rule does not fire on 'x'+'y' here because it's inside sequence? Actually it should.
4// But we want near-miss: make it so rule doesn't fire due to context.
5// Let's try: if the '+' is in a sequence but the whole thing is ignored...
60, ('ignored' + 'text'); // This should trigger rule but result ignored
7console.log('side effect only if assignment');
xy side effect only if assignment
r2-p008 · Getter Side Effects and Evaluation Order
1const obj = {
2 get a() { return console.log('get a'), 1; },
3 get b() { return console.log('get b'), 2; }
4};
5console.log('Result: ' + obj.a + obj.b);
--- orig.js 2026-10-22 14:31:40.092236875 -0400 +++ fixed.js 2026-10-22 14:31:43.862259565 -0400 @@ -2,4 +2,4 @@ get a() { return console.log('get a'), 1; }, get b() { return console.log('get b'), 2; } }; -console.log('Result: ' + obj.a + obj.b); +console.log(`Result: ${ obj.a }${obj.b}`);
get a get b Result: 12