Skip to content

Point a write of a template at the template, not its placeholder - #258

Merged
NullVoxPopuli merged 2 commits into
ember-tooling:mainfrom
aklkv:fix/template-placeholder-write-expr
Oct 9, 2026
Merged

NullVoxPopuli merged 2 commits into
ember-tooling:mainfrom
aklkv:fix/template-placeholder-write-expr

Conversation

@aklkv

@aklkv aklkv commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Fixes #257.

ESLint's require-atomic-updates crashes on a template written inside an async function:

export async function load(store) {
  const Greeting = <template>Hello</template>;
  await store.fetch();
  return Greeting;
}
// TypeError: Cannot read properties of undefined (reading 'right')

Scope analysis runs on the placeholder JS, where each template is a template literal. After the GlimmerTemplate is spliced in, a write whose value is a template keeps the placeholder as its writeExpr: a const/let initializer, an assignment, a compound or logical assignment (??=, +=), or a default parameter. The placeholder is no longer in the AST, so ESLint never sets its parent, and the rule reads writeExpr.parent.right (or .operator for a compound assignment).

Fix

After toTree, map each placeholder to its GlimmerTemplate from templateInfos and point any reference whose writeExpr is a placeholder at the template, the way esTreeNodeToTSNodeMap is already forwarded. The pass runs over the scope manager's references once per parse; on tests/bench/large.gts it takes about 1.5 µs of a ~4.7 ms parse.

Tests

  • points a write of a template at the template runs require-atomic-updates over a const, an assignment, a ??= and a default parameter for .gjs, .gts and .gjs with useBabel, and checks each write reference's writeExpr is the GlimmerTemplate. All three fail without the change.
  • test-projects/gjs/src/require-atomic-updates.gjs with the rule on for that file, and a test:check:require-atomic-updates script that requires it to lint clean. On main it crashes.

Also run: the full suite, lint, the test projects, eslint-plugin-ember's suite (scripts/eslint-plugin-ember-test.mjs) and the CI matrix's ^6/^7 overrides.

Not covered

  • The rule no longer crashes but can't report a race on a template-valued variable. It checks assignments on :expression:exit, and esquery's :expression doesn't match GlimmerTemplate, so shared = <template>…</template> after an await goes unreported where the same code with a string is reported.
  • Scopes the scope manager creates on a placeholder (a class field initializer's, or a class-member template's static {} block) still have the placeholder as their block. No rule I tried crashes on them, so they're left as they are.

@github-actions

github-actions Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

🏎️ Benchmark Comparison

Parse

Benchmark Control (p50) Experiment (p50) Δ
🟢 gts small 995.65 ms 935.98 ms -6.0%
🟠 gts medium 1620.03 ms 1675.18 ms +3.4%
⚪ gts large 793.32 ms 781.91 ms -1.4%
⚪ gjs small 798.50 ms 805.74 ms +0.9%
🟢 gjs medium 1420.88 ms 1390.18 ms -2.2%
🟢 gjs large 652.51 ms 638.76 ms -2.1%
🟢 hbs small 139.27 ms 135.99 ms -2.4%
🟢 hbs medium 778.80 ms 754.21 ms -3.2%
🟠 hbs large 831.38 ms 865.65 ms +4.1%

🟢 faster · 🔴 slower · 🟠 slightly slower · ⚪ within 2%

Full mitata output
clk: ~3.10 GHz
cpu: AMD EPYC 7763 64-Core Processor
runtime: node 24.21.0 (x64-linux)

benchmark                   avg (min … max) p75 / p99    (min … top 1%)
------------------------------------------- -------------------------------
gts small (control)             1.01 s/iter    1.03 s         █            
                       (827.89 ms … 1.31 s)    1.27 s    █    █            
                    ( 14.09 mb …  89.73 mb)  38.62 mb ██▁█▁▁▁███▁█▁▁▁▁▁▁▁▁█

gts small (experiment)       963.28 ms/iter 991.89 ms   █                  
                       (835.45 ms … 1.24 s)    1.12 s ▅ █▅▅  ▅▅ ▅▅  ▅     ▅
                    ( 14.47 mb …  90.81 mb)  34.41 mb █▁███▁▁██▁██▁▁█▁▁▁▁▁█

                             ┌                                            ┐
                             ╷      ┌───────────┬─┐                       ╷
         gts small (control) ├──────┤           │ ├───────────────────────┤
                             ╵      └───────────┴─┘                       ╵
                              ╷  ┌─────────┬──┐            ╷
      gts small (experiment)  ├──┤         │  ├────────────┤
                              ╵  └─────────┴──┘            ╵
                             └                                            ┘
                             827.89 ms            1.05 s             1.27 s

summary
  gts small (experiment)
   1.05x faster than gts small (control)

------------------------------------------- -------------------------------
gts medium (control)            1.69 s/iter    1.72 s                   █  
                          (1.54 s … 2.13 s)    1.74 s ▅  ▅▅▅▅ ▅     ▅  ▅█ ▅
                    ( 82.00 mb … 137.19 mb)  91.15 mb █▁▁████▁█▁▁▁▁▁█▁▁██▁█

gts medium (experiment)         1.71 s/iter    1.71 s █          █    █    
                          (1.59 s … 2.18 s)    1.75 s █▅   ▅    ▅█  ▅ █   ▅
                    ( 83.01 mb … 101.47 mb)  87.03 mb ██▁▁▁█▁▁▁▁██▁▁█▁█▁▁▁█

                             ┌                                            ┐
                             ╷        ┌──────────────────────┬──────┐    ╷
        gts medium (control) ├────────┤                      │      ├────┤
                             ╵        └──────────────────────┴──────┘    ╵
                                       ╷ ┌───────────────────────┬─┐      ╷
     gts medium (experiment)           ├─┤                       │ ├──────┤
                                       ╵ └───────────────────────┴─┘      ╵
                             └                                            ┘
                             1.54 s              1.65 s              1.75 s

summary
  gts medium (control)
   1.01x faster than gts medium (experiment)

------------------------------------------- -------------------------------
gts large (control)          837.22 ms/iter 832.84 ms   █   █              
                       (764.23 ms … 1.23 s) 868.02 ms ▅ █▅▅ █▅     ▅ ▅    ▅
                    ( 15.03 mb …  85.87 mb)  33.68 mb █▁███▁██▁▁▁▁▁█▁█▁▁▁▁█

gts large (experiment)       839.53 ms/iter 845.42 ms    █                 
                       (754.21 ms … 1.19 s) 925.54 ms █  █                 
                    ( 16.47 mb …  88.90 mb)  34.72 mb ██▁███▁▁▁▁▁█▁▁▁▁▁▁▁██

                             ┌                                            ┐
                                ╷ ┌────────────────┬       ╷
         gts large (control)    ├─┤                │───────┤
                                ╵ └────────────────┴       ╵
                             ╷ ┌───────────────────┬─┐                    ╷
      gts large (experiment) ├─┤                   │ ├────────────────────┤
                             ╵ └───────────────────┴─┘                    ╵
                             └                                            ┘
                             754.21 ms         839.88 ms          925.54 ms

summary
  gts large (control)
   1x faster than gts large (experiment)

------------------------------------------- -------------------------------
gjs small (control)          835.05 ms/iter 839.37 ms    █ █   █           
                       (747.97 ms … 1.13 s) 957.64 ms ▅▅▅█ █▅  █          ▅
                    ( 30.92 mb …  86.34 mb)  38.17 mb ████▁██▁▁█▁▁▁▁▁▁▁▁▁▁█

gjs small (experiment)       836.51 ms/iter 848.38 ms            █         
                       (760.52 ms … 1.12 s) 914.90 ms ▅▅▅▅▅ ▅▅▅  █        ▅
                    ( 31.55 mb …  47.20 mb)  34.27 mb █████▁███▁▁█▁▁▁▁▁▁▁▁█

                             ┌                                            ┐
                             ╷   ┌──────────────┬┐                        ╷
         gjs small (control) ├───┤              │├────────────────────────┤
                             ╵   └──────────────┴┘                        ╵
                                ╷ ┌─────────────┬──┐             ╷
      gjs small (experiment)    ├─┤             │  ├─────────────┤
                                ╵ └─────────────┴──┘             ╵
                             └                                            ┘
                             747.97 ms         852.81 ms          957.64 ms

summary
  gjs small (control)
   1x faster than gjs small (experiment)

------------------------------------------- -------------------------------
gjs medium (control)            1.46 s/iter    1.46 s       █   █   █      
                          (1.33 s … 1.88 s)    1.51 s ▅    ▅█ ▅ █   █   ▅ ▅
                    ( 20.91 mb …  90.60 mb)  72.01 mb █▁▁▁▁██▁█▁█▁▁▁█▁▁▁█▁█

gjs medium (experiment)         1.45 s/iter    1.48 s    █                 
                          (1.35 s … 1.85 s)    1.51 s    █                 
                    ( 20.70 mb …  93.25 mb)  72.39 mb ▇▁▁█▁▇▁▁▇▁▁▁▇▁▁▁▇▇▁▁▇

                             ┌                                            ┐
                             ╷            ┌───────────────────┬           ╷
        gjs medium (control) ├────────────┤                   │───────────┤
                             ╵            └───────────────────┴           ╵
                                 ╷     ┌───────────────────┬──────┐      ╷
     gjs medium (experiment)     ├─────┤                   │      ├──────┤
                                 ╵     └───────────────────┴──────┘      ╵
                             └                                            ┘
                             1.33 s              1.42 s              1.51 s

summary
  gjs medium (experiment)
   1.01x faster than gjs medium (control)

------------------------------------------- -------------------------------
gjs large (control)          694.82 ms/iter 672.98 ms   █                  
                    (636.85 ms … 990.11 ms) 770.30 ms  ██  █               
                    (  1.56 mb …  70.39 mb)  44.37 mb ███▁██▁▁▁█▁▁▁▁▁▁▁▁▁▁█

gjs large (experiment)       684.75 ms/iter 669.56 ms   ██                 
                    (611.34 ms … 979.92 ms) 795.54 ms ▅▅██▅▅▅  ▅          ▅
                    (  1.75 mb …  72.60 mb)  45.19 mb ███████▁▁█▁▁▁▁▁▁▁▁▁▁█

                             ┌                                            ┐
                                   ╷ ┌───────────┬                  ╷
         gjs large (control)       ├─┤           │──────────────────┤
                                   ╵ └───────────┴                  ╵
                             ╷    ┌────────────┬                          ╷
      gjs large (experiment) ├────┤            │──────────────────────────┤
                             ╵    └────────────┴                          ╵
                             └                                            ┘
                             611.34 ms         703.44 ms          795.54 ms

summary
  gjs large (experiment)
   1.01x faster than gjs large (control)

------------------------------------------- -------------------------------
hbs small (control)          149.91 ms/iter 151.19 ms     █     █          
                    (114.55 ms … 286.95 ms) 163.71 ms ▅ ▅ █  ▅  █   ▅▅ ▅  ▅
                    ( 51.34 mb …  54.61 mb)  53.02 mb █▁█▁█▁▁█▁▁█▁▁▁██▁█▁▁█

hbs small (experiment)       152.24 ms/iter 154.55 ms                     █
                    (111.38 ms … 300.88 ms) 161.42 ms ▅  ▅  ▅▅ ▅▅ ▅   ▅▅  █
                    ( 51.24 mb …  54.93 mb)  53.37 mb █▁▁█▁▁██▁██▁█▁▁▁██▁▁█

                             ┌                                            ┐
                                ╷       ┌─────────────────────┬┐          ╷
         hbs small (control)    ├───────┤                     │├──────────┤
                                ╵       └─────────────────────┴┘          ╵
                             ╷            ┌─────────────────────┬─┐     ╷
      hbs small (experiment) ├────────────┤                     │ ├─────┤
                             ╵            └─────────────────────┴─┘     ╵
                             └                                            ┘
                             111.38 ms         137.54 ms          163.71 ms

summary
  hbs small (control)
   1.02x faster than hbs small (experiment)

------------------------------------------- -------------------------------
hbs medium (control)         824.17 ms/iter 812.64 ms ███                  
                       (768.65 ms … 1.13 s) 870.99 ms ███ ▅▅   ▅     ▅    ▅
                    (  8.15 mb …  82.94 mb)  33.06 mb ███▁██▁▁▁█▁▁▁▁▁█▁▁▁▁█

hbs medium (experiment)      788.61 ms/iter 771.24 ms  ███  █              
                       (737.70 ms … 1.01 s) 849.16 ms ▅███  █          ▅  ▅
                    (  9.16 mb …  82.80 mb)  33.10 mb ████▁▁█▁▁▁▁▁▁▁▁▁▁█▁▁█

                             ┌                                            ┐
                                       ╷ ┌────────────────┬               ╷
        hbs medium (control)           ├─┤                │───────────────┤
                                       ╵ └────────────────┴               ╵
                             ╷ ┌──────────────┬                    ╷
     hbs medium (experiment) ├─┤              │────────────────────┤
                             ╵ └──────────────┴                    ╵
                             └                                            ┘
                             737.70 ms         804.34 ms          870.99 ms

summary
  hbs medium (experiment)
   1.05x faster than hbs medium (control)

------------------------------------------- -------------------------------
hbs large (control)          860.64 ms/iter 840.02 ms   █                  
                       (824.60 ms … 1.12 s) 885.43 ms  ▅█                  
                    ( 53.48 mb …  64.88 mb)  55.52 mb ▇██▇▁▇▁▇▁▁▁▁▁▁▁▁▁▁▁▁▇

hbs large (experiment)       892.71 ms/iter 869.48 ms  █ █                 
                       (857.56 ms … 1.12 s) 930.06 ms  ███                 
                    ( 53.48 mb …  64.85 mb)  55.52 mb ████▁▁▁█▁▁▁▁▁▁▁▁▁▁▁▁█

                             ┌                                            ┐
                             ╷┌─────────────┬          ╷
         hbs large (control) ├┤             │──────────┤
                             ╵└─────────────┴          ╵
                                           ╷ ┌────────────┬               ╷
      hbs large (experiment)               ├─┤            │───────────────┤
                                           ╵ └────────────┴               ╵
                             └                                            ┘
                             824.60 ms         877.33 ms          930.06 ms

summary
  hbs large (control)
   1.04x faster than hbs large (experiment)
### Project mode (type-aware)
Benchmark Control (p50) Experiment (p50) Δ
⚪ project warm parse x150 627.41 ms 630.68 ms +0.5%
🟢 project parse+typed x150 668.00 ms 630.48 ms -5.6%
⚪ projectService warm parse x150 617.17 ms 612.79 ms -0.7%
🟢 projectService parse+typed x150 679.22 ms 658.22 ms -3.1%

🟢 faster · 🔴 slower · 🟠 slightly slower · ⚪ within 2%

Full mitata output
clk: ~3.02 GHz
cpu: AMD EPYC 7763 64-Core Processor
runtime: node 24.21.0 (x64-linux)

benchmark                                   avg (min … max) p75 / p99    (min … top 1%)
----------------------------------------------------------- -------------------------------
project warm parse x150 (control)            633.13 ms/iter 639.37 ms           █          
                                    (588.07 ms … 711.68 ms) 666.59 ms █▁▁▁██▁██▁██▁█▁▁▁█▁▁█
                                  gc(281.61 ms … 310.17 ms) 113.91 mb (112.71 mb…115.08 mb)

project warm parse x150 (experiment)         632.17 ms/iter 635.95 ms             █▃▃      
                                    (589.35 ms … 660.65 ms) 654.10 ms ▆▁▁▁▁▁▁▁▁▆▁▁███▁▁▁▆▁▆
                                  gc(287.30 ms … 301.63 ms) 112.20 mb (111.66 mb…113.81 mb)

                                             ┌                                            ┐
                                             ╷          ┌──────────────┬──┐               ╷
           project warm parse x150 (control) ├──────────┤              │  ├───────────────┤
                                             ╵          └──────────────┴──┘               ╵
                                              ╷                    ┌──┬─┐          ╷
        project warm parse x150 (experiment)  ├────────────────────┤  │ ├──────────┤
                                              ╵                    └──┴─┘          ╵
                                             └                                            ┘
                                             588.07 ms         627.33 ms          666.59 ms

summary
  project warm parse x150 (experiment)
   1x faster than project warm parse x150 (control)

----------------------------------------------------------- -------------------------------
project parse+typed x150 (control)           676.58 ms/iter 688.10 ms     █                
                                    (635.41 ms … 731.04 ms) 726.80 ms █▁▁██▁███▁█▁██▁▁▁▁▁▁█
                                  gc(295.18 ms … 314.17 ms) 119.06 mb (117.71 mb…122.55 mb)

project parse+typed x150 (experiment)        638.93 ms/iter 643.60 ms             █        
                                    (615.95 ms … 695.06 ms) 660.46 ms ▆▁▆▆▁▆▆▆▆▁▁▁█▁▁▁▁▁▁▁▆
                                  gc(295.16 ms … 308.93 ms) 111.20 mb (107.41 mb…116.02 mb)

                                             ┌                                            ┐
                                                     ╷      ┌─────────┬───┐               ╷
          project parse+typed x150 (control)         ├──────┤         │   ├───────────────┤
                                                     ╵      └─────────┴───┘               ╵
                                             ╷  ┌─────┬─┐      ╷
       project parse+typed x150 (experiment) ├──┤     │ ├──────┤
                                             ╵  └─────┴─┘      ╵
                                             └                                            ┘
                                             615.95 ms         671.38 ms          726.80 ms

summary
  project parse+typed x150 (experiment)
   1.06x faster than project parse+typed x150 (control)

----------------------------------------------------------- -------------------------------
projectService warm parse x150 (control)     622.55 ms/iter 624.41 ms        ▃   █         
                                    (605.64 ms … 656.01 ms) 640.20 ms ▆▁▆▆▁▆▁█▁▁▁█▁▆▁▁▁▁▁▁▆
                                  gc(299.55 ms … 336.95 ms) 115.20 mb (113.70 mb…116.16 mb)

projectService warm parse x150 (experiment)  613.49 ms/iter 616.70 ms             █        
                                    (599.22 ms … 630.62 ms) 623.21 ms █▁▁█▁█▁▁▁████▁▁█▁▁▁██
                                  gc(292.32 ms … 310.37 ms) 112.19 mb (111.66 mb…112.88 mb)

                                             ┌                                            ┐
                                                    ╷     ┌────────────┬─┐                ╷
    projectService warm parse x150 (control)        ├─────┤            │ ├────────────────┤
                                                    ╵     └────────────┴─┘                ╵
                                             ╷     ┌─────────┬──┐      ╷
 projectService warm parse x150 (experiment) ├─────┤         │  ├──────┤
                                             ╵     └─────────┴──┘      ╵
                                             └                                            ┘
                                             599.22 ms         619.71 ms          640.20 ms

summary
  projectService warm parse x150 (experiment)
   1.01x faster than projectService warm parse x150 (control)

----------------------------------------------------------- -------------------------------
projectService parse+typed x150 (control)    687.82 ms/iter 689.30 ms   █   █ █            
                                    (647.41 ms … 770.87 ms) 754.18 ms ███▁█▁█▁█▁▁█▁▁▁▁▁▁▁▁█
                                  gc(300.28 ms … 332.76 ms) 119.69 mb (118.96 mb…123.53 mb)

projectService parse+typed x150 (experiment) 670.13 ms/iter 665.64 ms  █         ▃         
                                    (652.51 ms … 774.31 ms) 676.67 ms ▆█▆▁▁▆▁▁▁▁▆█▁▆▁▁▁▁▁▁▆
                                  gc(300.95 ms … 324.65 ms) 116.36 mb (115.38 mb…119.97 mb)

                                             ┌                                            ┐
                                             ╷   ┌────────────┬┐                          ╷
   projectService parse+typed x150 (control) ├───┤            │├──────────────────────────┤
                                             ╵   └────────────┴┘                          ╵
                                               ╷┌──────┬ ╷
projectService parse+typed x150 (experiment)   ├┤      │─┤
                                               ╵└──────┴ ╵
                                             └                                            ┘
                                             647.41 ms         700.79 ms          754.18 ms

summary
  projectService parse+typed x150 (experiment)
   1.03x faster than projectService parse+typed x150 (control)

Comment thread tests/parser.test.js
// ember-tooling/ember-eslint-parser#257: scope analysis runs on the
// placeholder JS, so a write of a template kept the placeholder, which is
// not in the AST, as its writeExpr.
it.each([

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's a deliberate error look like?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added one in 67fd500: the same function now also does count += await store.fetch() on a module-level count, and the test expects exactly that report (Possible race condition: \count` might be reassigned based on an outdated value of `count`.`). So the rule still reports with templates around, rather than only not crashing.

A race where the template itself is the assigned value (Comp = <template>…</template> after an await on a stale read) still isn't reported: the rule checks assignments on :expression:exit, and :expression doesn't match GlimmerTemplate. That's in the PR description under "Not covered".

aklkv added 2 commits October 9, 2026 12:19
Scope analysis runs on the placeholder JS, where each template is a
template literal. After the GlimmerTemplate is spliced in, a write whose
value is a template (`const Foo = <template>`, an assignment, a compound
or logical assignment, a default parameter) keeps the placeholder as its
`writeExpr`. The placeholder is no longer in the AST, so ESLint never sets
its `parent`, and require-atomic-updates crashes reading
`writeExpr.parent.right` or `writeExpr.parent.operator` inside an async
function.

Swap those references' `writeExpr` for the template that replaced the
placeholder, as esTreeNodeToTSNodeMap is already forwarded.

Fixes ember-tooling#257
@aklkv
aklkv force-pushed the fix/template-placeholder-write-expr branch from 67fd500 to 6edd52c Compare October 9, 2026 19:20
@NullVoxPopuli NullVoxPopuli added the bug Something isn't working label Oct 9, 2026
@NullVoxPopuli
NullVoxPopuli merged commit b764d7a into ember-tooling:main Oct 9, 2026
38 of 39 checks passed
@github-actions github-actions Bot mentioned this pull request Oct 9, 2026
@aklkv
aklkv deleted the fix/template-placeholder-write-expr branch October 9, 2026 20:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

require-atomic-updates crashes on a template assigned inside an async function

2 participants