Skip to content

Commit b81e6b1

Browse files
Merge pull request #149 from ember-tooling/copilot/add-hbs-parser-printer
Create hbs parser integration
2 parents 79c78cf + b271c89 commit b81e6b1

9 files changed

Lines changed: 390 additions & 1 deletion

File tree

README.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# ember-eslint-parser
22

3-
This is the eslint parser for ember's gjs and gts files (using `<template>`).
3+
This is the eslint parser for ember's gjs and gts files (using `<template>`), and also for Handlebars (`.hbs`) template files.
44

55
It is meant to be used with [eslint-plugin-ember](https://github.com/ember-cli/eslint-plugin-ember), which provides nice defaults for all the different file types in ember projects.
66

@@ -31,6 +31,26 @@ It's recommended to only use _overrides_ when defining your eslint config, so us
3131
if we detect a typescript parser, it will also be used for all files, otherwise babel parser will be used.
3232
If we cannot find a typescript parser when linting gts we throw an error.
3333

34+
## HBS (Handlebars) support
35+
36+
For `.hbs` template files, use the `ember-eslint-parser/hbs` parser. In ESLint's flat config format (ESLint 9+):
37+
38+
```js
39+
// eslint.config.mjs
40+
import hbsParser from 'ember-eslint-parser/hbs';
41+
42+
export default [
43+
{
44+
files: ['**/*.hbs'],
45+
languageOptions: {
46+
parser: hbsParser,
47+
},
48+
},
49+
];
50+
```
51+
52+
> **Note:** In `.hbs` files, all locals not defined in the template are assumed to be defined at runtime. This avoids false-positive `no-undef` errors for template identifiers. This is a known limitation of the classic HBS format — use `.gjs`/`.gts` for full static analysis.
53+
3454
## Support
3555

3656
eslint-plugin-ember is the primary consumer of this parser library, so SemVer _may_ not be respected for other consumers.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"author": "",
1212
"exports": {
1313
".": "./src/parser/gjs-gts-parser.js",
14+
"./hbs": "./src/parser/hbs-parser.js",
1415
"./noop": "./src/preprocessor/noop.js"
1516
},
1617
"main": "src/parser/gjs-gts-parser.js",

pnpm-lock.yaml

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/parser/hbs-parser.js

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
const eslintScope = require('eslint-scope');
2+
const DocumentLines = require('../utils/document');
3+
const { processGlimmerTemplate, buildGlimmerVisitorKeys } = require('./transforms');
4+
5+
/**
6+
* implements https://eslint.org/docs/latest/extend/custom-parsers
7+
* for Handlebars (.hbs) template files.
8+
*
9+
* The entire file is treated as a Glimmer template.
10+
* All locals not defined in the template are assumed to be defined
11+
* (no no-undef errors for template identifiers).
12+
*/
13+
14+
/**
15+
* @type {import('eslint').ParserModule}
16+
*/
17+
module.exports = {
18+
meta: {
19+
name: 'ember-eslint-parser/hbs',
20+
version: '*',
21+
},
22+
23+
parseForESLint(code, options) {
24+
const filePath = (options && options.filePath) || '<hbs>';
25+
const codeLines = new DocumentLines(code);
26+
27+
let result;
28+
try {
29+
result = processGlimmerTemplate({
30+
templateContent: code,
31+
codeLines,
32+
templateRange: [0, code.length],
33+
});
34+
} catch (e) {
35+
// Transform glimmer parse error to ESLint-compatible error
36+
const loc = e.location || (e.hash && e.hash.loc);
37+
if (loc && loc.start) {
38+
const err = Object.assign(new SyntaxError(e.message), {
39+
lineNumber: loc.start.line,
40+
column: loc.start.column,
41+
index: codeLines.positionToOffset(loc.start),
42+
fileName: filePath,
43+
});
44+
throw err;
45+
}
46+
throw e;
47+
}
48+
49+
const { ast: templateNode, comments } = result;
50+
51+
// Wrap in a synthetic Program node (required by ESLint)
52+
const program = {
53+
type: 'Program',
54+
body: [templateNode],
55+
tokens: templateNode.tokens,
56+
comments,
57+
range: [0, code.length],
58+
start: 0,
59+
end: code.length,
60+
loc: {
61+
start: { line: 1, column: 0 },
62+
end: codeLines.offsetToPosition(code.length),
63+
},
64+
};
65+
66+
// Build visitor keys: Program + all Glimmer node types
67+
const visitorKeys = { Program: ['body'], ...buildGlimmerVisitorKeys() };
68+
69+
// Create an empty scope manager.
70+
// For HBS, all locals are assumed to be defined at runtime,
71+
// so we don't track variable references (no no-undef errors).
72+
const scopeManager = eslintScope.analyze(
73+
{
74+
type: 'Program',
75+
body: [],
76+
range: [0, code.length],
77+
loc: program.loc,
78+
},
79+
{ range: true }
80+
);
81+
82+
return {
83+
ast: program,
84+
scopeManager,
85+
visitorKeys,
86+
services: {},
87+
};
88+
},
89+
};
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/**
2+
* Debugging:
3+
* https://eslint.org/docs/latest/use/configure/debug
4+
* ----------------------------------------------------
5+
*
6+
* Print a file's calculated configuration
7+
*
8+
* npx eslint --print-config path/to/file.hbs
9+
*
10+
* Inspecting the config
11+
*
12+
* npx eslint --inspect-config
13+
*
14+
*/
15+
import hbsParser from 'ember-eslint-parser/hbs';
16+
17+
export default [
18+
{
19+
files: ['**/*.hbs'],
20+
languageOptions: {
21+
parser: hbsParser,
22+
},
23+
rules: {
24+
'no-restricted-syntax': [
25+
'error',
26+
{
27+
selector: "GlimmerElementNode[name='Input']",
28+
message: "Do not use <Input>; use a native <input> element instead.",
29+
},
30+
],
31+
},
32+
},
33+
];

test-projects/hbs/package.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"name": "@test-project/hbs",
3+
"private": true,
4+
"scripts": {
5+
"test:check": "eslint src --max-warnings=0 --report-unused-disable-directives"
6+
},
7+
"devDependencies": {
8+
"ember-eslint-parser": "workspace:^",
9+
"eslint": "^9.0.0"
10+
}
11+
}

test-projects/hbs/src/disabled.hbs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
{{! eslint-disable-next-line no-restricted-syntax }}
2+
<Input @value={{this.val}} />

test-projects/hbs/src/greeting.hbs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<h1>Hello, {{name}}!</h1>
2+
3+
{{#let (service "router") as |router|}}
4+
<nav>
5+
<a href={{router.currentURL}}>Home</a>
6+
</nav>
7+
{{/let}}

0 commit comments

Comments
 (0)