Skip to content

INW019 diagram-missing-dependency

What it does

Opt-in, and a warning when on. Reads the marked Mermaid and PlantUML diagrams in the files the diagrams key lists, as INW017 and INW018 do, and compares them with the imports in the code. It reports an import from one layer or context the diagram draws into another one it draws when the diagram has no arrow for it. A whole-project run checks them; a check of some paths and the per-edit hook don't.

What counts as drawn:

  • In a %% inwards: layers diagram, a path of solid arrows from the importing layer to the imported one. api --> application --> domain draws an import from api into domain too, since a layer may import every layer below it and a diagram usually draws only the steps.
  • In a %% inwards: contexts diagram, a solid arrow from the importing context to the imported one, from any node inside the first to any node inside the second. depends-on isn't transitive, so neither is the diagram.

Each missing pair is reported once per diagram, on the line where the importing layer or context is first written, and the message names the first import behind it: the file, the line and what it imports.

Left alone: a layer or context the diagram doesn't draw, or draws only as a node with the class external; an import inside one layer or context; an import of anything outside every layer or context (the standard library, a third-party package).

Why is this bad

A diagram that shows fewer arrows than the code has tells every reader, and every agent that reads the docs before it writes code, that the code is more decoupled than it is. Someone who trusts the picture moves a module or removes a layer and breaks an import nobody drew. INW018 catches an arrow the config forbids; INW019 catches the arrow the diagram forgot.

[tool.inwards] stays the only source of truth (ADR-045): the diagram is compared with the code, never read as config.

Example

pyproject.toml
[tool.inwards]
diagrams = ["docs/*.mmd"]
layers = [
  { name = "domain", modules = ["shop.domain"] },
  { name = "application", modules = ["shop.application"] },
  { name = "infrastructure", modules = ["shop.infrastructure"] },
]

[tool.inwards.rules]
extend-select = ["INW019"]
shop/domain/order.py
class Order:
    pass
shop/application/place_order.py
from shop.domain.order import Order
shop/infrastructure/db.py
from shop.application.place_order import Order

A diagram that draws the infrastructure's link to the application dotted, as if it were no import:

docs/layers.mmd
%% inwards: layers
flowchart LR
  application --> domain
  infrastructure -.-> application
inwards check
docs/layers.mmd:4:3: INW019 (warning) Layer "infrastructure" imports layer "application" in the code (shop/infrastructure/db.py:1 imports shop.application.place_order.Order), and the diagram draws no arrow or path of arrows from "infrastructure" to "application".
  fix: Draw the import in the diagram: "infrastructure --> application".
    1. Arrows point from the importing layer to the imported one, and solid arrows only: a dotted one (-.->) stands for something other than an import.
    2. If the code shouldn't import "application" from "infrastructure", remove the import instead; INW001 reports it when [tool.inwards] forbids it.
  docs: https://sircypkowskyy.github.io/inwards/rules/INW019/

Fixed: the arrow is solid. The whole project passes:

docs/layers.mmd
%% inwards: layers
flowchart LR
  infrastructure --> application --> domain
inwards check

In a contexts diagram, with sales importing shop.catalog.api, the arrow from the checkout node to the api node draws that import; without it, INW019 reports sales on line 3:

pyproject.toml
[[tool.inwards.contexts]]
name = "sales"
modules = ["shop.sales"]
depends-on = ["catalog"]

[[tool.inwards.contexts]]
name = "catalog"
modules = ["shop.catalog"]
public = ["shop.catalog.api"]
docs/contexts.mmd
%% inwards: contexts
flowchart LR
  subgraph sales["shop.sales"]
    checkout["shop.sales.checkout"]
  end
  subgraph catalog["shop.catalog"]
    api["shop.catalog.api"]:::public
  end
  checkout --> api

How it works

The diagram is read once for INW017, INW018 and INW019 (the reader is described on INW017's page). The imports are the ones a whole-project run collects for INW004, with two differences:

  • An import under if TYPE_CHECKING: counts. INW004 leaves it out because it never runs and so can't close a cycle at run time. INW001, INW002 and INW003 judge it like any other import, and an arrow means "may import" in their sense, so a diagram that leaves a type-only import out draws less than the code depends on.
  • An import goes to the layer or context its target belongs to, by the same longest-prefix match INW001 and INW002 use, whether or not the target is a checked module.

A layer or context is drawn when a node that names it (by its id, or by its quoted label for a name that can't be a Mermaid id) is in the diagram and isn't external; in a contexts diagram, a node inside a subgraph stands for the subgraph's context. Arrows are the one-way solid or thick ones INW018 reads; dotted, open and two-way links draw no import.

The import skeleton can take an import line inside a string for an import. A finding that stands on such a file has the file read again with the full parse, and the diagrams are judged again, as INW004 confirms a cycle, so a finding always stands on a real import.

How to fix

  1. Draw the arrow the fix names, from the importing node to the imported one, as a solid arrow.
  2. In a layers diagram, an arrow to a layer in between is enough when that layer already has a path to the imported one.
  3. If the import shouldn't exist, remove it from the code instead. When [tool.inwards] forbids it, INW001 (layers) or INW002 (contexts) reports it too.
  4. If the diagram leaves a layer out on purpose, don't draw it, or mark its node :::external: INW019 then ignores every import into or out of it.

Fix safety

No automatic fix (autofix: false). Adding an arrow changes docs only, but only a person knows whether the picture or the code is wrong.

Configuration

Severity warning, so docs drift doesn't block the Stop gate by default. Opt-in: turn it on with extend-select, usually together with INW017 and INW018, and list the files in diagrams:

pyproject.toml
[tool.inwards]
diagrams = ["README.md", "docs/**/*.md"]

[tool.inwards.rules]
extend-select = ["INW017", "INW018", "INW019"]
severity = { INW019 = "error" }      # optional: block on drift

The rule has no options of its own; [tool.inwards.rules.diagram-missing-dependency] takes only modules, which has no effect, since a diagram isn't a module. A finding can't be suppressed inline, in the diagram or in the Python file: draw the arrow, or mark the node :::external. inwards import-diagram turns INW017 and INW018 on, not INW019, since a diagram imported before the code is written can't draw imports that don't exist yet.

Known limitations

  • Whole-project runs only. A check of some paths can't tell which imports the rest of the project makes.
  • Layers and contexts, not modules. An arrow between two module nodes inside one context isn't compared with the imports between those modules.
  • The message names one import. A pair with many imports behind it shows the first, in file path and source order; fix the diagram, not that one file.
  • The same limits as INW017: the Mermaid flowchart and PlantUML component subsets, one line at a time, and globs skip hidden directories.

References