Source Analysis
It answers “what did this change actually touch, and what has to be rebuilt, reviewed, or
re-tested because of it?” — from .al source and git alone. It never invokes a compiler,
restores symbols, or reaches the network, so it runs in seconds on a fresh checkout and returns
the same answer every time for the same input.
alwasp analyze # whole config, no change detection
alwasp analyze ci --changed-since latest # everything a release tag's diff implies
alwasp analyze --project src/Core --json out/analysis.json
alwasp analyze ci --changed-since main --include impacted-tests,affected-apps
Sections
| Section | What it answers |
|---|---|
affected-apps |
Which apps a change reaches, and by which of three routes |
dependency-graph |
App-level dependencies, build order by level, and any cycles |
object-graph |
Every declared object, where it lives, and how connected it is |
changed-objects |
Which objects were added, modified, removed, or merely moved |
public-api |
The surface another app can bind to, per object |
references |
Which object references which, what is external, and what could not be resolved |
impacted-tests |
Which test codeunits a change reaches, and the chain that proves it |
Every section is produced by default; --include narrows the report to the named sections
(repeat the option or pass a comma-separated list). A section that wasn’t requested is absent
from the JSON rather than empty, so a consumer can tell “not asked for” from “nothing found”.
schemaVersion, scope, summary, and diagnostics are not sections — they’re always present
and --include never affects them.
Changed objects, not changed files
Change detection reuses the same --changed-since / changeDetection configuration as build
and version apply — see Configuration. Where a build
maps changed files to projects, analyze goes one level deeper: it parses each changed file at
both refs and compares them object by object. Editing one codeunit in a file that declares three
does not report the other two as changed, and moving an object to another file — with its text
unchanged — is reported as Moved and impacts nothing. Moving source into .alpackages or bin
takes it out of analyzable scope and is reported as Removed.
Deleting an object propagates too: the head revision has no node for it, but the objects that still name it do, so removing a public object while a downstream app or test still references it reports those dependents and warns that the references no longer resolve.
Impacted tests
Impact propagates along the object graph in the direction changes travel — from an object to
everything that references it. A test codeunit (Subtype = Test, or any codeunit with a [Test]
procedure) reached this way is reported with the chain that reached it:
Impacted tests (1 codeunit(s), 2 test procedure(s)):
sales-tests: codeunit "Sales Post Tests" — 2 test(s), distance 2
via codeunit "Wasp Mgt" -> codeunit "Sales Poster" -> codeunit "Sales Post Tests"
The path is the evidence for the verdict, so a result is reviewable rather than taken on faith.
--max-depth caps how far impact propagates.
What counts as a reference
Only forms that can only mean a reference are counted: extends/implements, a declared type
(a variable, a table field, a parameter or return type), an object-id expression
(Codeunit::"Foo", Database::"Customer"), an [EventSubscriber] binding, and the properties
whose value names an object — SourceTable, TableRelation, CalcFormula, RunObject,
Permissions, IncludedPermissionSets, and a report/query dataitem. Names are never guessed
from text; anything inside a comment or string literal is not source.
Reference kinds
kind |
The form that produced it |
|---|---|
Extends |
An extension object’s extends target |
Implements |
An implements interface, or a page customization’s customizes target |
DeclaredType |
A declared type outside a member signature — a variable, a table field |
ParameterType |
A declared type in a procedure’s parameter list |
ReturnType |
A procedure’s declared return type |
ObjectIdExpression |
Codeunit::"Foo", Database::"Customer" |
EventSubscriber |
The publisher named by an [EventSubscriber] attribute |
PropertyReference |
A typed object in a property value, e.g. RunObject = Page "X" |
SourceTable |
A page/query/report SourceTable |
TableRelation |
A TableRelation target table |
CalcFormula |
A table named by a field’s CalcFormula |
DataItem |
A report or query dataitem table |
Permission |
A Permissions entry or an IncludedPermissionSets member |
Reference resolution
Every reference also carries what’s actually known about its target, so a parser limitation, invalid source, and a legitimate external reference are never mistaken for one another.
resolution |
Meaning | Where it appears |
|---|---|---|
Resolved |
The target is declared exactly once in the analyzed source | references.resolved |
External |
Not declared in the analyzed source — the base application, or a dependency outside the selection. Never diagnosed | references.external |
Ambiguous |
The name is declared more than once; the edge points at the first declaration | references.resolved |
Invalid |
The reference form is present but names no target | references.unresolvable |
Unsupported |
A form that cannot be resolved without a compiler | references.unresolvable |
Object identity
Identity is the object’s kind plus name, compared case-insensitively — the same identity AL resolves a reference by. The declared object number is not part of it, so:
- Renaming an object is a
Removedplus anAdded, because that’s what it is for every referrer of the old name. When a removal and an addition share a kind and object number, anObjectRenameddiagnostic (info) names both sides. - Renumbering keeps the identity. The object is
Modified;idandpreviousIdcarry both numbers, and anObjectIdChangeddiagnostic (warning) reports it. - Changing an object’s kind creates a new identity — removal plus addition, with an
ObjectTypeChangeddiagnostic (warning) when name and number are otherwise the same. - A declaration crossing an app boundary is
MovedorModifiedwithpreviousAppset and anObjectMovedBetweenAppsdiagnostic (warning); both apps are directly affected. - Two declarations of one identity is invalid AL: both are reported as a duplicate, the graph
resolves the name to the first, every reference to it becomes
Ambiguous, and aDuplicateObjectIdentitydiagnostic (error) is emitted.
Diagnostics
diagnostics is always present and --include never gates it. Codes are stable and suitable
for pipeline policy:
| Code | Severity | Meaning |
|---|---|---|
SourceFileUnreadable |
error | A file inside a selected project could not be read |
UnterminatedObjectBody |
error | An object declaration has no closing brace |
DuplicateObjectIdentity |
error | Two declarations share one kind + name |
RemovedObjectStillReferenced |
error | A deleted object is still named by source that remains |
DependencyCycle |
error | The analyzed apps form a dependency cycle |
ChangeDetectionFailed |
error | The git diff behind change detection failed |
AmbiguousObjectReference |
warning | A reference whose target is declared more than once |
UnsupportedObjectReference |
warning | A reference site the analyzer cannot resolve without a compiler |
InvalidObjectReference |
warning | A reference form that names no target |
ObjectIdChanged |
warning | An object kept its identity but changed its object number |
ObjectTypeChanged |
warning | One name and number, two different object kinds |
ObjectMovedBetweenApps |
warning | A declaration moved from one analyzed app to another |
BaseRevisionUnavailable |
warning | A file in the diff could not be read at the base ref |
ObjectRenamed |
info | A removal and an addition share a kind and an object number |
RemovedObjectUnreferenced |
info | An object was removed and nothing analyzed still references it |
Severity classifies the finding; it does not set the exit code. analyze exits 0 whenever
the analysis completed, and non-zero only for a usage or I/O error — a pipeline decides its own
policy from the codes.
Documented limitations
analyze reads source and git, never symbols, so some things it deliberately can’t answer —
each is reported rather than silently dropped:
- Objects resolve by name, not by number —
Codeunit::50100isUnsupported, not an edge. [EventSubscriber]publishers must be a static<ObjectType>::"<Name>"expression; a variable or constant in that slot isUnsupported.- A duplicated name resolves to the first declaration; the choice is reported as
Ambiguousrather than hidden. - A reference to an undeclared name is assumed external — without symbol packages there’s no way to prove it belongs to the base application rather than a typo.
#ifbranches are all kept — impact analysis wants the superset.- Identity-change diagnostics are scoped to one app, since object numbers repeat across apps.
Affected apps
An app is affected for one of three reasons, reported strongest-evidence-first:
DirectlyChanged— a file inside the project changed (source or not:app.json, XLIFF, and permission XML count).ReferencesChangedObject— one of its objects references a changed object.DependsOnChangedApp— itsapp.jsondepends, transitively, on a directly changed app, even with no object-level reference.
Output
The console form is a summary capped at --max-items entries per section (0 or --verbose
lists everything). --json <path> writes the complete report, as does --format json, which
carries it in the result document’s data.report.
analyze is a reporting command: it exits 0 whenever the analysis completed. Everything it
found — duplicate object names, dependency cycles, unreadable files, unresolvable references — is
reported in diagnostics, never by an exit code.
Report contract
The report is a versioned contract meant to be read by CI pipelines, other tools, and agents.
schemaVersionidentifies the JSON contract, not the ALWasp package version. It’s currently"1.0". MAJOR changes when a consumer of the previous version can break; MINOR changes when the document is extended in a way an existing consumer can ignore.- The full document is described by a checked-in JSON Schema,
schemas/alwasp-analyze-report.schema.json, covering all seven sections plusscope,summary, anddiagnostics, withadditionalProperties: falseand every enum value enumerated.
Determinism
The report carries no timestamps, no durations, no absolute paths, and no random identifiers. Paths are repository-relative with forward slashes, so a report produced on Windows and one produced on Linux compare equal byte for byte. Every collection has one documented, enforced order, built only from values the document itself carries — ordinal comparisons only, never file-system enumeration, hash iteration, OS, culture, or thread scheduling.