Skip to content

feat: codeceptjs lint command with Claude Code hook - #5737

Closed
DavertMik wants to merge 2 commits into
4.xfrom
feat/lint-command
Closed

DavertMik wants to merge 2 commits into
4.xfrom
feat/lint-command

Conversation

@DavertMik

@DavertMik DavertMik commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds npx codeceptjs lint: an AST check (acorn) of tests, page objects and helpers for CodeceptJS anti-patterns. It also runs as a Claude Code PreToolUse hook that blocks an edit which adds a new error.

Base branch is 4.x.

npx codeceptjs lint                    # tests, include and helpers from config
npx codeceptjs lint tests/ pages/      # explicit paths
npx codeceptjs lint --json
npx codeceptjs lint --hook claude      # reads the hook payload from stdin

Code

  • lib/lint.js: a Linter class (files(), includes(), lint(), lintFile()) and one small class per rule extending Rule.
  • lib/command/lint.js: CLI output and hook mode.

Rules

Rule Default Detects
no-fixed-wait error I.wait(5) with a number
no-sleep error setTimeout outside helper classes
no-only error on CI, warn locally Scenario.only, Feature.only, Data(...).only
no-pause error on CI, warn locally pause()
secret-credentials error I.fillField on a password/token/secret/API key field, or such a process.env.*, without secret()
await-grab error I.grab*() assigned or used without await (returning it is allowed)
no-actor-in-helper error I inside a class extending Helper
raw-browser-in-test warn I.use*To / I.executeScript in a Scenario

Levels can be changed in lint.rules in the config. A single case can be suppressed with // codeceptjs-lint-disable-line <rule> or -next-line.

Hook

  • Checks only files that the config lints (tests glob, include, helpers). Other files are allowed.
  • Builds the file after the edit, lints before and after, and blocks (exit 2) only on errors that are new. Errors are compared by rule and source, counted, so a second identical I.wait(5) is new.
  • Warnings never block. Any failure (bad payload, no config, parse error) allows the edit.

TypeScript is read with Node's stripTypeScriptTypes. Files with enum/namespace are reported as parse errors.

Test plan

  • npx mocha test/unit/command: 44 passing. The lint tests check each rule on inline code, plus suppression comments, config levels, TypeScript lines, CommonJS, file collection, the CLI exit code, and hook block/allow cases.
  • Ran on test/acceptance and examples: no parse failures. Findings are plain-text passwords in examples and one setTimeout in a config file.
  • eslint and prettier clean.

🤖 Generated with Claude Code

AST-based checker for CodeceptJS anti-patterns in tests, page objects
and helpers: no-fixed-wait, no-sleep, no-only, no-pause,
secret-credentials, await-grab, no-actor-in-helper, raw-browser-in-test.

`--hook claude` reads a PreToolUse payload, lints the file before and
after the edit and blocks only on new error findings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread lib/lint.js Outdated
const ignore = [].concat(config.lint?.ignore || [])
if (!ignore.length) return false
const target = path.resolve(file)
return withoutNodeWarnings(() => ignore.some(pattern => path.matchesGlob(target, path.resolve(root, pattern))))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

return withoutNodeWarnings(() => ignore.some(pattern => path.matchesGlob(target, path.resolve(root, pattern))))
path.matchesGlob doesn't exist on Node <20.17/22.5 while engines is >=16: CLI throws TypeError, hook mode silently disables itself. Fall back to minimatch

Comment thread lib/lint.js Outdated
id: 'no-actor-in-helper',
level: 'error',
check(node, ancestors, ctx) {
const isActorRef =

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

(node.type === 'Identifier' && node.name === 'I') / if (!isActorRef || !insideHelperClass(ancestors)) return
no-actor-in-helper flags const { I } = this.helpers which its own message recommends. Skip destructuring of this.helpers and non-reference identifiers

Comment thread lib/lint.js Outdated
if (seen.has(key)) return
seen.add(key)
if (suppressed.get(line)?.has(rule.id)) return
findings.push({ file, line, column: column + 1, rule: rule.id, level, message, source: ctx.source(target) })

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

findings.push({ file, line, column: column + 1, rule: rule.id, level, message, source: ctx.source(target) }) --json leaks the plaintext secret in source for secret-credentials findings (e.g. I.fillField('Password', 'hunter2')). Mask the value argument.

One Linter class with a class per rule. Drop the typescript fallback,
source maps, ignore config, warning suppression and per-rule fixtures.
The hook only checks files the config lints. return I.grab*() is no
longer flagged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@DavertMik DavertMik closed this Oct 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants