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
6 changes: 6 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
node_modules
fixtures/*/.venv
fixtures/*/.code-quality-tmp
fixtures/*/.import_linter_cache
fixtures/*/.complexipy_cache
fixtures/*/complexipy-results.json
dist
launcher/dist
artifacts
fixtures/*/artifacts
fixtures/*/node_modules
fixtures/php-project/vendor
.git
10 changes: 5 additions & 5 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ coverage/
.code-quality-tmp/
fixtures/*/artifacts/
fixtures/*/.code-quality-tmp/
fixtures/ts-project/node_modules/
fixtures/ts-project/package-lock.json
fixtures/*/node_modules/
fixtures/*/package-lock.json
fixtures/php-project/vendor/
fixtures/python-project/.venv/
fixtures/python-project/.import_linter_cache/
fixtures/python-project/complexipy-results.json
fixtures/*/.venv/
fixtures/*/.import_linter_cache/
fixtures/*/complexipy-results.json
*.log
.pnpm-store/
.deptrac.cache
11 changes: 11 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,19 @@ RUN for t in phpcs phpstan deptrac; do composer install --working-dir=/opt/php/$
&& curl -fsSL -o /opt/php/bin/composer-require-checker https://github.com/maglnet/ComposerRequireChecker/releases/download/4.24.0/composer-require-checker.phar \
&& echo "35d1dfdd7aa94a750f4b4c9b0b22dd5e76b5e9e35fb910c8901f3dafd2ad2ab9 /opt/php/bin/composer-require-checker" | sha256sum -c - \
&& chmod +x /opt/php/bin/* && rm -rf /opt/composer-home/cache
ENV COREPACK_HOME=/opt/corepack COREPACK_DEFAULT_TO_LATEST=0 UV_PYTHON_INSTALL_DIR=/opt/python UV_NO_MODIFY_PATH=1
RUN corepack enable \
&& corepack prepare pnpm@10.17.1 yarn@1.22.22 --activate \
&& corepack prepare pnpm@11.5.2
# Python tools
COPY docker/python/requirements.txt /opt/requirements.txt
RUN python3 -m venv /opt/uv && /opt/uv/bin/pip install --no-cache-dir uv==0.12.14 \
&& ln -s /opt/uv/bin/uv /usr/local/bin/uv \
&& uv python install 3.14.7 \
&& ln -s "$(uv python find 3.14.7)" /usr/local/bin/python3.14 \
&& uv venv /opt/venv314 --python 3.14.7 \
&& uv pip install --python /opt/venv314/bin/python --no-cache -r /opt/requirements.txt \
&& rm -rf /root/.cache
RUN python3 -m venv /opt/venv && /opt/venv/bin/pip install --no-cache-dir -r /opt/requirements.txt
# CLI bundle + grammar assets
COPY --from=builder /build/dist /opt/code-quality/dist
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ checks:
reason: "Architecture rules are not defined yet"
```

See [Configuration](docs/configuration.md) for detection, defaults, validation, Drupal behavior, and every key.
See [Configuration](docs/configuration.md) for detection, defaults, validation, framework profiles (Drupal, Payload/Next), and every key.

## Documentation

Expand Down
22 changes: 20 additions & 2 deletions docs/checks.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Built-in test, generated-file, and framework exclusions apply before these check

## v1 tool pins

`versions` prints the 15 binary pins and two library pins; `doctor` verifies them.
`versions` prints the 15 check-tool binary pins, two library pins, and the image Runtime group; `doctor` verifies the pinned tools and runtime availability.

| Family | Binary or package | Pin |
| --- | --- | --- |
Expand All @@ -53,6 +53,24 @@ Built-in test, generated-file, and framework exclusions apply before these check

The two Composer library pins come from installed package metadata. Although they are not standalone binary probes, they remain part of the version contract.

## Runtime toolchain

| Verified Runtime pin | Version |
| --- | --- |
| uv | 0.12.14 |
| CPython (`python3.14`) | 3.14.7 |
| `/opt/venv314/bin/ruff` | 0.16.6 |
| `/opt/venv314/bin/complexipy` | 8.0.1 |
| `/opt/venv314/bin/vulture` | 2.16 |
| `/opt/venv314/bin/deptry` | 0.25.1 |
| `/opt/venv314/bin/lint-imports` | 2.15 |

| Presence-only Runtime | Provisioning |
| --- | --- |
| corepack | pnpm 10.17.1 by default; pnpm 11.5.2 and yarn 1.22.22 cached; other `packageManager` pins download during setup. |

Python targets at or above 3.14 run the Python tools from `/opt/venv314` with a `Notice:`; other targets use the default CPython 3.13 environment, and Ruff receives the matching `target-version`. See [Runtime targets](configuration.md#language-detection-and-source-roots) for how the target is read. Both environments install `docker/python/requirements.txt`, and the version gate probes each tool through the environment its check uses.

## Baseline semantics

Each concrete adapter owns `quality/<adapter-id>-baseline.json`. A schema-version-1 snapshot contains its exact `tool@version` stamp, a SHA-256 configuration hash, and a sorted map of positive finding counts.
Expand Down Expand Up @@ -93,7 +111,7 @@ Each unused-code tool has its own adapter, version stamp, and baseline:

Knip requires `node_modules/` only when `package.json` declares any dependency field (`dependencies`, `devDependencies`, `peerDependencies`, or `optionalDependencies`). Install with the repository's package manager before checking; the gate reports a missing install directly.

Knip disables configuration-executing plugins, so dependencies referenced only by tool configuration can appear unused and be baselined. Entry heuristics include conventional `index`, `main`, and `cli` files, files below `bin` directories in each source root, repository-level tests and test patterns, and scripts referenced from `package.json`, such as `node scripts/build.ts`.
Knip disables every plugin in the pinned Knip registry by generating a `false` toggle, so dependencies referenced only by tool configuration can appear unused and be baselined. Entry heuristics include conventional `index`, `main`, and `cli` files, files below `bin` directories in each source root, repository-level tests and test patterns, and scripts referenced from `package.json`, such as `node scripts/build.ts`. Next routes, middleware, instrumentation, and Payload, Vite, Vitest, and Playwright configs are added as static entries; monorepos get one Knip workspace per source-root owner.

PHP checks require a usable `vendor/` directory containing application dependencies and PHPStan context. Run `composer install` before checking; the gate reports a missing install directly.

Expand Down
9 changes: 9 additions & 0 deletions docs/ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,15 @@ jobs:

For npm, use `setup: npm ci`. For Composer, use `setup: composer install` or a suitable non-interactive variant such as `setup: composer install --no-interaction`.

The image supports these dependency setup recipes:

- `setup: pnpm install --frozen-lockfile` — Corepack honors the repository's `packageManager`; pnpm 10.17.1 and 11.5.2 are pre-cached, and other pins download on demand.
- `setup: yarn install --frozen-lockfile` — yarn 1.22.22 is pre-cached.
- `setup: uv sync --frozen` — uv uses the image's CPython 3.14.7 or 3.13; `.python-version` drives the choice.
- `setup: composer install --ignore-platform-req=php` — use this for repositories pinned to `php: ~8.3.0`, because the image runs PHP 8.4.

When possible, widen PHP dependency templates to `^8.3` so both PHP 8.3 and 8.4 satisfy the declared platform requirement.

A complete customized caller can select checks, install PHP dependencies, pin the image major, select a consumer subdirectory, and retain reports:

```yaml
Expand Down
25 changes: 23 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,11 @@ Without an explicit `languages` list, code-quality combines detected languages w
| Python | `pyproject.toml` or `setup.py` | Existing `src/` |
| Web | `.twig`, `.html`, `.css`, `.scss`, or `.less` below `templates/` or `assets/` | Existing `templates/` and `assets/` directories |

When a manifest detects a language but its default roots contain no source files, configuration loading discovers eligible source directories at depth 1 on every command. It ignores hidden, test, build, dependency, generated, and other conventional non-source directories; `init` records the discovered directories in the generated `paths.<language>`.
Conventional roots with sources are kept and workspace roots from manifests are added: TS/JS from `pnpm-workspace.yaml`, `package.json` `workspaces`, and depth-1 directories with their own `package.json`; PHP from `composer.json` `autoload` directories; Python from `[tool.uv.workspace]` members and `[tool.setuptools.packages.find]` `where`. A TS member maps to `<member>/src` only when no source exists elsewhere in the member. Member patterns support literal segments, `*`, `**`, one `*` inside a segment, and `!` negations; other patterns are ignored. Without conventional roots, depth-1 discovery is unioned with workspace roots. A `Notice:` lists added roots and `init` writes them. Drupal keeps its curated PHP roots.

The Python target comes from the repository-root `.python-version`, then the root `project.requires-python`; if neither defines it, the highest target among `[tool.uv.workspace]` members is used. Targets newer than the image still use CPython 3.14 tools with a notice. A Python target is part of the configuration hash only when Python resolves. PHPStan infers the PHP version from `composer.json` `config.platform.php`.

A directory without a manifest inside a workspace container, such as `packages/scripts`, is not analysed unless listed in `paths.<language>`.

If discovery finds no roots, configuration loading omits the language and prints a `Notice:` explaining how to add `paths.<language>`. An explicitly listed language or explicit path still fails when it contains no source files.

Expand All @@ -75,6 +79,7 @@ Dependency, virtual-environment, distribution, and artifact directories named `n
When `composer.json` requires `drupal/core`, `drupal/core-recommended`, or another `drupal/core-*` package, code-quality detects custom code automatically:

- PHP uses existing custom modules, themes, and profiles under `web/` or `docroot/`.
- PHP checks also scan `.module`, `.theme`, `.install`, `.inc`, `.profile`, and `.engine` files with structural anchors.
- Web uses custom theme directories.
- TS/JS adds custom theme directories that contain JavaScript.
- Ordinary source-root discovery remains the fallback when no custom directory exists.
Expand All @@ -91,11 +96,27 @@ exclude:
- "web/modules/custom/site/generated/**"
```

## Payload and Next profiles

Profiles are detected separately for the repository root and each workspace owner of a resolved TS root. `next.config.{js,mjs,cjs,ts,mts}` activates the Next profile, which excludes the owner's `.next/**` and `next-env.d.ts` only.

`payload.config.{ts,js,mjs,mts}` in the owner root or `src/`, or a `payload` dependency or dev dependency in that owner's `package.json`, activates the Payload profile. It excludes these paths under every TS root owned by that package:

- `<ts-root>/**/payload-types.ts`
- `<ts-root>/**/importMap.js`
- `<ts-root>/**/app/[(]payload[)]/**`
- `<ts-root>/**/migrations/**`
- `<ts-root>/**/migrations-*/**`
- `<ts-root>/**/seed/**`
A TS root named `app` also excludes `<ts-root>/[(]payload[)]/**`. One `Notice:` identifies each active profile kind and lists non-root owners. Dependency-cruiser does not apply exclude globs.

Payload exclusions scoped to a TS root also apply to other languages under the same directory.

## What `init` writes

Run `npx @runroom/code-quality init` once the first source and manifest files exist. It writes or updates:

- `.code-quality.yml` with detected languages and paths, unless the file already exists;
- `.code-quality.yml` with detected languages and resolved paths, including workspace roots, unless the file already exists;
- missing `quality/<adapter-id>-baseline.json` files while keeping existing snapshots and reporting each as `● Kept existing quality/<adapter-id>-baseline.json`;
- `.github/workflows/quality.yml`, unless it already exists;
- a Makefile or an appended target block when safe; and
Expand Down
8 changes: 6 additions & 2 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

The repository uses Node 24 and pnpm. Do not update generated policy output without reviewing the native tool behavior that produced it.

The runtime image has a default CPython 3.13 tool virtual environment and a second CPython 3.14 environment at `/opt/venv314`. Both are installed from `docker/python/requirements.txt`.

## Verification commands

```sh
Expand All @@ -17,7 +19,7 @@ pnpm capture <adapter-id> <fixture-directory>
| `pnpm verify` | Runs type checking, linting, tests, and the bundle build. |
| `pnpm build:launcher` | Builds the npm launcher package. |
| `pnpm docker:build` | Builds the local analysis image. |
| `pnpm integration` | Exercises the TS, PHP, Python, and web fixture repositories plus deliberate mutations against a locally built image. |
| `pnpm integration` | Exercises the TS, PHP, Python (`python-project` and `python314-project`), web, Drupal, Payload/Next, and monorepo fixture repositories plus deliberate mutations against a locally built image. |
| `pnpm capture …` | Captures a selected adapter's native output from a pinned image tool for parser tests. |

On its first run, integration installs fixture dependencies through the image. Native-output fixtures preserve the actual supported tool format and feed adapter parser tests.
Expand All @@ -26,14 +28,16 @@ On its first run, integration installs fixture dependencies through the image. N

A tool bump changes policy:

1. Change the pin in `src/registry.ts` and the image build definition.
1. Change the pin in `src/registry.ts` and the image build definition. A Python tool bump changes `docker/python/requirements.txt`, which feeds both virtual environments.
2. Build an image containing exactly that tool and capture fresh native-output fixtures.
3. Run unit tests, integration tests, and `doctor`.
4. Build and release the new image tags.
5. Ask consumers to review the change, remove affected snapshots, and run `check --initialize`.

Ordinary `--update` does not cross a tool-version gate. The project does not use Renovate for v1, and `doctor` is the final authority for versions in a built image.

Generated-configuration changes that alter tool settings or scan scope, including plugin toggles, extensions, and profiles, must bump `POLICY_VERSION` like a tool bump so consumers re-initialize affected baselines.

## Version changes

A version bump updates these three locations together:
Expand Down
6 changes: 5 additions & 1 deletion docs/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ code-quality analyzes untrusted repository content with tools that may execute c

- dependency-cruiser configuration and import-linter imports can execute consumer code inside the container.
- A reusable-workflow `setup` command, such as `composer install`, executes consumer code inside the job container.
- Knip runs with configuration-executing plugins disabled, but other supported tools can still load repository configuration where their operation requires it.
- Knip runs with configuration-executing plugins disabled; its plugin toggles are generated from the complete plugin registry of the pinned Knip version and verified by a test. Framework entry points are matched as static file patterns and parsed, never executed, but other supported tools can still load repository configuration where their operation requires it.
- Local launcher containers bind-mount the current repository at `/work` read-write.

## Container users
Expand All @@ -17,6 +17,7 @@ code-quality analyzes untrusted repository content with tools that may execute c

## Environment handling

- `/opt/corepack`, `/opt/python`, and `/opt/venv314` are root-owned and read-only for the `node` user. Under the reusable workflow (`--user root`), a setup command can write them, and anything cached there remains in scope for the rest of the job; the consumer `packageManager` field selects what corepack resolves.
- Tool subprocesses receive an allow-list plus constant and invocation-specific values, not the complete parent environment.
- The npm launcher forwards `CI`, `GITHUB_ACTIONS`, `NO_COLOR`, and `FORCE_COLOR` into the analysis container.
- When host stdout is a TTY and neither `NO_COLOR` nor `FORCE_COLOR` is set, the launcher injects `FORCE_COLOR=1`; it invokes `docker run` without `-t`, so container stdout is never a TTY.
Expand Down Expand Up @@ -48,4 +49,7 @@ code-quality analyzes untrusted repository content with tools that may execute c
- Global npm packages' transitive dependencies and Python packages are pinned only at the top level.
- Composer dependencies are fully locked.
- Downloaded phars are pinned and checksum-verified.
- uv is installed from PyPI by version pin without a hash.
- CPython 3.14.7 integrity relies on uv's bundled download metadata.
- `corepack prepare` specifications carry no integrity hash.
- These are accepted v1 limits; a tool-version change is a reviewed policy change, not an automatic baseline update.
4 changes: 4 additions & 0 deletions fixtures/drupal-project/.code-quality.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
checks:
disabled:
- id: unused
reason: "fixture has no installed Composer dependencies"
6 changes: 6 additions & 0 deletions fixtures/drupal-project/.github/workflows/quality.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
name: quality
on: [pull_request, push]
permissions: { contents: read, packages: read }
jobs:
quality:
uses: Runroom/code-quality/.github/workflows/quality.yml@v1
21 changes: 21 additions & 0 deletions fixtures/drupal-project/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# code-quality — run these through make; `$$PWD` is Make escaping for `$PWD`
CODE_QUALITY_IMAGE ?= ghcr.io/runroom/code-quality:v1
# Must be a single image reference; it is interpolated into docker run unquoted.
CODE_QUALITY = docker run --rm -v "$$PWD:/work" $(CODE_QUALITY_IMAGE)

.PHONY: quality quality-all quality-baseline quality-report quality-doctor

quality:
$(CODE_QUALITY) check

quality-all:
$(CODE_QUALITY) check --all

quality-baseline:
$(CODE_QUALITY) baseline

quality-report:
$(CODE_QUALITY) report

quality-doctor:
$(CODE_QUALITY) doctor
1 change: 1 addition & 0 deletions fixtures/drupal-project/composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"name":"fixture/drupal","require":{"drupal/core-recommended":"^11"}}
6 changes: 6 additions & 0 deletions fixtures/drupal-project/quality/php-cognitive-baseline.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"version": 1,
"tool": "phpcs@4.0.4",
"configHash": "788bbffbbd6d43bd4fcab5854d279bebdf4215471f87f6d6b60cd83bacc49260",
"findings": {}
}
6 changes: 6 additions & 0 deletions fixtures/drupal-project/quality/php-complexity-baseline.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"version": 1,
"tool": "phpcs@4.0.4",
"configHash": "788bbffbbd6d43bd4fcab5854d279bebdf4215471f87f6d6b60cd83bacc49260",
"findings": {}
}
6 changes: 6 additions & 0 deletions fixtures/drupal-project/quality/php-duplication-baseline.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"version": 1,
"tool": "jscpd@5.2.0",
"configHash": "788bbffbbd6d43bd4fcab5854d279bebdf4215471f87f6d6b60cd83bacc49260",
"findings": {}
}
6 changes: 6 additions & 0 deletions fixtures/drupal-project/quality/web-duplication-baseline.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"version": 1,
"tool": "jscpd@5.2.0",
"configHash": "788bbffbbd6d43bd4fcab5854d279bebdf4215471f87f6d6b60cd83bacc49260",
"findings": {}
}
7 changes: 7 additions & 0 deletions fixtures/drupal-project/web/modules/custom/demo/demo.install
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

declare(strict_types=1);

function demo_install(): void
{
}
18 changes: 18 additions & 0 deletions fixtures/drupal-project/web/modules/custom/demo/demo.module
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

function demo_help(string $route_name): string
{
return $route_name === "help" ? "Demo help" : "";
}

function demo_simple(int $value): int
{
return $value * 2;
}

function demo_status(): string
{
return "ready";
}
13 changes: 13 additions & 0 deletions fixtures/drupal-project/web/modules/custom/demo/src/Demo.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace Drupal\demo;

final class Demo
{
public function run(int $value): int
{
return $value * 2;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<main class="page">
<h1>{{ page.title }}</h1>
<div class="page__content">
{{ page.content }}
</div>
</main>
1 change: 1 addition & 0 deletions fixtures/monorepo-project/.code-quality.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
languages: [ts]
6 changes: 6 additions & 0 deletions fixtures/monorepo-project/.github/workflows/quality.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
name: quality
on: [pull_request, push]
permissions: { contents: read, packages: read }
jobs:
quality:
uses: Runroom/code-quality/.github/workflows/quality.yml@v1
21 changes: 21 additions & 0 deletions fixtures/monorepo-project/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# code-quality — run these through make; `$$PWD` is Make escaping for `$PWD`
CODE_QUALITY_IMAGE ?= ghcr.io/runroom/code-quality:v1
# Must be a single image reference; it is interpolated into docker run unquoted.
CODE_QUALITY = docker run --rm -v "$$PWD:/work" $(CODE_QUALITY_IMAGE)

.PHONY: quality quality-all quality-baseline quality-report quality-doctor

quality:
$(CODE_QUALITY) check

quality-all:
$(CODE_QUALITY) check --all

quality-baseline:
$(CODE_QUALITY) baseline

quality-report:
$(CODE_QUALITY) report

quality-doctor:
$(CODE_QUALITY) doctor
1 change: 1 addition & 0 deletions fixtures/monorepo-project/apps/web/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"name":"@fixture/web","private":true,"type":"module"}
3 changes: 3 additions & 0 deletions fixtures/monorepo-project/apps/web/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function webValue(): string {
return "web";
}
1 change: 1 addition & 0 deletions fixtures/monorepo-project/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"name":"monorepo-fixture","private":true,"type":"module","packageManager":"pnpm@10.17.1"}
1 change: 1 addition & 0 deletions fixtures/monorepo-project/packages/core/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"name":"@fixture/core","private":true,"type":"module"}
3 changes: 3 additions & 0 deletions fixtures/monorepo-project/packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function coreValue(): string {
return "core";
}
Loading
Loading