fix(template-no-template-lint-directives): preserve directive scope on conversion - #2833
Draft
johanrd wants to merge 1 commit into
Draft
fix(template-no-template-lint-directives): preserve directive scope on conversion#2833johanrd wants to merge 1 commit into
johanrd wants to merge 1 commit into
Conversation
johanrd
marked this pull request as draft
July 31, 2026 13:56
johanrd
force-pushed
the
fix/template-lint-directive-next-line-scope
branch
from
July 31, 2026 14:43
b0dd449 to
a796013
Compare
…n conversion
The fixer emitted `eslint-disable` for every `template-lint-disable`, but that
directive carries three different scopes depending on placement, and only one
of them is file-wide. ember-template-lint's docs/configuration.md:
An in-element instruction will apply to only that element
An in-element instruction with the `-tree` suffix will apply to that element
and all its descendants
Converting either in-element form to a file-wide `eslint-disable` widened a
one-element suppression into the rest of the file, so a single migrated
directive silently swallowed every later violation of that rule:
<div
{{! template-lint-disable no-positive-tabindex }}
tabindex="1"
></div>
<span tabindex="2"></span>
ember-template-lint reports the <span>; after `--fix` nothing reported at all.
ESLint has no element scope, but an `eslint-disable` / `eslint-enable` pair
delimits an arbitrary region, which reproduces every template-lint scope
exactly. Closing the region as the element's first child covers the opening tag
alone; closing it after the element covers the subtree. So:
standing alone -> eslint-disable (rest of file)
in opening tag -> eslint-disable before element, eslint-enable first child
`-tree` suffix -> eslint-disable before element, eslint-enable after it
`-tree` directives were previously not matched at all and migrated to nothing
without a report; they now convert.
Removing the converted directive is by line only when the comment has that line
to itself. A directive written inline — `<div {{! template-lint-disable … }}
tabindex="1">` — shares its line with the tag, and reaching back to the line
start would delete `<div ` along with the comment, so the inline form strips
just the comment and the single space it sat in.
The closing comment is inserted without a surrounding newline: `{{! }}`
compiles away, but a newline would add a whitespace text node and can change
inline layout.
Tests assert suppression scope end-to-end — migrate, then lint — against the
scopes ember-template-lint 7.9.3 produces on the same templates, rather than
only checking the rewritten comment text.
The rule doc claimed the file-wide output was correct and that `-tree` had no
ESLint equivalent; the README claimed only two scopes were expressible. Both
are corrected.
johanrd
force-pushed
the
fix/template-lint-directive-next-line-scope
branch
from
August 2, 2026 18:04
a796013 to
a4aadc6
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
{{! template-lint-disable }}does not have one scope. Per ember-template-lint'sdocs/configuration.md:Standing alone it runs from the comment to the end of the template; inside an element's opening tag it covers that element; with
-treeit covers the subtree.This rule converted all of them to
eslint-disable, which is file-wide. That silently widened the element-scoped forms:ember-template-lint reports the
<span>. Aftereslint --fix, nothing reported at all — one migrated directive swallowed a real violation, and every later violation of that rule in the file.That is the worst failure mode for a migration tool: it rewrites a codebase and quietly turns off the linting you were migrating to, with no diagnostic. It surfaced as three unrelated a11y rules reported as "no longer reporting failures that ember-template-lint reported"; differential testing cleared those rules, and the losses traced back here.
-treewas a second hole: it never matched the rule at all, so those directives migrated to nothing, also without a report.The fix
ESLint's own scopes are the next line and the rest of the file — neither matches an element. But an
eslint-disable/eslint-enablepair delimits an arbitrary region, and bracketing the element reproduces both in-element scopes exactly. Closing the region as the element's first child covers the opening tag alone; closing it after the element covers the subtree:eslint-disable(rest of file)template-lint-enableeslint-disable…eslint-enableeslint-disablebefore element,eslint-enableas first child-treesuffixeslint-disablebefore element,eslint-enableafter itThe closing comment is inserted without a surrounding newline:
{{! }}compiles away and leaves no trace in the DOM, but a newline would add a whitespace text node and can change inline layout.Verification
Each scope checked against ember-template-lint 7.9.3 on the same template, using a fixture where every element carries a distinct
tabindexso violations stay identifiable after the fixer shifts lines:--fixdisabletabindex="2",tabindex="3"tabindex="2",tabindex="3"disable-treetabindex="3"tabindex="3"disableAlso covered: nested in-tag directives (converted across several fix passes — asserted to converge and to keep each scope), two directives on one element, and self-closing elements. Tests assert suppression scope end-to-end — migrate, then lint — rather than only the rewritten comment text, which was never the part that broke.
Relation to earlier attempts
Two closed PRs went at this gap from the processor direction — teaching ESLint to honour
template-lint-*comments natively, so codebases keep their existing directives:template-lint-disableprocessor. Next-line only; it explicitly did not implementtemplate-lint-enable,-next-lineor-tree. Closed after review, with a perf benchmark requested since every file pays a preprocess cost.template-lint-enablerange semantics, on the reasoning that "projects migrating from ember-template-lint have existing block comments written for ETL's semantics. With next-line-only behaviour those comments silently do nothing beyond the first line, making the migration painful."That second PR had already identified the right diagnosis — ranges are the missing piece — and this PR reaches the same semantics from the other side. Rather than emulating template-lint's range behaviour in a processor, it emits ESLint's own
eslint-disable/eslint-enable, which already provides ranges natively. No processor, no per-file preprocess cost, and no second directive system to keep in step with ESLint.It also extends the idea past where #3 stopped. #3 covered the standalone
disable … enablerange; the in-element and-treescopes are the ones that were actually losing violations here, and a region pair expresses those too — the element's first child and the element's end are just two more range boundaries.And it answers the question left open on the #2627 thread — "maybe it is enough with next line + whole file disable?" — with a measured no. Next-line is too narrow for a multi-line element, whole-file is too wide, and the pair is what closes the gap.
Behaviour change
User-visible for anyone on the
template-lint-migrationconfig. Newly converted templates preserve scope exactly, so--fixshould not change which violations report. Templates converted by an earlier version keep their over-broad file-wide directives and want a manual pass — re-running--fixwill not find them, since they no longer match.Docs
Three claims were wrong and are corrected: the rule doc presented the file-wide output as correct (contradicting the README, which is what made the behaviour look deliberate); the rule doc said
-treehas no ESLint equivalent; and the README said only two scopes are expressible.