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
14 changes: 14 additions & 0 deletions src/parser/gjs-gts-parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,20 @@ export function parseForESLint(code, options) {
}
}

// Scope analysis ran on the placeholder JS, so a write of a template
// (`const Foo = <template>`, an assignment, a default parameter) still
// has the placeholder as its writeExpr. The placeholder is out of the
// AST and never gets a `parent`, which rules read (#257).
if (result.scopeManager && result.templateInfos?.length) {
const templates = new Map(result.templateInfos.map((ti) => [ti.placeholder, ti.ast]));
for (const scope of result.scopeManager.scopes) {
for (const ref of scope.references) {
const template = ref.writeExpr && templates.get(ref.writeExpr);
if (template) ref.writeExpr = template;
}
}
}

// Surface Glimmer template comments in program.comments as type:'Block'
// so ESLint's inline-config scanner and plugin rules recognise them.
if (glimmerComments.length > 0) {
Expand Down
7 changes: 7 additions & 0 deletions test-projects/gjs/.eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,12 @@ module.exports = {
plugins: ['ember'],
extends: ['eslint:recommended', 'plugin:ember/recommended', 'plugin:ember/recommended-gjs'],
},
{
// ember-tooling/ember-eslint-parser#257
files: ['src/require-atomic-updates.gjs'],
rules: {
'require-atomic-updates': 'error',
},
},
],
};
1 change: 1 addition & 0 deletions test-projects/gjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"test:check": "pnpm run /test:check:.*/",
"test:check:correct-handle-syntax-error": "eslint . | grep -q \"26:16 error Parsing error: × Expected ',', got '<eof>'\"",
"test:check:only-one-error": "eslint --format compact . | egrep '^[0-9]+ problem[s]*' | wc -l | grep -q 1",
"test:check:require-atomic-updates": "eslint src/require-atomic-updates.gjs --max-warnings=0",
"test:fix": "eslint --fix --format compact . | egrep '^[0-9]+ problem[s]*' | wc -l | grep -q 1"
},
"devDependencies": {
Expand Down
8 changes: 8 additions & 0 deletions test-projects/gjs/src/require-atomic-updates.gjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export async function load(store, Fallback = <template>Loading</template>) {
const Greeting = <template>Hello</template>;
let Farewell;
Farewell = <template>Bye</template>;
Farewell ??= <template>Later</template>;
await store.ready();
return [Fallback, Greeting, Farewell];
}
64 changes: 64 additions & 0 deletions tests/parser.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2577,6 +2577,70 @@ export const NotFound = <template>
{ column: 39, nodeType: 'GlimmerBlockParam' },
]);
});

// 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".

{ filename: 'example.gjs', useBabel: false },
{ filename: 'example.gts', useBabel: false },
{ filename: 'example.gjs', useBabel: true },
])(
'points a write of a template at the template ($filename, useBabel: $useBabel)',
({ filename, useBabel }) => {
const code = [
'let count = 0;',
'export async function load(store, Fallback = <template>Fallback</template>) {',
' const Greeting = <template>Hello</template>;',
' let Farewell;',
' Farewell = <template>Bye</template>;',
' Farewell ??= <template>Later</template>;',
' count += await store.fetch();',
' return [Fallback, Greeting, Farewell];',
'}',
].join('\n');
const parse = (source, options) => parseForESLint(source, { ...options, useBabel });

const linter = new Linter();
linter.defineParser('ember-eslint-parser', { parseForESLint: parse });
const messages = linter.verify(
code,
{
parser: 'ember-eslint-parser',
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
rules: { 'require-atomic-updates': 'error' },
},
{ filename }
);
// The deliberate race on `count` is still reported.
expect(messages.map(({ line, message }) => ({ line, message }))).toEqual([
{
line: 7,
message:
'Possible race condition: `count` might be reassigned based on an outdated value of `count`.',
},
]);

const { scopeManager } = parse(code, {
filePath: filename,
comment: true,
loc: true,
range: true,
tokens: true,
});
const writes = scopeManager.scopes
.flatMap((scope) => scope.references)
.filter((ref) => ref.isWrite());
expect(writes.map((ref) => [ref.identifier.name, ref.writeExpr.type])).toEqual([
['count', 'Literal'],
['Fallback', 'GlimmerTemplate'],
['Greeting', 'GlimmerTemplate'],
['Farewell', 'GlimmerTemplate'],
['Farewell', 'GlimmerTemplate'],
['count', 'AwaitExpression'],
]);
}
);
});

describe('replaceExtensions', () => {
Expand Down
Loading