Added recursive layout detection#816
Conversation
ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (27)
✅ Files skipped from review due to trivial changes (16)
🚧 Files skipped from review as they are similar to previous changes (3)
WalkthroughThis pull request introduces GS130-NO-RECURSIVE-LAYOUT, a new Ghost theme validation check that detects and prevents recursive template layout inheritance. The implementation includes a 154-line check module that extracts Handlebars layout directives from Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
85debbc to
4ebe6a1
Compare
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
test/130-template-inheritance.test.js (1)
1-46:⚠️ Potential issue | 🟠 Major | ⚡ Quick winReplace
shouldassertions with Vitestexpectintest/130-template-inheritance.test.js.This file uses
require('should')andoutput.should.../failure.message.should...assertions; update them to vitest’sexpect(...)style per the test guidelines.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@test/130-template-inheritance.test.js` around lines 1 - 46, Update the test file to replace all "should" style assertions with Vitest "expect" assertions: remove the require('should') line, import Vitest's expect if not global, and change checks on output and results to expect(output).toBeValidThemeObject() (or the appropriate custom matcher), expect(Object.keys(output.results.fail)).toEqual([ruleCode]), expect(output.results.fail).toEqual({}) / expect(output.results.pass).toContain(ruleCode) as applicable, and change failure.message.should.containEql(...) to expect(failure.message).toContain(...) and failure.ref.should.eql('default.hbs') to expect(failure.ref).toEqual('default.hbs'); update all uses of thisCheck, output, failure, and ruleCode accordingly so assertions use expect(...) with toEqual/toContain/toBe... matchers.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@AGENTS.md`:
- Around line 105-114: Update the examples under "Common Test Patterns" to use
vitest's global expect assertions instead of the legacy should style: replace
the `check(...)` usage examples so that the passing test asserts the results
array with expect(theme.results.pass).toContain('GS010-PJ-REQ') (using check and
themePath to locate the theme) and the failing test asserts the specific failure
key with expect(theme.results.fail['GS010-PJ-NAME-REQ']).toBeDefined() or
toBeTruthy(); keep using the same helpers (`check`, `themePath`) and the same
result properties (`theme.results.pass`, `theme.results.fail`) but swap out
`.should.*` expressions for the equivalent `expect(...).toContain` /
`expect(...).toBeDefined` style.
- Around line 162-177: The example exported check function checkSomething is
missing the required third parameter themePath; update its signature to accept
(theme, options, themePath) and ensure it returns a Promise (make it async if
needed), and if options or themePath are unused add the appropriate
eslint-disable comment as noted in the guidelines so the linter won't complain.
- Around line 193-204: Update the test example in the
`test/XXX-check-name.test.js` snippet to use vitest's expect-style assertions
instead of the deprecated should library: keep the existing test structure and
the call to utils.testCheck(checkSomething, 'XXX-check-name/failing-case'), but
replace the old "output.results.fail['GSXXX-ERROR-CODE'].should.exist()"
assertion with a vitest expect assertion (e.g.,
expect(output.results.fail['GSXXX-ERROR-CODE']).toBeDefined() or another
appropriate expect matcher) so it matches the project's vitest examples and
style.
In `@lib/checks/130-template-inheritance.js`:
- Around line 137-154: The exported check function checkTemplateInheritance
currently is a synchronous two-arg function (checkTemplateInheritance(theme,
options)) but must follow the check-module contract: change it to an async
function with signature async function checkTemplateInheritance(theme, options,
themePath) (or equivalent arrow) so it returns a Promise that resolves to theme;
keep the existing logic intact, accept the extra themePath parameter (even if
unused) and ensure the module still exports checkTemplateInheritance so callers
receive a Promise.
---
Outside diff comments:
In `@test/130-template-inheritance.test.js`:
- Around line 1-46: Update the test file to replace all "should" style
assertions with Vitest "expect" assertions: remove the require('should') line,
import Vitest's expect if not global, and change checks on output and results to
expect(output).toBeValidThemeObject() (or the appropriate custom matcher),
expect(Object.keys(output.results.fail)).toEqual([ruleCode]),
expect(output.results.fail).toEqual({}) /
expect(output.results.pass).toContain(ruleCode) as applicable, and change
failure.message.should.containEql(...) to expect(failure.message).toContain(...)
and failure.ref.should.eql('default.hbs') to
expect(failure.ref).toEqual('default.hbs'); update all uses of thisCheck,
output, failure, and ruleCode accordingly so assertions use expect(...) with
toEqual/toContain/toBe... matchers.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: f4b0a8b8-a43b-4a7c-bcec-3e8457482a6e
📒 Files selected for processing (10)
AGENTS.mdlib/checks/130-template-inheritance.jslib/specs/v6.jstest/130-template-inheritance.test.jstest/checker.test.jstest/fixtures/themes/130-template-inheritance/indirect-cycle/default.hbstest/fixtures/themes/130-template-inheritance/indirect-cycle/layouts/base.hbstest/fixtures/themes/130-template-inheritance/recursive-default/default.hbstest/fixtures/themes/130-template-inheritance/valid-default/default.hbstest/fixtures/themes/130-template-inheritance/valid-default/index.hbs
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 85debbc2a6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
4ebe6a1 to
59acb95
Compare
| return `${layoutPath}.hbs`; | ||
| } | ||
|
|
||
| function resolveLayoutPath(sourceFile, layoutName) { |
There was a problem hiding this comment.
One possible issue here:
Ghost uses express-hbs without configuring layoutsDir, so declared layouts resolve relative to the file declaring them, not relative to the theme root.
For example:
theme/
├── default.hbs
└── members/
├── base.hbs
└── default.hbs
If members/base.hbs contains:
Ghost resolves that to:
members/default.hbs
The current implementation appears to resolve it to:
default.hbs
So if there's a theme set up this way we might end up with false positives. However, i had an agent try to find a ghost theme that has a structure like this and (it claimed to) check like 450 of them and didn't find this in the wild lol. So it might not be a real issue.
I don't think it'd happen with default.hbs either, but maybe if someone built a theme with a bunch of layout.hbs at every level or something? i'm not sure.
I think this will fix it and the tests might explain the issue a little better, though i'm not 100% sure if i'm missing something.
I made it as a PR into your branch here, so if you like it you can add it, if not we can merge your change as-is!
There was a problem hiding this comment.
@troyciesco this is a great catch! Thanks a lot for the thorough review, I didn't expect you to open a PR with the fix and have an agent go through existing Ghost themes to double-check 🙏 🙏
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@test/checker.test.js`:
- Around line 10-31: The test currently removes 'testHelper' from
v6Spec.knownHelpers only on the success path, risking cross-test leakage if the
expect fails; move the cleanup that finds and splices 'testHelper' from
v6Spec.knownHelpers into the existing finally block so it always runs: inside
the finally (where delete labsEnabledHelpers.testHelper is done) also retrieve
const v6Spec = spec.get(['v6']) and remove 'testHelper' if present (using
indexOf/splice) to guarantee the knownHelpers array is restored regardless of
test outcome.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: ab4139f6-bdd9-4050-9714-33ec2c3741f3
📒 Files selected for processing (27)
lib/checks/130-template-inheritance.jslib/specs/v6.jstest/130-template-inheritance.test.jstest/checker.test.jstest/fixtures/themes/130-template-inheritance/absolute-default/default.hbstest/fixtures/themes/130-template-inheritance/indirect-root-cycle/base.hbstest/fixtures/themes/130-template-inheritance/indirect-root-cycle/default.hbstest/fixtures/themes/130-template-inheritance/missing-and-empty/empty.hbstest/fixtures/themes/130-template-inheritance/missing-and-empty/index.hbstest/fixtures/themes/130-template-inheritance/nested-cycle/layouts/base.hbstest/fixtures/themes/130-template-inheritance/nested-cycle/layouts/default.hbstest/fixtures/themes/130-template-inheritance/nested-default-target/default.hbstest/fixtures/themes/130-template-inheritance/nested-default-target/layouts/base.hbstest/fixtures/themes/130-template-inheritance/nested-default-target/layouts/default.hbstest/fixtures/themes/130-template-inheritance/nested-shell-target/layout.hbstest/fixtures/themes/130-template-inheritance/nested-shell-target/members/layout.hbstest/fixtures/themes/130-template-inheritance/nested-shell-target/members/shell.hbstest/fixtures/themes/130-template-inheritance/recursive-default/default.hbstest/fixtures/themes/130-template-inheritance/relative-explicit-extension/index.hbstest/fixtures/themes/130-template-inheritance/relative-explicit-extension/layouts/default.hbstest/fixtures/themes/130-template-inheritance/secondary-directive/default.hbstest/fixtures/themes/130-template-inheritance/secondary-directive/index.hbstest/fixtures/themes/130-template-inheritance/shared-layout-target/a.hbstest/fixtures/themes/130-template-inheritance/shared-layout-target/c.hbstest/fixtures/themes/130-template-inheritance/shared-layout-target/shared.hbstest/fixtures/themes/130-template-inheritance/valid-default/default.hbstest/fixtures/themes/130-template-inheritance/valid-default/index.hbs
✅ Files skipped from review due to trivial changes (16)
- test/fixtures/themes/130-template-inheritance/nested-default-target/default.hbs
- test/fixtures/themes/130-template-inheritance/shared-layout-target/c.hbs
- test/fixtures/themes/130-template-inheritance/nested-shell-target/layout.hbs
- test/fixtures/themes/130-template-inheritance/recursive-default/default.hbs
- test/fixtures/themes/130-template-inheritance/nested-shell-target/members/shell.hbs
- test/fixtures/themes/130-template-inheritance/shared-layout-target/a.hbs
- test/fixtures/themes/130-template-inheritance/nested-cycle/layouts/base.hbs
- test/fixtures/themes/130-template-inheritance/missing-and-empty/index.hbs
- test/fixtures/themes/130-template-inheritance/indirect-root-cycle/default.hbs
- test/fixtures/themes/130-template-inheritance/secondary-directive/default.hbs
- test/fixtures/themes/130-template-inheritance/missing-and-empty/empty.hbs
- test/fixtures/themes/130-template-inheritance/nested-cycle/layouts/default.hbs
- test/fixtures/themes/130-template-inheritance/absolute-default/default.hbs
- test/fixtures/themes/130-template-inheritance/indirect-root-cycle/base.hbs
- test/fixtures/themes/130-template-inheritance/nested-shell-target/members/layout.hbs
- test/fixtures/themes/130-template-inheritance/nested-default-target/layouts/base.hbs
🚧 Files skipped from review as they are similar to previous changes (3)
- test/fixtures/themes/130-template-inheritance/valid-default/default.hbs
- test/fixtures/themes/130-template-inheritance/valid-default/index.hbs
- lib/specs/v6.js
There was a problem hiding this comment.
Caution
Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@test/checker.test.js`:
- Around line 10-31: The test currently removes 'testHelper' from
v6Spec.knownHelpers only on the success path, risking cross-test leakage if the
expect fails; move the cleanup that finds and splices 'testHelper' from
v6Spec.knownHelpers into the existing finally block so it always runs: inside
the finally (where delete labsEnabledHelpers.testHelper is done) also retrieve
const v6Spec = spec.get(['v6']) and remove 'testHelper' if present (using
indexOf/splice) to guarantee the knownHelpers array is restored regardless of
test outcome.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: ab4139f6-bdd9-4050-9714-33ec2c3741f3
📒 Files selected for processing (27)
lib/checks/130-template-inheritance.jslib/specs/v6.jstest/130-template-inheritance.test.jstest/checker.test.jstest/fixtures/themes/130-template-inheritance/absolute-default/default.hbstest/fixtures/themes/130-template-inheritance/indirect-root-cycle/base.hbstest/fixtures/themes/130-template-inheritance/indirect-root-cycle/default.hbstest/fixtures/themes/130-template-inheritance/missing-and-empty/empty.hbstest/fixtures/themes/130-template-inheritance/missing-and-empty/index.hbstest/fixtures/themes/130-template-inheritance/nested-cycle/layouts/base.hbstest/fixtures/themes/130-template-inheritance/nested-cycle/layouts/default.hbstest/fixtures/themes/130-template-inheritance/nested-default-target/default.hbstest/fixtures/themes/130-template-inheritance/nested-default-target/layouts/base.hbstest/fixtures/themes/130-template-inheritance/nested-default-target/layouts/default.hbstest/fixtures/themes/130-template-inheritance/nested-shell-target/layout.hbstest/fixtures/themes/130-template-inheritance/nested-shell-target/members/layout.hbstest/fixtures/themes/130-template-inheritance/nested-shell-target/members/shell.hbstest/fixtures/themes/130-template-inheritance/recursive-default/default.hbstest/fixtures/themes/130-template-inheritance/relative-explicit-extension/index.hbstest/fixtures/themes/130-template-inheritance/relative-explicit-extension/layouts/default.hbstest/fixtures/themes/130-template-inheritance/secondary-directive/default.hbstest/fixtures/themes/130-template-inheritance/secondary-directive/index.hbstest/fixtures/themes/130-template-inheritance/shared-layout-target/a.hbstest/fixtures/themes/130-template-inheritance/shared-layout-target/c.hbstest/fixtures/themes/130-template-inheritance/shared-layout-target/shared.hbstest/fixtures/themes/130-template-inheritance/valid-default/default.hbstest/fixtures/themes/130-template-inheritance/valid-default/index.hbs
✅ Files skipped from review due to trivial changes (16)
- test/fixtures/themes/130-template-inheritance/nested-default-target/default.hbs
- test/fixtures/themes/130-template-inheritance/shared-layout-target/c.hbs
- test/fixtures/themes/130-template-inheritance/nested-shell-target/layout.hbs
- test/fixtures/themes/130-template-inheritance/recursive-default/default.hbs
- test/fixtures/themes/130-template-inheritance/nested-shell-target/members/shell.hbs
- test/fixtures/themes/130-template-inheritance/shared-layout-target/a.hbs
- test/fixtures/themes/130-template-inheritance/nested-cycle/layouts/base.hbs
- test/fixtures/themes/130-template-inheritance/missing-and-empty/index.hbs
- test/fixtures/themes/130-template-inheritance/indirect-root-cycle/default.hbs
- test/fixtures/themes/130-template-inheritance/secondary-directive/default.hbs
- test/fixtures/themes/130-template-inheritance/missing-and-empty/empty.hbs
- test/fixtures/themes/130-template-inheritance/nested-cycle/layouts/default.hbs
- test/fixtures/themes/130-template-inheritance/absolute-default/default.hbs
- test/fixtures/themes/130-template-inheritance/indirect-root-cycle/base.hbs
- test/fixtures/themes/130-template-inheritance/nested-shell-target/members/layout.hbs
- test/fixtures/themes/130-template-inheritance/nested-default-target/layouts/base.hbs
🚧 Files skipped from review as they are similar to previous changes (3)
- test/fixtures/themes/130-template-inheritance/valid-default/default.hbs
- test/fixtures/themes/130-template-inheritance/valid-default/index.hbs
- lib/specs/v6.js
🛑 Comments failed to post (1)
test/checker.test.js (1)
10-31:
⚠️ Potential issue | 🟡 Minor | ⚡ Quick winMove
knownHelperscleanup intofinallyto avoid cross-test state leakage.If the
expectat Line 21 fails,testHelpermay remain inv6Spec.knownHelpersand affect subsequent tests.Suggested fix
it('adds labs-enabled helpers to knownHelpers when flag is set', async function () { labsEnabledHelpers.testHelper = 'testFlag'; + const v6Spec = spec.get(['v6']); try { await check(themePath('is-empty'), { checkVersion: 'v6', labs: {testFlag: true}, skipChecks: true }); - const v6Spec = spec.get(['v6']); expect(v6Spec.knownHelpers).toContain('testHelper'); - - // Clean up the spec - const idx = v6Spec.knownHelpers.indexOf('testHelper'); - if (idx > -1) { - v6Spec.knownHelpers.splice(idx, 1); - } } finally { + const idx = v6Spec.knownHelpers.indexOf('testHelper'); + if (idx > -1) { + v6Spec.knownHelpers.splice(idx, 1); + } delete labsEnabledHelpers.testHelper; } });📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.it('adds labs-enabled helpers to knownHelpers when flag is set', async function () { labsEnabledHelpers.testHelper = 'testFlag'; const v6Spec = spec.get(['v6']); try { await check(themePath('is-empty'), { checkVersion: 'v6', labs: {testFlag: true}, skipChecks: true }); expect(v6Spec.knownHelpers).toContain('testHelper'); } finally { const idx = v6Spec.knownHelpers.indexOf('testHelper'); if (idx > -1) { v6Spec.knownHelpers.splice(idx, 1); } delete labsEnabledHelpers.testHelper; } });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@test/checker.test.js` around lines 10 - 31, The test currently removes 'testHelper' from v6Spec.knownHelpers only on the success path, risking cross-test leakage if the expect fails; move the cleanup that finds and splices 'testHelper' from v6Spec.knownHelpers into the existing finally block so it always runs: inside the finally (where delete labsEnabledHelpers.testHelper is done) also retrieve const v6Spec = spec.get(['v6']) and remove 'testHelper' if present (using indexOf/splice) to guarantee the knownHelpers array is restored regardless of test outcome.
ref https://linear.app/ghost/issue/BER-3133
Problem
Users have caused site outages by introducing infinite recursion in their custom theme templates (specifically, using
{{!< default}}indefault.hbs).Solution
Added a v6-only
GS130-NO-RECURSIVE-LAYOUTrule to catch recursive{{!< ...}}template inheritance.Error message: