CLI Options¶
Qualimetrix provides the check command for code analysis and several utility commands for baseline management, git hooks, and dependency graph visualization.
check command¶
Paths argument¶
Specify one or more directories or files to analyze:
# Analyze specific directories
bin/qmx check src/ lib/
# Analyze a single file
bin/qmx check src/Service/UserService.php
If you omit paths, Qualimetrix auto-detects them from the autoload section of your composer.json.
File options¶
--config, -c¶
Path to a YAML configuration file:
--exclude¶
Exclude directories from analysis. Can be repeated:
A value that removes no directory is reported as
discovery.unmatched-exclude — a warning at project
level, not a refusal, and only on a run whose paths cover the project's
production autoload roots. On a narrower run the pattern may bind nothing
simply because the code it names lies outside the slice.
--include-generated¶
By default, Qualimetrix automatically skips files that contain a @generated annotation in the first 2 KB. This flag overrides that behavior and includes generated files in the analysis:
Can also be set in qmx.yaml:
--suppress-path¶
Suppress violations for files matching a glob pattern. The files are still analyzed (their metrics contribute to namespace-level calculations), but violations are not reported. Can be repeated:
Merged with suppress_paths from qmx.yaml — both sources are combined.
Does not apply to architecture.* rules
architecture.layer-violation and architecture.circular-dependency violations are never
suppressed by this option — see Suppress Paths
for why and for the alternatives.
--suppress-namespace¶
Suppress violations for classes in namespaces matching a prefix or glob pattern. The classes are still analyzed (their metrics contribute to aggregated calculations), but violations are not reported. Can be repeated:
Merged with suppress_namespaces from qmx.yaml — both sources are combined.
Does not apply to architecture.* rules
architecture.layer-violation and architecture.circular-dependency violations are never
suppressed by this option — see Suppress Namespaces
for why and for the alternatives.
Preset options¶
--preset¶
Apply a named preset or a custom YAML file. Can be repeated or comma-separated:
# Built-in presets
bin/qmx check src/ --preset=strict
bin/qmx check src/ --preset=legacy
# Combine presets (merged left-to-right)
bin/qmx check src/ --preset=strict,ci
bin/qmx check src/ --preset=strict --preset=ci
# Custom preset file
bin/qmx check src/ --preset=./my-preset.yaml
Available built-in presets: strict, legacy, ci.
Presets are applied after composer.json auto-detection but before qmx.yaml, so your config file always takes precedence. See Configuration > Presets for details.
Output options¶
--format, -f¶
Choose the output format. Default: summary.
Available formats: summary, text, text-verbose, json, metrics, checkstyle, sarif, gitlab, github, health, html, suppressed.
See Output Formats for details on each format.
--group-by¶
Group violations in the output. Default depends on the formatter.
Available values: none, file, rule, severity, class, namespace.
--format-opt¶
Pass formatter-specific options as key=value pairs. Can be repeated:
A key no formatter reads is refused with exit 3. A key some other formatter reads stays accepted, so a script that sweeps one option set across formats still works.
JSON format options:
| Option | Default | Description |
|---|---|---|
violations=N\|all |
all | Max violations in output (0=none) |
limit=N |
all | Alias for violations |
top=N |
10 | Number of worst offenders to include |
rank-by=count\|density |
count | Reorder worst-offender lists by violation count (default) or by violation density |
bin/qmx check src/ --format=json --format-opt=limit=100
bin/qmx check src/ --format=json --format-opt=violations=all
bin/qmx check src/ --format=json --format-opt=rank-by=density
Summary format options:
| Option | Default | Description |
|---|---|---|
top=N |
3 | Number of worst offenders to include |
rank-by=count\|density |
count | Reorder worst-offender lists by violation count (default) or by violation density |
Two unrelated options named top
--format-opt=top=N (JSON and summary formats) caps the worst-namespace/worst-class
offender lists. The global --top flag is a different knob: it caps the
separate "Top issues by impact" list. The two can be set independently.
Health format options:
| Option | Default | Description |
|---|---|---|
contributors=N |
3 | Number of worst contributors listed per health dimension |
HTML format options:
| Option | Default | Description |
|---|---|---|
project-name=NAME |
auto-detected | Overrides the project name shown in the HTML report |
--fail-on¶
Set the minimum severity that causes a non-zero exit code. Default: error.
# Default behavior: only errors cause non-zero exit code
bin/qmx check src/
# Also fail on warnings
bin/qmx check src/ --fail-on=warning
# Never fail on violations
bin/qmx check src/ --fail-on=none
By default, warnings are shown in the output but do not cause CI failure. Use --fail-on=warning to also fail on warnings.
Can also be set in qmx.yaml:
--exclude-health¶
Exclude specific health dimensions from scoring. The excluded dimensions are not shown in the health summary and do not contribute to the overall score. Can be repeated:
# Exclude typing from health scoring
bin/qmx check src/ --exclude-health=typing
# Exclude multiple dimensions
bin/qmx check src/ --exclude-health=typing --exclude-health=maintainability
Available dimensions: complexity, cohesion, coupling, typing, maintainability.
Can also be set in qmx.yaml:
--detail¶
Show a grouped violation list after the summary. Only affects summary format.
# Default limit (200 violations)
bin/qmx check src/ --detail
# Show all violations (no limit)
bin/qmx check src/ --detail=all
# Custom limit
bin/qmx check src/ --detail=50
Auto-enabled when --namespace or --class is used.
--top¶
Number of top-impact issues to show. Default: 10; 0 disables the section.
# Default: top 10
bin/qmx check src/
# Show the top 25
bin/qmx check src/ --top=25
# Disable the section
bin/qmx check src/ --top=0
Controls the "Top issues by impact" section of the summary format and the
topIssues key of --format=json — a list of findings ranked by impact
(combining ClassRank, severity and remediation time), separate from the
worst-namespace and worst-class offender lists. No other format renders it.
This is a different knob from --format-opt=top=N, which caps the worst-offender
lists instead — see the format options tables above.
--all¶
Show all violations without truncation. This is a shorthand for --format-opt=violations=all --detail=all.
# Show all violations in JSON format
bin/qmx check src/ --format=json --all
# Show all violations in summary format
bin/qmx check src/ --all
Cannot be combined with --format-opt=violations=N (numeric limit) — this produces a clear error. Combining --all with --format-opt=violations=all is allowed (they are synonyms).
--namespace¶
Filter output to a specific namespace subtree. The value is a namespace pattern, not a literal prefix:
- Without glob characters it matches on namespace boundaries —
App\ServicematchesApp\Serviceand everything under it, but notApp\ServiceBus. - With
*,?or[it is matched as a glob, soApp\*\OrderselectsApp\Billing\OrderandApp\Sales\Orderrather than a namespace literally spelled with an asterisk. - A trailing
\is cosmetic:App\Service\andApp\Serviceare the same pattern. - An empty value matches nothing at all, the global namespace included.
Filters violations and worst offenders to the selected namespaces. Shows subtree health scores. Auto-enables --detail.
Project-wide findings (architecture.coverage-gap and the other diagnostics that judge the run as a whole) are never selected by a namespace pattern, not even *: they belong to no namespace.
The same matching rule governs the health drill-down and the worst-offender lists this option turns on, and the include_namespaces option of coupling.distance.
A pattern that selects no analysed namespace is refused with exit 3, and the refusal says how many namespaces the run did have. A pattern that does select something and still reports nothing prints the ordinary empty result — that is the half of the pair worth telling apart.
Mutually exclusive with --class.
--class¶
Filter output to a specific class by exact FQCN match.
Filters violations to the specified class. Auto-enables --detail.
An FQCN matching no analysed class is refused with exit 3, for the same reason
--namespace is: an empty report otherwise reads as a clean class.
Mutually exclusive with --namespace.
Cache options¶
Qualimetrix caches parsed ASTs to speed up repeated runs.
--no-cache¶
Disable caching entirely:
--cache-dir¶
Set a custom cache directory. Default: .qmx-cache.
The directory is created when missing. A path that cannot be created or is not
writable is refused with exit 3 rather than silently disabling the cache, and
the door closes the same way on every command that resolves a cache directory,
not only on check.
--clear-cache¶
Clear the cache before running analysis:
Baseline options¶
See Baseline for the lifecycle and file format.
--baseline=BASELINE¶
Use a baseline file to apply accepted ceilings to live findings:
--show-resolved¶
Count entries whose complete identity no longer appears in the measured set:
Stale and inert entries are reported without failing the run or disabling other baseline entries. A group that still fires with fewer members is not resolved.
Baseline lifecycle commands¶
The commands below are the complete baseline write and inspection surface:
bin/qmx baseline:generate <baseline> [<paths>...] [--mode=MODE] [--force]
bin/qmx baseline:update <baseline> [<paths>...] [--force]
bin/qmx baseline:cleanup <baseline> [<paths>...] [--remove=REMOVE]... [--force]
bin/qmx baseline:explain <symbol> [<paths>...] [--baseline=BASELINE] [--channel=CHANNEL]
bin/qmx baseline:rename-channels <baseline> <map> [--format=FORMAT]
The first four commands accept --config=CONFIG, --preset=PRESET, --disable-rule=DISABLE-RULE, --only-rule=ONLY-RULE, and --rule-opt=RULE-OPT. They do not accept any exclusion or suppression option. baseline:rename-channels accepts none of them: it runs no analysis, so there is no measured set for them to define.
baseline:generatecaptures the current measured findings.--mode=ratchetis the default;--mode=suppressrecords unconditional acceptance for captured identities. Its--forceoverwrites an existing file.baseline:updatetightens existing entries only. Its--forceoverrides the recorded-scope coverage guard.baseline:cleanuplists candidates by default and removes only repeated--remove=REMOVEselectors. Its--forcealso overrides the scope guard.baseline:explainshows the configured threshold, accepted baseline level, and source override for a canonical symbol;--channel=CHANNELnarrows the answer.baseline:rename-channelsrewrites thechannelfield of the entries a declared tab-separated map names, and nothing else, without analysing anything. Exit1covers a refusal on content or an unreadable baseline or map file;2is a malformed--formatvalue. Either way the baseline is left byte-identical, and the refusal is reported in the chosen format — under--format=jsonas an object with anerrorkey. See Carry a baseline onto renamed channels — note that carrying an entry changes its selector.
The four analysing commands refuse incomplete analysis with exit 4 before interpreting
or writing a baseline. --force overrides file/scope guards only; it cannot make
a partial measured set acceptable. Existing destinations remain byte-identical,
and baseline:generate does not create a missing destination.
Loadable baseline versions and the migration procedure for an older file are documented under Replace an older baseline.
The removed --generate-baseline and --baseline-ignore-stale options have no aliases. Use baseline:generate and explicit baseline:cleanup --remove instead.
Suppression options¶
--show-suppressed¶
Show violations that were suppressed by @qmx-ignore tags, and violations suppressed by a
per-rule suppress_namespaces / suppress_namespace_channels / suppress_paths entry in qmx.yaml (see
Rules):
Independently of --show-suppressed, running with -v prints a per-rule count of how many
violations were suppressed this way. The namespace bucket includes both namespace options and
is separate from suppress_paths; each is broken down by rule name. Unlike @qmx-ignore, this suppression is otherwise silent: nothing in
the default output indicates it happened.
--show-suppressed renders part of this as prose on the text surface.
--format=suppressed reports the full composition — all seven suppression
mechanisms, not only these two — as machine-readable JSON; see
Output Formats. Either --show-suppressed or
selecting --format=suppressed (including format: suppressed in
qmx.yaml) is enough to arm the per-rule exclusion capture; you do not need
both. The two surfaces are not otherwise equivalent — see
suppressed for what each one shows.
Suppression is a closed set of seven mechanisms. Several neighboring decisions
also make a finding invisible but are not suppression, and neither surface
covers them: a rule that never ran (--disable-rule, --only-rule,
enabled: false) produced nothing to suppress; a disabled channel for a
classless producer (visible in qmx rules) is removed the same way, before
the ledger runs; a threshold that keeps a finding from being produced at all
(@qmx-threshold) is audited separately rather than through this surface;
formatter truncation (--detail, violations=N) keeps the finding in the
payload and only flags it truncated; and --namespace/--class drill-down
narrows presentation per invocation without removing anything from the
underlying result.
--no-suppression-annotations¶
Report every violation, including the ones @qmx-ignore tags suppress:
It does not change what a baseline measures
The flag affects the report only. A baseline measures the findings your
configuration and your source annotations leave standing, so a finding an
@qmx-ignore tag removes is never captured into a baseline and never
compared against one — whether or not this flag is passed.
The visible consequence: under this flag an annotated finding is shown at
its own severity and is never promoted to an error, because no baseline
entry covers it. A flag can narrow what a baseline measures
(--suppress-path, --suppress-namespace); none can widen it.
Git scope options¶
Report only violations from changed files. See Git Integration for the full guide.
--report¶
Control which violations to report. Analyzes the full project but only shows violations from changed files:
--report-strict¶
In diff mode, only show violations from the changed files themselves. Without this flag, violations from parent namespaces are also shown:
Execution options¶
--workers, -w¶
Control parallel processing. Default: auto-detect based on CPU count.
# Disable parallel processing (single-threaded)
bin/qmx check src/ --workers=1
# Disable parallel processing (sequential)
bin/qmx check src/ --workers=0
# Use exactly 4 workers
bin/qmx check src/ --workers=4
Tip
Use --workers=1 for debugging or single-process environments. --workers=0 disables parallelism (sequential execution); auto-detect is the default when the option is omitted.
--memory-limit¶
Set the PHP memory limit for analysis. By default, PHP's memory_limit from php.ini is used.
# Set memory limit to 1GB for large projects
bin/qmx check src/ --memory-limit=1G
# Unlimited memory
bin/qmx check src/ --memory-limit=-1
Valid formats: -1 (unlimited), or a positive integer with optional K/M/G suffix (e.g., 512M, 2G).
Equivalent YAML: memory_limit: 1G
--log-file¶
Write a debug log to a file:
--log-level¶
Set the minimum log level. Default: info.
Available levels: debug, info, warning, error. A value outside them is
refused with exit 3 instead of falling back to info.
--no-progress¶
Disable the progress bar. Useful in CI pipelines:
Accepted by every command that shows a progress bar: check, directives,
debug:layer-assignment, baseline:generate, baseline:update,
baseline:cleanup and baseline:explain. graph:export analyses a tree too
but draws no bar, so it does not take the option.
The progress bar is written to standard error, so the report on standard output
stays machine-readable even on a terminal — bin/qmx check src/ --format=json >
report.json produces valid JSON without this flag. It is drawn only when
standard error is a terminal; redirecting standard error silences the bar
without touching the report, rather than writing control bytes into the file.
The bar shares the error stream with detailed logging (-v, -vv, -vvv) and
with warnings emitted during a run. Both are drawn through one owner: a
diagnostic line pushes the bar down and stays on the screen, and the bar is
redrawn beneath it. Raising verbosity therefore does not turn the bar off, and
the bar does not eat log lines.
--silent, -q/--quiet¶
Symfony console flags that suppress normal output. Both are accepted by every command:
--silent no longer guarantees zero bytes on every exit code
--silent and -q/--quiet currently behave identically: both suppress the
report on standard output, but a configuration or input refusal (e.g. a path
that does not exist) is still written to standard error. A run refused
before analysis starts produces 0 bytes on stdout and a human-readable error
line on stderr, whichever of the two flags was passed:
A CI wrapper built on the older assumption that --silent means "zero output
on any exit code" will see that stderr text on a refusal. Redirect standard
error too (--silent 2>/dev/null) if that assumption must hold.
Profiling options¶
--profile¶
Enable the internal profiler. Optionally specify a file to save the profile:
<!-- llms:skip-end -->
# Show profiling summary on screen
bin/qmx check src/ --profile
# Save profile to file
bin/qmx check src/ --profile=profile.json
--profile-format¶
Choose the profile export format. Default: json.
Available formats: json, chrome-tracing.
Tip
Use chrome-tracing format and open the file in Chrome DevTools (chrome://tracing) for a visual timeline.
Rule options¶
--disable-rule¶
Disable a producer rule, an entire group, or a finding channel. A selector is either an
exact name (a producer rule, a group like complexity, or a channel), or X.* for
strictly the descendants of X — X itself is not included. A bare prefix without the
star is an error. A channel selector can be narrowed to one level of the aggregation tree with
:level, same as --only-rule. Disabling one channel keeps its producer active so that other
channels can still be reported. Can be repeated:
# Disable one rule
bin/qmx check src/ --disable-rule=size.class-count
# Disable all complexity rules
bin/qmx check src/ --disable-rule=complexity.*
# Disable multiple
bin/qmx check src/ --disable-rule=complexity.* --disable-rule=cohesion.lcom
# Disable only one computed finding channel
bin/qmx check src/ --disable-rule=health.complexity
Memory optimization
Disabling the duplication.clone rule also skips the memory-intensive duplication detection phase entirely. On large codebases (500+ files), this can significantly reduce memory usage. Use --disable-rule=duplication.clone if you encounter out-of-memory errors. The level-narrowed spelling --disable-rule=duplication.clone:project skips it too: the channel reports at that one level, so silencing the level silences the rule. A producer stops as soon as the disable selectors together cover every level of every channel it emits — one level of a two-level channel leaves it running, since the other level still has findings to report.
--only-rule¶
Run only matching producer rules or finding channels. A selector is either an exact name
(a producer rule, a group like complexity, or a channel), or X.* for strictly its
descendants, either optionally narrowed to one level of the aggregation tree with :level.
A selector carrying a level keeps its producer running, since a producer filtered out would
never emit the level that was asked for. Can be repeated:
# Run only complexity rules
bin/qmx check src/ --only-rule=complexity.*
# Run two specific rules
bin/qmx check src/ --only-rule=complexity.ccn --only-rule=size.method-count
# Select one channel of a built-in health dimension: producer and channel
# share the name, since each of the six dimensions is its own producer
bin/qmx check src/ --only-rule=health.complexity
Selectors must match a registered producer, group, or emitted channel exactly, or resolve an
X.* to at least one descendant. Unknown selectors — including a bare prefix without the
star, or an X.* that matches nothing — fail closed with exit 3 before stdout receives a
report payload:
Likewise, the owner before : in --rule-opt=RULE:OPTION=VALUE must be an exact
producer rule, not a group or channel — a group or channel there is an error. The same rule
governs the rules: YAML section keys.
Every channel obeys selection, including the one assembled last
annotation.unused-directive — the "this suppression silenced nothing" verdict — can only be
reached once every other rule has produced its findings, so a run assembles it after rule
execution. It is selected like any other channel all the same:
--disable-rule=annotation.unused-directive (or annotation.unused-directive:file) silences
it, and an --only-rule that names other channels of annotation.directive without naming
this one does not report it.
The producer's exclusion options are a separate matter and do not reach this channel:
rules.annotation.directive.suppress_paths gates its early channels only, and
suppress_namespaces reaches none of them, since these findings are reported against the file
the annotation was written in.
--rule-opt¶
Override rule options from the command line. Format: rule-name:option=value, where
rule-name must be an exact producer rule — never a group, never a channel, and never a
wildcard. This is the same constraint that governs the owner before : in
--only-rule/--disable-rule and the rules: YAML section keys. Can be repeated:
bin/qmx check src/ --rule-opt=complexity.ccn:callable.warning=15
bin/qmx check src/ --rule-opt=complexity.ccn:callable.error=30
All three ways of getting it wrong are refused with exit 3, where the option
pair used to be dropped without a word: a value written without =VALUE, a
rule name no registered producer answers to, and an option name that rule does
not accept. The refusal for the last one lists the options the rule does
accept.
suppress_namespace_channels is configured in YAML, not through --rule-opt: each selector
requires a non-empty list of namespace patterns, while --rule-opt carries scalar values. Its
keys are channel selectors and follow the same exact-or-X.* rule as @qmx-ignore — a bare
prefix like health is now an error, not a shorthand for health.*. A key may add :namespace
and no other level: the option is offered namespace aggregates only, so any other level would
name a filter that can never fire.
Rule-specific shortcut flags¶
Many rules have dedicated CLI flags for quick rule-option configuration:
| Flag | Rule | Option |
|---|---|---|
--cyclomatic-warning=N |
complexity.ccn | callable.warning |
--cyclomatic-error=N |
complexity.ccn | callable.error |
--cyclomatic-class-warning=N |
complexity.ccn | class.max_warning |
--cyclomatic-class-error=N |
complexity.ccn | class.max_error |
--cognitive-warning=N |
complexity.cognitive | callable.warning |
--cognitive-error=N |
complexity.cognitive | callable.error |
--cognitive-class-warning=N |
complexity.cognitive | class.max_warning |
--cognitive-class-error=N |
complexity.cognitive | class.max_error |
--npath-warning=N |
complexity.npath | callable.warning |
--npath-error=N |
complexity.npath | callable.error |
--npath-class-warning=N |
complexity.npath | class.max_warning |
--npath-class-error=N |
complexity.npath | class.max_error |
--wmc-warning=N |
complexity.wmc | warning |
--wmc-error=N |
complexity.wmc | error |
--wmc-exclude-data-classes |
complexity.wmc | excludeDataClasses |
| Flag | Rule | Option |
|---|---|---|
--cbo-warning=N |
coupling.cbo | class.warning |
--cbo-error=N |
coupling.cbo | class.error |
--cbo-ns-warning=N |
coupling.cbo | namespace.warning |
--cbo-ns-error=N |
coupling.cbo | namespace.error |
--distance-warning=N |
coupling.distance | max_distance_warning |
--distance-error=N |
coupling.distance | max_distance_error |
--instability-class-warning=N |
coupling.instability | class.max_warning |
--instability-class-error=N |
coupling.instability | class.max_error |
--instability-ns-warning=N |
coupling.instability | namespace.max_warning |
--instability-ns-error=N |
coupling.instability | namespace.max_error |
--class-rank-warning=N |
coupling.class-rank | warning |
--class-rank-error=N |
coupling.class-rank | error |
| Flag | Rule | Option |
|---|---|---|
--class-count-warning=N |
size.class-count | warning |
--class-count-error=N |
size.class-count | error |
--method-count-warning=N |
size.method-count | warning |
--method-count-error=N |
size.method-count | error |
--property-count-warning=N |
size.property-count | warning |
--property-count-error=N |
size.property-count | error |
| Flag | Rule | Option |
|---|---|---|
--dit-warning=N |
design.dit | warning |
--dit-error=N |
design.dit | error |
--lcom-warning=N |
cohesion.lcom | warning |
--lcom-error=N |
cohesion.lcom | error |
--lcom-min-methods=N |
cohesion.lcom | minMethods |
--lcom-exclude-readonly |
cohesion.lcom | excludeReadonly |
--lcom-exclude-methods=NAME |
cohesion.lcom | excludeMethods |
--noc-warning=N |
design.noc | warning |
--noc-error=N |
design.noc | error |
--param-type-coverage-warning=N |
design.type-coverage.param | warning |
--param-type-coverage-error=N |
design.type-coverage.param | error |
--return-type-coverage-warning=N |
design.type-coverage.return | warning |
--return-type-coverage-error=N |
design.type-coverage.return | error |
--property-type-coverage-warning=N |
design.type-coverage.property | warning |
--property-type-coverage-error=N |
design.type-coverage.property | error |
--property-exclude-readonly |
size.property-count | excludeReadonly |
--property-exclude-promoted-only |
size.property-count | excludePromotedOnly |
| Flag | Rule | Option |
|---|---|---|
--mi-warning=N |
maintainability.mi | warning |
--mi-error=N |
maintainability.mi | error |
--mi-min-statements=N |
maintainability.mi | minStatements |
--mi-exclude-tests |
maintainability.mi | excludeTests |
| Flag | Rule | Option |
|---|---|---|
--constructor-overinjection-warning=N |
code-smell.constructor-overinjection | warning |
--constructor-overinjection-error=N |
code-smell.constructor-overinjection | error |
--data-class-woc-threshold=N |
design.data-class | wocThreshold |
--data-class-wmc-threshold=N |
design.data-class | wmcThreshold |
--data-class-min-members=N |
design.data-class | minMembers |
--data-class-exclude-readonly |
design.data-class | excludeReadonly |
--data-class-exclude-promoted-only |
design.data-class | excludePromotedOnly |
--data-class-exclude-exceptions |
design.data-class | excludeExceptions |
--god-class-wmc-threshold=N |
design.god-class | wmcThreshold |
--god-class-lcom-threshold=N |
design.god-class | lcomThreshold |
--god-class-tcc-threshold=N |
design.god-class | tccThreshold |
--god-class-class-loc-threshold=N |
design.god-class | classLocThreshold |
--god-class-min-criteria=N |
design.god-class | minCriteria |
--god-class-min-methods=N |
design.god-class | minMethods |
--god-class-exclude-readonly |
design.god-class | excludeReadonly |
--long-parameter-list-warning=N |
code-smell.long-parameter-list | warning |
--long-parameter-list-error=N |
code-smell.long-parameter-list | error |
--long-parameter-list-vo-warning=N |
code-smell.long-parameter-list | vo-warning |
--long-parameter-list-vo-error=N |
code-smell.long-parameter-list | vo-error |
--unreachable-code-warning=N |
code-smell.unreachable-code | warning |
--unreachable-code-error=N |
code-smell.unreachable-code | error |
| Flag | Rule | Option |
|---|---|---|
--circular-deps |
architecture.circular-dependency | enabled |
--max-cycle-size=N |
architecture.circular-dependency | maxCycleSize |
--layer-violation |
architecture.layer-violation | enabled |
--layer-violation-severity=SEVERITY |
architecture.layer-violation | severity |
--unassigned-class-mode=MODE |
architecture.unassigned-class | mode |
Other commands¶
baseline:cleanup¶
Inspect stale candidates in a baseline. Without --remove, it only lists them and never writes the file; remove an explicitly reviewed selector as described in Baseline:
bin/qmx baseline:cleanup baseline.json src/
bin/qmx baseline:cleanup baseline.json src/ --remove=<selector>
debug:layer-assignment¶
Report which architecture layer a class is assigned to, and every other layer whose criteria would also have matched it (a potential shadow source). See Inspecting layer assignment for a single class for the full walkthrough.
bin/qmx debug:layer-assignment 'App\Service\Foo'
bin/qmx debug:layer-assignment 'App\Service\Foo' --config qmx.yaml
# Machine-readable output — for agents and scripts, not for parsing the text report
bin/qmx debug:layer-assignment 'App\Service\Foo' --format=json
| Option | Description |
|---|---|
-c, --config=FILE |
Path to qmx.yaml (default: qmx.yaml in the current directory) |
--format=FORMAT |
text (default) or json |
--format=json serializes the same resolution the text report renders. The command answers only for classes the run analysed: an FQN naming no analysed declaration — a typo, or a class kept out of the run by paths, exclude or the generated-file filter — exits with code 3 and the error envelope instead of being classified. Schema:
{
"fqn": "App\\Service\\Foo",
"assigned": { "layer": "any-foo", "criteria": ["pattern \"App\\**\\Foo\""] },
"shadowed": [
{ "layer": "service", "criteria": ["pattern \"App\\Service\\**\""] }
],
"hasLayers": true
}
assignedisnullwhen no layer matched (emptyshadowedfollows).shadowedlists every other matching layer in declaration order — each entry would have won the assignment had it been declared beforeassigned.hasLayersdistinguishes "no layers configured" (false) from "layers configured but none matched this class" (truewithassigned: null).- On error,
--format=jsonprints{"error": "...", "exit_code": N}to stdout instead of the human<error>line, and an unrecognized--formatvalue exits with code 3 regardless of format.
directives¶
Report what every inline @qmx-ignore and @qmx-threshold in the analysed tree actually does. A suppression is judged by what it silenced; a threshold directive is judged by removing it and executing the rules again over the run's own measurements — by default only the rule it addresses, one execution per directive.
bin/qmx directives src/
# Re-execute every enabled rule instead of just the addressed one — the control the narrow default is measured against
bin/qmx directives src/ --sweep=full
# Machine-readable output — for agents, scripts and CI
bin/qmx directives src/ --format=json
| Option | Description |
|---|---|
-c, --config=FILE |
Path to qmx.yaml (default: qmx.yaml in the current directory) |
--format=FORMAT |
text (default) or json |
--sweep=SCOPE |
How much of the rule layer each counterfactual re-executes: narrow (default) or full |
--preset=PRESET |
Apply a named preset (repeatable) |
--only-rule=RULE |
Judge under a run that ran only these rules (repeatable) |
--disable-rule=RULE |
Judge under a run with these rules off (repeatable) |
--rule-opt=RULE:OPT=VAL |
Judge under a run with this rule option (repeatable) |
The four selection options exist because a verdict is relative to the run that produced it: point the command at the same rules and boundaries your CI checks with, or it will answer about a different run.
A @qmx-threshold names exactly one rule, so under --sweep=narrow a counterfactual re-executes only that rule. --sweep=full re-executes every enabled rule for the same verdicts, at far higher cost — it is not a slower fallback but the control that measures, rather than assumes, that removing a directive of one rule cannot move another rule's findings: the two scopes are swept over the same tree and compared verdict for verdict. On this project's own src/ the narrow sweep is several times cheaper and the two scopes agree on every verdict. Both the text report and --format=json state the sweep the verdicts were measured under.
Exit codes: 0 nothing inert, 2 at least one inert directive whose boundary was observable, 3 bad input or configuration — including a scope that analysed no PHP files at all (a directory with no PHP in it, an exclude that swallowed everything, or nothing but @generated files), 4 the run failed to parse part of the tree, 1 the command itself failed unexpectedly.
Four verdicts, of which three are answers and one is the absence of one:
| Verdict | What it states |
|---|---|
| effective | Removing it changes what the rules produce. |
| applied-boundary-only | It applied, and nothing moved except the boundary the finding prints. |
| inert | Removing it changes nothing. This is the only verdict that moves the exit code. |
| unmeasured | No answer is available, and the report says why: the producer did not run, the directive was already refused elsewhere, it carries no rule filter, or another directive of the same rule covers the same subject. |
A verdict is relative to the analysed scope
A threshold on a metric computed over the analysed subgraph — coupling above all — can be alive over the whole project and dead over one directory of it, and neither answer is wrong. Point the command at what the project actually analyses. The report prints the scope it measured under, and a run that failed to parse part of the tree exits 4 instead of calling anything dead.
Judged against what the rules produced, not against the report
suppress_paths, suppress_namespaces and suppress_namespace_channels suppress publication, not measurement. A directive that moved a finding inside an excluded namespace still did something, so the audit asks its question against every finding the rules produced, not against the report. The one channel outside that universe is annotation.unused-directive, which a run assembles after the rules have run — no directive may address it, so no verdict is judged against it.
The one thing a suppression is not credited with is silencing a configuration error (annotation.unresolved-directive and its two siblings). Those channels are exempt from annotation suppression by construction, not by configuration, so a directive aimed at one is reported inert however it is written.
annotation.unused-directive is exempt in a louder way: a directive addressing it is refused rather than judged, and the audit reports it unmeasured / already-refused — the same answer check gives as an annotation.unresolved-directive on that line.
The applied-boundary-only verdict deliberately makes no claim about direction. The rule layer has no notion of which way is stricter — coupling.instability is worse when higher, cohesion.tcc when lower — so a directive that tightens a boundary and one that raises a boundary the measured value had already passed are the same observable. In --format=json this verdict keeps the stable key overrun.
Where a rule publishes no boundary alongside its finding, an inert verdict carries a note saying so, and does not fail the build: a boundary the value had already passed would have looked identical, so demanding the directive be deleted would report an unasked question as proven debt. --format=json reports it as "boundary_observable": false.
On error, --format=json prints {"error": "...", "exit_code": N} to stdout instead of the human <error> line.
graph:export¶
Export the dependency graph for visualization:
# Export as DOT (default)
bin/qmx graph:export src/ -o graph.dot
# Export as JSON (aggregated adjacency list with metadata)
bin/qmx graph:export src/ --format=json -o graph.json
# Filter by namespace
bin/qmx graph:export src/ --namespace=App\\Service --namespace=App\\Repository
# Exclude namespaces
bin/qmx graph:export src/ --exclude-namespace=App\\Generated
# Change layout direction
bin/qmx graph:export src/ --direction=TB
# Disable namespace grouping
bin/qmx graph:export src/ --no-clusters
| Option | Description |
|---|---|
-o, --output=FILE |
Output file (default: stdout) |
-f, --format=FORMAT |
dot (default) or json |
-d, --direction=DIR |
Graph direction: LR, TB, RL, BT (default: LR) |
--no-clusters |
Do not group nodes by namespace |
--namespace=NS |
Include only these namespaces (repeatable) |
--exclude-namespace=NS |
Exclude these namespaces (repeatable) |
A --namespace value matching no vertex is refused with exit 3 rather than
exporting an empty graph. --exclude-namespace keeps its silence on purpose: a
missed exclusion leaves the picture whole, so the viewer loses nothing.
If any discovered file fails parsing or processing, graph:export exits 4 and
emits no partial graph. It does not create a missing output file and preserves
an existing destination byte-for-byte.
hook:install¶
Install a git pre-commit hook:
hook:status¶
Show the current status of the pre-commit hook:
hook:uninstall¶
Remove the pre-commit hook:
bin/qmx hook:uninstall
# Restore the original hook from backup
bin/qmx hook:uninstall --restore-backup
rules¶
List all available rules with their descriptions and CLI options:
Example output (for --group=complexity):
4 rules available
Complexity
complexity.cognitive Checks cognitive complexity at method and class levels
--cognitive-warning (--rule-opt=complexity.cognitive:callable.warning=...)
--cognitive-error (--rule-opt=complexity.cognitive:callable.error=...)
--cognitive-class-warning (--rule-opt=complexity.cognitive:class.max_warning=...)
--cognitive-class-error (--rule-opt=complexity.cognitive:class.max_error=...)
complexity.ccn Checks cyclomatic complexity at method and class levels
--cyclomatic-warning (--rule-opt=complexity.ccn:callable.warning=...)
--cyclomatic-error (--rule-opt=complexity.ccn:callable.error=...)
--cyclomatic-class-warning (--rule-opt=complexity.ccn:class.max_warning=...)
--cyclomatic-class-error (--rule-opt=complexity.ccn:class.max_error=...)
...
Usage: bin/qmx check --disable-rule=<name> | --only-rule=<name>
bin/qmx check --rule-opt=<name>:<option>=<value>
Rules are grouped by category, and each CLI alias is listed with the long
--rule-opt form it expands to. Default threshold values are not part of this
output — see Default thresholds.