Project 28: Compiler-Aware Deprecation and Migration Linter
Build a production-grade Elixir migration assistant that understands quoted syntax, compiler events, source locations, and the difference between detecting an unsafe call and safely proposing a replacement.
Quick Reference
| Attribute | Value |
|---|---|
| Difficulty | Level 3: Advanced |
| Suggested Seniority | Senior Elixir engineer |
| Time Estimate | 20-30 hours |
| Main Programming Language | Elixir |
| Alternative Programming Languages | Erlang for compiler tooling; Gleam for comparison |
| Coolness Level | Level 4: Hardcore Tech Flex |
| Business Potential | Level 2: Micro-SaaS / Pro Tool |
| Prerequisites | Mix projects, modules and functions, pattern matching, basic macros, ExUnit |
| Key Topics | quoted AST, compiler tracers, Macro.Env, diagnostics, source fidelity, safe migrations |
1. Learning Objectives
By completing this project, you will:
- Explain how Elixir source becomes quoted syntax and where lexical information is preserved or lost.
- Traverse and classify calls without evaluating the inspected project.
- implement a compiler tracer that performs minimal synchronous work and delegates analysis safely.
- Emit stable diagnostics with rule identifiers, severity, file, line, column, explanation, and migration guidance.
- Distinguish a detection rule from a source rewrite and prove when an automatic fix is unsafe.
- Package the analyzer as a documented Mix task that works in ordinary and umbrella projects.
2. All Theory Needed (Per-Concept Breakdown)
Quoted Syntax, AST Traversal, and Source Fidelity
Fundamentals
Elixir represents code as nested tuples, lists, atoms, literals, and metadata commonly called quoted syntax or an abstract syntax tree. A call usually contains a function name or qualified alias, metadata such as line and column, and a list of arguments. Literals may appear directly, while blocks and special forms have recognizable tuple shapes. quote creates this representation, Macro.prewalk/2 and related functions traverse it, and Code.string_to_quoted_with_comments/2 parses source while retaining comments separately. The tree is not a complete concrete-syntax tree: formatting, some parentheses, and the exact placement of comments are not inherently preserved in every AST node. A trustworthy linter therefore treats source metadata as evidence, resolves only what it can prove, and avoids pretending that every syntactic match identifies the same runtime call.
Deep Dive
The apparent simplicity of an Elixir call such as a qualified function invocation hides several resolution layers. The source may use a fully qualified module, an alias, an imported function, a locally defined function, a macro, or a variable whose syntax resembles a zero-arity call. The quoted representation records syntax, not the final runtime dispatch in every case. A linter that merely searches for a tuple containing a banned function name will produce embarrassing false positives. The central design question is therefore not “does this tree contain this atom?” but “what claim can this syntax and its lexical environment support?”
Start by defining rule precision tiers. A syntactic rule can identify an exact fully qualified call, such as a module alias followed by a named function and known arity. A lexical rule can additionally use Macro.Env information about aliases, imports, requires, and the current module. A semantic rule may depend on compiler tracer events that report resolved remote calls. Each tier should state its confidence and limitations. This makes uncertainty visible rather than burying it inside an overly clever matcher.
Metadata is part of correctness. File paths, line numbers, columns, end positions, and generated-code markers determine whether a diagnostic is actionable. Normalize paths relative to the project root so CI output is stable across machines. Preserve the original source excerpt separately from the normalized call identity. Never use formatted regenerated code as though it were the user’s exact input; formatting an AST is useful for a preview but may move comments or alter harmless stylistic choices.
Traversal should also respect scope. A match inside a quoted block used to generate code is not necessarily a direct call in the current module. Generated code, macro definitions, tests, vendored sources, and migration directories may require different policy. Pass a context accumulator during traversal that records whether the visitor is inside a quote, a definition, a guard, or a module attribute. This allows a rule to say “report direct production calls, annotate generated calls, and ignore fixtures” without duplicating the parser.
Autofix is a separate capability with a higher proof burden. A rename from Legacy.fetch(x) to Current.fetch(x) may appear mechanical, but the new function may return a different error shape, require an option, or have different evaluation behavior. A migration linter should produce three possible dispositions: diagnostic only, patch preview requiring review, or proven-safe rewrite. The default project scope stops at a preview. The preview includes the original range, replacement text, rule rationale, and preconditions. It must refuse a fix when aliases are ambiguous, comments intersect the replacement range, arity is uncertain, or the rule requires data-flow knowledge.
Finally, analysis must be deterministic. Given the same source, rule configuration, Elixir version, and dependency graph, the output order and identifiers should be stable. Sort diagnostics by normalized path and source position. Create fingerprints from rule ID plus a normalized call identity and surrounding structural context, not absolute workspace paths. Stable fingerprints let CI platforms distinguish a new problem from a moved or resolved one.
How this fits on the project
This concept drives the offline scanner, rule matcher, source excerpt renderer, patch-preview generator, and most unit tests.
Definitions and key terms
- Quoted syntax: Elixir’s data representation of source constructs.
- Metadata: Location and compiler annotations attached to AST nodes.
- Lexical environment: Alias, import, require, module, and context information available at a source location.
- Source fidelity: The degree to which an analysis or rewrite preserves the user’s exact text and intent.
- Rule confidence: An explicit statement of how strongly evidence identifies the targeted call.
Mental model diagram
source text
|
v
[parser] ---- comments ----> [source index]
|
v
[quoted tree] -- walk + context --> [candidate facts]
|
aliases/imports/arity evidence
|
v
[rule decision]
/ | \
ignore diagnostic patch preview
How it works
- Parse a file without evaluating it and retain comments and location metadata.
- Walk the quoted tree with a context accumulator.
- Convert interesting nodes into neutral call facts.
- Resolve aliases or imports only when evidence is available.
- Apply rules to facts and attach confidence and explanation.
- Map findings back to exact source excerpts.
- Invariant: a proposed edit never has lower confidence than its diagnostic.
- Failure modes include missing columns, generated nodes, ambiguous imports, malformed source, and version-dependent AST shapes.
Minimal concrete example
PSEUDOCODE
for each parsed node with context:
if node can become a call_fact:
resolved = resolve_as_far_as_provable(call_fact, context)
for each rule matching resolved:
emit diagnostic
attach patch preview only when all rule preconditions hold
Common misconceptions
- AST matching automatically resolves aliases and imports. It does not.
- Formatting a modified AST preserves exact comments and whitespace. It may not.
- A deprecated name always has a semantics-preserving replacement. Often it does not.
Check-your-understanding questions
- Why is a function-name atom insufficient evidence for a banned remote call?
- Why should diagnostic generation and patch generation be separate stages?
- Which data must be normalized to keep CI findings stable?
Check-your-understanding answers
- The same syntax may represent local calls, imports, macros, variables, or functions from different modules.
- Detection can be correct even when a safe textual or semantic rewrite cannot be proven.
- Paths, output ordering, source ranges, and finding fingerprints need deterministic normalization.
Real-world applications
- Framework upgrade assistants
- Organization-wide API policy enforcement
- Security deprecation campaigns
- Mechanical preparation for library major-version migrations
Where you will apply it
- Project 28 offline scan, source renderer, rule engine, and patch preview
- Project 33 when reasoning about Ecto query ASTs and adapter capabilities
References
Key insight
A useful migration tool reports only what its syntax and compiler evidence can actually prove.
Summary
Quoted syntax enables powerful analysis, but production trust comes from scope awareness, source fidelity, explicit confidence, and conservative fixes.
Homework/Exercises
- Classify five call forms as fully qualified, alias-resolved, imported, local, or ambiguous.
- Design a diagnostic fingerprint that survives different checkout paths.
Solutions
- Record the syntactic shape and list the environment facts needed before choosing a category; ambiguous forms stay unresolved.
- Hash the rule ID, normalized project-relative path, resolved call identity, and structural neighborhood while excluding absolute paths.
Compiler Tracers and Actionable Diagnostics
Fundamentals
Elixir compiler tracers observe events generated while files are compiled. A tracer implements a trace/2 callback and receives an event plus Macro.Env. Events can reveal resolved aliases, imports, remote function and macro calls, and lexical boundaries more accurately than a standalone text scan. The callback runs on the compilation path, so latency and failure behavior matter: slow work slows every compilation, and an unstable tracer can make a normal build unreliable. The tracer should capture a small immutable fact, hand it to a bounded collector, and return :ok. Diagnostics then need a stable schema and rendering layer so terminal users, CI systems, and editors receive the same underlying finding rather than three inconsistent interpretations.
Deep Dive
A compiler tracer gains semantic context by standing at a sensitive point in the toolchain. This is both its advantage and its operational risk. Events arrive while modules are being compiled, often concurrently, and their precise availability depends on the Elixir version and construct. A sound design begins with a versioned event adapter. The adapter accepts the raw event, recognizes supported shapes, and emits a small internal fact such as “resolved remote function call at this environment.” Unknown events are ignored or counted, never pattern-matched with a crash that aborts compilation.
The tracer callback should do no filesystem access, network calls, large AST traversal, rule configuration parsing, or report rendering. Those activities belong before compilation or after fact collection. Synchronous work should be bounded: validate the event shape, copy required scalar data, enqueue the fact, and return. If the collector is unavailable, decide the failure policy explicitly. A developer-mode default may record a one-time warning and continue, while a policy-enforcement CI mode may fail after compilation with a clear tool error. The project must never deadlock the compiler waiting on its own analysis process.
Compilation concurrency introduces ordering questions. Event arrival order is not a stable report order, and a module can be recompiled more than once. Give each compilation session an identifier and deduplicate facts using a structural key. At the end of a run, freeze the collected facts, apply the configured rules, sort findings deterministically, and render. The collector must have a bounded memory strategy: reject unexpectedly huge facts, cap retained source excerpts, and optionally spill neutral facts to a temporary file for very large umbrella projects.
Diagnostics are an API. Define required fields: schema version, rule ID, severity, title, explanation, normalized file, start line and column, optional end position, resolved subject, confidence, suggested action, and fingerprint. Terminal output should be concise, while JSON or SARIF-like output can preserve details. Exit policy is separate from severity: teams may allow warnings locally but fail CI on the same rule. Do not bake policy into individual rule matchers.
Rule configuration also needs validation. Unknown severities, duplicate rule IDs, invalid module/function/arity patterns, and contradictory include/exclude globs should fail before compilation starts. Compile the configuration into efficient match indexes keyed by module and function where possible. A wildcard rule is useful but should not force every event through an expensive linear scan.
Testing a tracer requires more than testing its rule functions. Compile fixture projects in isolated temporary build paths, capture the resulting facts, and confirm that tracer presence does not change compiled behavior. Measure overhead against a baseline project. Inject a collector crash and an unknown event to confirm the documented failure policy. Run with parallel compilation repeatedly and compare byte-for-byte normalized output. The project is successful when developers trust both its findings and its restraint.
Tracer registration also needs lifecycle hygiene. Save the previous compiler option, add the session tracer only for the intended compilation, and restore the prior option even when compilation raises. Tests should prove that a completed or failed audit does not leak its tracer into later unrelated Mix tasks in the same VM. Session finalization must reject late facts from an earlier run rather than attaching them to a new report.
How this fits on the project
This concept defines the live compiler integration, collector lifecycle, normalized finding schema, output renderers, and performance budget.
Definitions and key terms
- Compiler tracer: A callback module that observes compiler events.
- Fact: A neutral observation captured before policy rules are applied.
- Finding: A fact that matches a configured rule and has user-facing meaning.
- Renderer: A formatter for terminal or machine-readable output.
- Failure policy: The declared behavior when the tool itself cannot complete analysis.
Mental model diagram
Mix compile
|
parallel compiler workers
| event + Macro.Env
v
[tiny tracer callback] -- bounded fact --> [session collector]
|
compile completes
|
v
[rule evaluation]
/ | \
terminal JSON CI exit
How it works
- Validate and compile rule configuration before enabling the tracer.
- Start a session-scoped collector.
- Register the tracer through the documented compiler option.
- Convert supported compiler events into bounded facts.
- Finalize the session after compilation and deduplicate facts.
- Apply rules, sort findings, render, and determine exit status.
- Invariant: tracer callbacks never perform unbounded or blocking analysis.
- Failure modes include unsupported events, collector exit, duplicate compilation, mailbox growth, and version drift.
Minimal concrete example
PSEUDOCODE
on compiler_event(event, environment):
case event_adapter(event, environment):
supported fact -> nonblocking_record(session, fact)
unsupported -> increment_ignored_counter()
return ok
Common misconceptions
- A compiler tracer is an appropriate place to render warnings synchronously.
- Event arrival order is a stable source order.
- A warning’s severity must dictate the process exit code.
Check-your-understanding questions
- Why should rule evaluation occur after fact collection?
- What prevents parallel compilation from producing flaky output?
- How should an unknown tracer event be handled?
Check-your-understanding answers
- It keeps callbacks fast and separates observation from policy.
- Session IDs, deduplication, deterministic sorting, and normalized paths.
- Ignore or count it through a version adapter; never crash compilation through an incomplete match.
Real-world applications
- CI deprecation gates
- Compiler-integrated architectural boundaries
- Upgrade readiness reports
- Security-sensitive API inventories
Where you will apply it
- Project 28 compiler mode, umbrella support, and CI renderer
References
Key insight
Compiler integration is valuable only when the observer remains cheaper and safer than the compilation it observes.
Summary
Tracer architecture requires bounded callbacks, session isolation, deterministic finalization, a version adapter, and a diagnostics schema treated as a public contract.
Homework/Exercises
- Set a tracer overhead budget and name the measurements needed to enforce it.
- Define local and CI failure policies for collector failure.
Solutions
- Compare clean and incremental compile wall time, peak memory, fact count, and collector queue length against a tracer-free baseline.
- Local mode may warn and continue; enforcement mode should finish compilation, emit a tool-failure diagnostic, and exit with a distinct infrastructure code.
3. Project Specification
3.1 What You Will Build
Build mix migrate.audit, a Mix task with two complementary engines: an offline source scanner for patch previews and a compiler-tracer mode for high-confidence resolved-call detection. Rules identify deprecated or organization-banned module/function/arity combinations, attach migration guidance, and emit terminal plus versioned JSON output.
3.2 Functional Requirements
- Load and validate a versioned rule file before analysis.
- Scan selected Elixir files without evaluating them.
- Observe supported compiler call events with minimal callback work.
- Resolve exact remote calls and label lower-confidence syntactic findings.
- Emit stable findings with source positions and fingerprints.
- Produce patch previews only for rules whose safety preconditions hold.
- Support umbrella applications and include/exclude path policy.
- Return distinct exit statuses for clean, policy violation, parse failure, and tool failure.
3.3 Non-Functional Requirements
- Incremental compilation overhead stays below a documented budget on the reference fixture.
- Repeated parallel runs generate byte-identical normalized JSON.
- The analyzer never evaluates target source or loads target modules merely to inspect them.
- Rules and output schemas are versioned and backward compatibility is documented.
- Diagnostics explain uncertainty instead of hiding it.
3.4 Example Usage / Output
$ mix migrate.audit --rules config/migration-rules.exs --format terminal
lib/billing/charge.ex:42:11 [MIG-014] warning
LegacyMoney.to_float/1 loses decimal precision.
Use Money.to_decimal/1 and keep arithmetic in decimal form.
confidence=resolved-call autofix=unavailable
summary files=84 facts=391 findings=1 errors=0
exit=2 policy_violation
3.5 Data Formats / Schemas / Protocols
RULE CONFIGURATION SHAPE
schema_version: 1
rules:
- id: MIG-014
match: {module, function, arity}
severity: warning
message: human explanation
replacement: optional preview specification
preconditions: explicit safety checks
FINDING SHAPE
{schema_version, rule_id, severity, file, start, end,
resolved_subject, confidence, message, suggestion, fingerprint}
3.6 Edge Cases
- Aliased modules with the same short name in different scopes
- Imported functions shadowed by local definitions
- Calls generated inside quoted macros
- Syntax errors in one file while other files remain analyzable
- Files with Unicode before the reported column
- Umbrella children compiled concurrently
- Generated files and dependencies appearing under custom source paths
- Deprecated macro calls that resemble ordinary functions
3.7 Real World Outcome
3.7.1 How to Run
$ mix deps.get
$ mix test
$ mix migrate.audit --fixture test/fixtures/legacy_app --format json
3.7.2 Golden Path Demo
The fixture contains one fully qualified deprecated call, one alias-resolved call, one ambiguous imported name, and one safe replacement. The tool reports the first two, labels the imported case as ambiguous only in offline mode, and offers a patch preview solely for the rule with proven textual preconditions.
3.7.3 Exact Terminal Transcript
$ mix migrate.audit --fixture test/fixtures/legacy_app --format summary
session=7f2c mode=compiler+source
files_parsed=6 compile_facts=18
MIG-001 resolved=2 ambiguous=1 ignored_generated=1
patch_previews=1 automatically_applied=0
result=policy_violation
3.8 Scope Boundary Versus Projects 1-13
Projects 1-13 teach runtime processes, supervision, distribution, ETS, real-time flow, telemetry, and releases. This project operates at source and compile time. It does not build another supervised service, runtime metrics pipeline, or hot-upgrade mechanism. Its central feedback loop is compiler evidence to migration guidance.
4. Solution Architecture
4.1 High-Level Design
+-------------------+
rule file ------>| config validator |----> compiled rule index
+-------------------+ |
v
source files --> parser --> contextual AST facts --> matcher
^
Mix compile --> tracer --> bounded collector --------+
|
v
findings + patch previews
/ | \
terminal JSON exit policy
4.2 Key Components
| Component | Responsibility | Important decision |
|---|---|---|
| Rule Loader | Validate and index policies | Reject invalid configuration before compilation |
| Source Index | Parse files and preserve comments/ranges | Never evaluate inspected source |
| Contextual Walker | Turn nodes into call facts | Preserve ambiguity and quote context |
| Event Adapter | Normalize supported compiler events | Isolate Elixir-version differences |
| Fact Collector | Store bounded session facts | Never block compiler workers on analysis |
| Rule Engine | Convert facts to findings | Separate detection from exit policy |
| Patch Previewer | Suggest conservative edits | Refuse unsafe or comment-crossing changes |
| Renderer | Terminal and versioned JSON | Deterministic sorting and fingerprints |
4.3 Data Structures (No Full Code)
Rule: stable ID, matcher, severity, explanation, replacement policy, confidence floor.CallFact: source location, syntactic identity, resolved identity if known, lexical context, origin engine.Finding: normalized public diagnostic contract.PatchPreview: source range, original digest, replacement text, preconditions, refusal reason.AnalysisSession: ID, compiler version, rule digest, counters, start/end state.
4.4 Algorithm Overview
Index exact rules by module/function/arity and keep wildcard rules in bounded secondary indexes. Gather facts from both engines, deduplicate by source position and resolved subject, then choose the highest-confidence evidence. Match rules, materialize findings, validate any proposed patch against the original source digest, sort, render, and apply exit policy.
For F facts and R_w wildcard rules, exact matching is approximately O(F) while wildcard work is O(F * R_w). The design should keep R_w intentionally small. Memory is O(F + D), where D is retained diagnostic/source data.
5. Implementation Guide
5.1 Development Environment Setup
Use a current stable Elixir and Erlang/OTP pair, create an isolated Mix library, and keep fixtures in separate temporary build paths. No third-party parser is required for the core project.
5.2 Project Structure
migration_linter/
├── lib/
│ ├── mix/tasks/migrate.audit.ex
│ └── migration_linter/
│ ├── rule_loader.ex
│ ├── source_index.ex
│ ├── walker.ex
│ ├── tracer.ex
│ ├── event_adapter.ex
│ ├── collector.ex
│ ├── matcher.ex
│ ├── patch_preview.ex
│ └── renderer.ex
├── test/fixtures/
└── test/
5.3 The Core Question You Are Answering
“How can a tool use compiler knowledge to accelerate migrations without making claims or edits it cannot prove safe?”
5.4 Concepts You Must Understand First
- Quoted expressions and metadata — Metaprogramming Elixir by Chris McCord, Chapters 1-3.
- Macros and lexical environment — Metaprogramming Elixir, Chapters 4-6.
- Mix tasks and compilation — official
Mix.Task,Mix.Tasks.Compile, andCodedocumentation. - Types and public diagnostic contracts — Programming Elixir ≥ 1.6 by Dave Thomas, “Protocols” and “More Cool Stuff” chapters.
5.5 Questions to Guide Your Design
- Which findings require resolved compiler evidence, and which may be syntactic hints?
- How will output remain stable under parallel compilation?
- What is the exact refusal policy for a patch preview?
- How does the tool behave when its collector crashes?
- Which schema fields are public compatibility commitments?
5.6 Thinking Exercise
Trace four forms of fetch(value): a local function, an imported function, a macro, and a variable invocation. Write down what the offline AST knows, what Macro.Env adds, and what a compiler event may resolve. The correct design preserves “unknown” instead of guessing.
5.7 The Interview Questions They Will Ask
- “What information is lost when Elixir source is converted to quoted syntax?”
- “Why can a compiler tracer slow unrelated modules?”
- “How would you resolve aliases and imports without evaluating a project?”
- “What makes a source rewrite semantics-preserving?”
- “How would you version diagnostics consumed by CI and editors?”
- “How do you test deterministic output under parallel compilation?”
5.8 Hints in Layers
Hint 1: Begin with facts — Define a neutral call-fact schema before writing any migration rule.
Hint 2: Separate engines — Make source scanning and compiler tracing independent producers of the same fact shape.
Hint 3: Keep the tracer tiny — Adapt, enqueue, return. Parse configuration and render reports elsewhere.
Hint 4: Make refusal visible — A patch preview should explain why it was withheld, such as ambiguity or intersecting comments.
5.9 Books That Will Help
| Topic | Book | Precise reading |
|---|---|---|
| Quoted syntax | Metaprogramming Elixir — Chris McCord | Chapters 1-3, “The Language of Macros” through AST traversal |
| Macro APIs | Metaprogramming Elixir — Chris McCord | Chapters 4-6, macro hygiene and compile-time hooks |
| Elixir abstractions | Programming Elixir ≥ 1.6 — Dave Thomas | “Protocols — Polymorphic Functions” and “Macros and Code Evaluation” |
| Compiler perspective | The BEAM Book — Erik Stenman | “Code Loading” and compiler/BEAM file discussions |
5.10 Implementation Phases
Phase 1: Source Facts and Rules (5-7 hours)
- Define versioned rule, fact, finding, and refusal schemas.
- Parse fixtures and traverse fully qualified calls.
- Add alias-aware facts and deterministic terminal output.
Phase 2: Compiler Integration (6-9 hours)
- Add the versioned event adapter and bounded collector.
- Correlate resolved facts with source facts.
- Measure clean and incremental compilation overhead.
Phase 3: Migration Guidance (5-7 hours)
- Add source excerpts and conservative patch previews.
- Add JSON output, stable fingerprints, and exit policy.
- Support umbrella child paths and configuration inheritance.
Phase 4: Hardening (4-7 hours)
- Run concurrency determinism tests.
- Inject collector, parser, and configuration failures.
- Document supported event/version matrix and known ambiguities.
5.11 Key Implementation Decisions
| Decision | Options | Recommendation | Rationale |
|---|---|---|---|
| Primary evidence | AST only / tracer only / both | Both, unified as facts | Balances source previews with resolved calls |
| Tracer transport | synchronous calls / bounded asynchronous collector | Bounded asynchronous | Protects compilation latency |
| Autofix | apply automatically / preview / none | Preview with refusal reasons | Avoids silent semantic changes |
| Rule policy | hard-coded / versioned configuration | Versioned configuration | Enables adoption and CI control |
| Output order | event arrival / source order | Normalized source order | Deterministic across compilation schedules |
6. Testing Strategy
6.1 Test Categories
| Category | Purpose | Examples |
|---|---|---|
| Unit | Validate node classification and rules | aliases, imports, quoted blocks, arity |
| Property | Stress AST walkers and normalization | arbitrary valid nested quoted forms |
| Integration | Compile fixture applications | resolved function and macro events |
| Golden | Protect output contracts | terminal and JSON snapshots with normalized paths |
| Failure | Verify tool resilience | parser error, collector exit, invalid rule file |
| Performance | Enforce tracer budget | clean and incremental compilation baselines |
6.2 Critical Test Cases
- Fully qualified and alias-resolved calls produce one deduplicated finding.
- A same-named local function is not reported as the remote deprecated API.
- An ambiguous imported call is never offered an automatic patch.
- A deprecated macro is classified distinctly from a function.
- Comments intersecting a replacement range cause a documented refusal.
- Unicode source before a finding yields the correct displayed column.
- Twenty parallel compile runs produce identical normalized JSON.
- Collector failure follows local and CI failure policies without deadlock.
- An unsupported compiler event is counted and ignored safely.
- Umbrella child paths and duplicate compilation facts normalize correctly.
6.3 Test Data
Maintain tiny fixture projects for aliases, imports, macro-generated calls, syntax errors, umbrella children, and comments around candidate replacements. Store expected facts separately from rendered output so renderer changes do not hide semantic regressions.
6.4 Definition of Done
- Offline and compiler engines share a documented versioned fact schema.
- Exact remote calls are detected without false positives in all reference fixtures.
- Ambiguous findings state their confidence and never receive unsafe patches.
- Output is byte-identical across repeated parallel runs after normalization.
- JSON fields, exit statuses, and failure policies are documented.
- Tracer overhead stays within the declared reference budget.
- Tool failure, policy violation, and target parse failure are distinguishable.
- Existing project behavior is unchanged when the tracer is enabled.
- All tests pass without evaluating inspected source.
- README includes supported Elixir versions and event compatibility limits.
7. Common Pitfalls & Debugging
7.1 Frequent Mistakes
| Problem | Why it happens | Fix | Quick test |
|---|---|---|---|
| Local call reported as remote API | Matcher searches only by function name | Require module/lexical evidence and confidence tiers | Add a local same-name fixture |
| Compile becomes slow | Tracer performs matching or I/O synchronously | Capture bounded facts and analyze after compilation | Compare incremental compile baseline |
| Duplicate findings | Source and tracer facts are rendered independently | Correlate and keep strongest evidence | One known call must yield one fingerprint |
| Patch moves comments | AST formatting is treated as concrete syntax | Use exact ranges and refuse intersecting comments | Place comments inside candidate call |
| CI findings churn | Absolute paths or event order enter fingerprints | Normalize paths and sort by source location | Run from two checkout roots |
| Analyzer crashes on new Elixir | Raw event matching is exhaustive and brittle | Add a version adapter with safe unknown handling | Feed a synthetic unknown event |
7.2 Debugging Strategies
- Add an opt-in fact dump that contains normalized neutral facts, not target secrets.
- Render the AST path and lexical context for a single selected source range.
- Track collector queue high-water mark and ignored-event counts.
- Compare offline and compiler evidence for one finding before changing rules.
- Reproduce path issues inside a temporary checkout with a different absolute root.
7.3 Performance Traps
- Re-parsing rule configuration for every compiler event
- Copying entire ASTs or source files into the collector
- Linear matching against hundreds of wildcard rules
- Holding full source excerpts for clean files
- Synchronous calls from parallel compiler workers to one serialized analyzer
8. Extensions & Challenges
8.1 Beginner Extensions
- Add terminal color with a
--no-colordeterministic mode. - Add rule documentation links and per-rule counts.
- Support baseline suppression by stable fingerprint.
8.2 Intermediate Extensions
- Emit SARIF-compatible output for code-host annotations.
- Add a rule author test kit with positive and negative fixtures.
- Compare findings between two dependency-lock versions.
8.3 Advanced Extensions
- Build a source-preserving rewrite engine with explicit semantic preconditions.
- Add cross-file facts such as deprecated callback implementations.
- Run organization-wide scans while isolating each project’s compiler environment.
- Publish the core rule engine as a Hex package with compatibility guarantees.
9. Real-World Connections
9.1 Industry Applications
Large Elixir estates need controlled framework upgrades, security API removals, architectural policy checks, and evidence that migration campaigns are shrinking. Compiler-aware findings reduce false positives, while conservative patch previews prevent a productivity tool from becoming a source-corruption tool.
9.2 Related Open Source Projects
- Elixir’s compiler tracer interface, the primary integration contract
- Credo, useful for comparing lint-rule ergonomics and reporting concepts
- Sourceror, an optional source-preserving AST ecosystem reference for an extension, not required by the core project
9.3 Interview Relevance
This project demonstrates mastery of compile-time versus runtime execution, macro environments, build-tool integration, diagnostic API design, performance budgeting, and conservative automation—signals expected from a senior Elixir engineer working on platform tooling.
10. Resources
10.1 Essential Official Reading
- Elixir
Codemodule and compilation tracers - Elixir
Macrotraversal and expansion APIs - Elixir
Macro.Env - Elixir
Modulecompile callbacks - Elixir quoted expressions
- Mix.Task behaviour
10.2 Books and Deeper Study
- Metaprogramming Elixir by Chris McCord — Chapters 1-6.
- Programming Elixir ≥ 1.6 by Dave Thomas — protocols, macros, and code evaluation chapters.
- The BEAM Book by Erik Stenman — code loading and BEAM file discussions.
10.3 Verification Checklist Before Sharing the Project
- Confirm every referenced compiler event exists in the supported Elixir version.
- Publish a measured overhead table rather than claiming the tracer is “fast.”
- Include fixtures demonstrating false-positive resistance.
- Show at least one refused patch and explain why refusal is correct.
- Keep all examples as pseudocode, schema descriptions, CLI transcripts, or protocol records rather than a full solution.