Skip to content
Closed
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
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ dist
launcher/dist
artifacts
fixtures/*/artifacts
fixtures/*/node_modules
fixtures/php-project/vendor
.git
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ 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/
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
2 changes: 1 addition & 1 deletion docs/checks.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,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 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`. Framework entry points detected from static config files (Next app/pages routes, middleware, instrumentation, payload.config, vite/vitest/playwright configs) are added as static entry patterns, and in monorepos the generated configuration declares one Knip workspace per resolved source root owner with workspaces that own no root ignored; plugins stay disabled.

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
20 changes: 18 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ 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>`.
When a manifest detects a language, conventional roots with sources are kept, then workspace roots from manifests are added. TypeScript workspace roots come from `pnpm-workspace.yaml` packages, `package.json` workspaces, and depth-1 directories with their own `package.json`; PHP roots come from `composer.json` autoload `psr-4`, `psr-0`, and `classmap` directories; Python roots come from `[tool.uv.workspace]` members minus `exclude` and `[tool.setuptools.packages.find]` `where`. Declared TypeScript members must contain `package.json`, and declared Python members must contain `pyproject.toml`. Member patterns support literal path segments, `*`, `**`, at most one `*` inside a segment, and `!` negations. The member walk reaches depth three; each manifest pattern is capped at 200 characters and only the first 200 include and first 200 exclude patterns are considered. Leading `./` prefixes, repeated slashes, and `.` path segments are normalized. A member maps to `<member>/src` when it exists. Only directories with source files count. When no conventional root has sources, workspace roots and depth-1 discovered roots are unioned, and a discovered directory containing a workspace root is dropped. A `Notice:` lists added roots, and `init` writes them into the generated `paths.<language>`. A depth-1 directory with its own `package.json`, such as `website/`, becomes a TypeScript root. Depth-1 discovery remains the fallback when nothing else resolves; it ignores hidden, test, build, dependency, generated, and other conventional non-source directories. Drupal projects keep the curated custom module, theme, and profile PHP roots instead of adding Composer autoload roots.

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 +75,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 +92,26 @@ exclude:
- "web/modules/custom/site/generated/**"
```

## Payload/Next profile

When `next.config.{js,mjs,cjs,ts,mts}` exists at the repository root, or `payload.config.{ts,js,mjs,mts}` exists at the root or under `src/`, code-quality activates the Payload/Next profile for resolved TypeScript sources. For every resolved TypeScript root `<ts-root>`, it excludes:

- `<ts-root>/**/payload-types.ts`
- `<ts-root>/**/importMap.js`
- `<ts-root>/**/app/[(]payload[)]/**`
- `<ts-root>/**/migrations/**`
- `<ts-root>/**/migrations-*/**`
- `<ts-root>/**/seed/**`
- `.next/**`
- `next-env.d.ts`

A `Notice:` identifies the active Payload/Next defaults. `.next/**` and `next-env.d.ts` remain repository-root exclusions; the generated-code patterns are scoped to TypeScript roots. Dependency-cruiser does not apply exclude globs.

## 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
2 changes: 1 addition & 1 deletion docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,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, 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 Down
2 changes: 1 addition & 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 Down
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": "e0fa6304b2c3e12e85b64f67a40efc89b20573dc649440e738beef4f68e3a3cb",
"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": "e0fa6304b2c3e12e85b64f67a40efc89b20573dc649440e738beef4f68e3a3cb",
"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": "e0fa6304b2c3e12e85b64f67a40efc89b20573dc649440e738beef4f68e3a3cb",
"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": "e0fa6304b2c3e12e85b64f67a40efc89b20573dc649440e738beef4f68e3a3cb",
"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"}
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";
}
1 change: 1 addition & 0 deletions fixtures/monorepo-project/pnpm-workspace.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages: ["packages/*", "apps/*"]
6 changes: 6 additions & 0 deletions fixtures/monorepo-project/quality/ts-cognitive-baseline.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"version": 1,
"tool": "fallow@3.23.0",
"configHash": "a1fc39c1d0e3108d4200b0e7390f3c947bb47f4335d262111a170cee51e2b1dc",
"findings": {}
}
6 changes: 6 additions & 0 deletions fixtures/monorepo-project/quality/ts-complexity-baseline.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"version": 1,
"tool": "oxlint@1.82.0",
"configHash": "a1fc39c1d0e3108d4200b0e7390f3c947bb47f4335d262111a170cee51e2b1dc",
"findings": {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"version": 1,
"tool": "jscpd@5.2.0",
"configHash": "a1fc39c1d0e3108d4200b0e7390f3c947bb47f4335d262111a170cee51e2b1dc",
"findings": {}
}
6 changes: 6 additions & 0 deletions fixtures/monorepo-project/quality/ts-unused-baseline.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"version": 1,
"tool": "knip@6.35.1",
"configHash": "a1fc39c1d0e3108d4200b0e7390f3c947bb47f4335d262111a170cee51e2b1dc",
"findings": {}
}
11 changes: 11 additions & 0 deletions fixtures/monorepo-project/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"target": "ES2023",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"allowImportingTsExtensions": true,
"noEmit": true
},
"include": ["src"]
}
1 change: 1 addition & 0 deletions fixtures/payload-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/payload-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/payload-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
3 changes: 3 additions & 0 deletions fixtures/payload-project/next.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/** @type {import("next").NextConfig} */
const config = {};
export default config;
1 change: 1 addition & 0 deletions fixtures/payload-project/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"name":"payload-fixture","private":true,"type":"module","dependencies":{"next":"file:stubs/next","payload":"file:stubs/payload"},"devDependencies":{"vitest":"file:stubs/vitest","@playwright/test":"file:stubs/playwright-test"}}
3 changes: 3 additions & 0 deletions fixtures/payload-project/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { defineConfig } from "@playwright/test";

export default defineConfig({});
6 changes: 6 additions & 0 deletions fixtures/payload-project/quality/ts-cognitive-baseline.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"version": 1,
"tool": "fallow@3.23.0",
"configHash": "38b0f0af9a11119a26c1815b8d15c587b61191194c80a6fdb4575ac3397b1d93",
"findings": {}
}
6 changes: 6 additions & 0 deletions fixtures/payload-project/quality/ts-complexity-baseline.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"version": 1,
"tool": "oxlint@1.82.0",
"configHash": "38b0f0af9a11119a26c1815b8d15c587b61191194c80a6fdb4575ac3397b1d93",
"findings": {}
}
6 changes: 6 additions & 0 deletions fixtures/payload-project/quality/ts-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": "38b0f0af9a11119a26c1815b8d15c587b61191194c80a6fdb4575ac3397b1d93",
"findings": {}
}
6 changes: 6 additions & 0 deletions fixtures/payload-project/quality/ts-unused-baseline.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"version": 1,
"tool": "knip@6.35.1",
"configHash": "38b0f0af9a11119a26c1815b8d15c587b61191194c80a6fdb4575ac3397b1d93",
"findings": {}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions fixtures/payload-project/src/app/(payload)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Generated by Payload. Do not edit.
export default function PayloadLayout(): string {
return "payload";
}
3 changes: 3 additions & 0 deletions fixtures/payload-project/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page(): string {
return "page";
}
3 changes: 3 additions & 0 deletions fixtures/payload-project/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page(): string {
return "page";
}
1 change: 1 addition & 0 deletions fixtures/payload-project/src/collections/Users.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const Users = { slug: "users", fields: [] };
1 change: 1 addition & 0 deletions fixtures/payload-project/src/migrations/20260101_000000.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export async function up(): Promise<void> {}
3 changes: 3 additions & 0 deletions fixtures/payload-project/src/payload-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface User {
id: string;
}
4 changes: 4 additions & 0 deletions fixtures/payload-project/src/payload.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { buildConfig } from "payload";
import { Users } from "./collections/Users.ts";

export default buildConfig({ collections: [Users] });
1 change: 1 addition & 0 deletions fixtures/payload-project/src/seed/seed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export async function seed(): Promise<void> {}
1 change: 1 addition & 0 deletions fixtures/payload-project/stubs/next/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type NextConfig = Record<string, unknown>;
Loading
Loading