Guides: Exceptions and baselines: Permit one import that breaks a rule on purpose, with a reason. Record the old problems of a codebase in a baseline that only gets smaller. # Exceptions and baselines > Permit one import that breaks a rule on purpose, with a reason. Record the old problems of a codebase in a baseline that only gets smaller. This guide tells you how to let code break a rule for now, and keep each case visible. Archangel has two ways: * An **exception** is a comment above one import, with the rule and a reason. * The **baseline** is a file that records each problem that existed when a rule started. Each check counts both, for each rule, in its summary. So a team can see the numbers go down. Neither one turns a rule off. ## Before you start * Read [Getting started](/archangel/start/getting-started/). The example is a shop whose features must not import each other: archangel.config.ts ```ts import { defineConfig } from '@enhance-eng/archangel'; export default defineConfig({ parts: { feature: 'src/features/{feature}/**', shared: 'src/shared/**', }, rules: ({ mayImport }) => [ mayImport('app', { feature: ['shared'], shared: [], }, { because: 'A feature changes alone. Code that two features need goes in shared.', }), ], }); ``` ## Write an exception An exception is a comment directly above the import: above the line of its specifier, or above the first line of its statement when the statement takes more than one line. It names the rule, and it gives the reason after a colon: ```ts // archangel-allow app.feature: checkout shows the cart total until the total moves to shared import { cartTotal } from '../cart'; ``` 1. Look at the checkout feature. It imports two other features, and each import has an exception: src/features/checkout/index.ts ```ts // archangel-allow app.feature: checkout shows the cart total until the total moves to shared import { cartTotal } from '../cart'; // archangel-allow app.feature import { currentUser } from '../account'; export function checkoutSummary(prices: number[]): string { return `${currentUser()}: ${cartTotal(prices)}`; } ``` 2. Check: Terminal ```ansi $ npx archangel check error[exception-without-reason]: the exception for "app.feature" gives no reason, so it permits nothing; write the reason after the colon --> src/features/checkout/index.ts:3:1 error[may-import]: feature may not import another instance of feature, through "../account": feature may import only shared, and itself in the same instance --> src/features/checkout/index.ts:4:29 | 4 | import { currentUser } from '../account'; | ^^^^^^^^^^^^ feature may import only shared, and itself in the same instance | = help: remove the import, or move the code to a file whose row permits the import = note: rule: app.feature = note: because: A feature changes alone. Code that two features need goes in shared. checked 5 files and 4 imports: 2 problems app.feature: 1 permitted by exceptions, 0 by the baseline For what a code means, run `archangel docs exception-without-reason` or `archangel docs may-import`. ``` The first exception permits its import. The second exception has no reason, so it permits nothing. Archangel shows `exception-without-reason`, and it also shows the import that the exception did not permit. 3. Give the second exception a reason: src/features/checkout/index.ts ```ts // archangel-allow app.feature: checkout shows the cart total until the total moves to shared import { cartTotal } from '../cart'; // archangel-allow app.feature: the account feature has no shared session yet import { currentUser } from '../account'; export function checkoutSummary(prices: number[]): string { return `${currentUser()}: ${cartTotal(prices)}`; } ``` 4. Check again: Terminal ```ansi $ npx archangel check checked 5 files and 4 imports: no problems app.feature: 2 permitted by exceptions, 0 by the baseline ``` The summary counts the two imports that exceptions permit. An exception permits one import to break one rule. Archangel still checks the import against each other rule. To permit one import for two rules, write two comments on the lines directly above it. A blank line between a comment and the import breaks the link. ## Archangel checks each exception An exception must stay correct as the code changes. **The rule name must exist.** A typing error in the name is `exception-unknown-rule`, and the error lists the names of the rules: src/features/account/index.ts ```ts // archangel-allow app.featur: the profile reads the cart size import { cartTotal } from '../cart'; export function currentUser(): string { return `Sam (${cartTotal([])})`; } ``` Terminal ```ansi $ npx archangel check error[exception-unknown-rule]: the exception names "app.featur", which is not a rule; the rules are app.feature, app.shared, app.every-file-has-a-kind --> src/features/account/index.ts:1:1 error[may-import]: feature may not import another instance of feature, through "../cart": feature may import only shared, and itself in the same instance --> src/features/account/index.ts:2:27 | 2 | import { cartTotal } from '../cart'; | ^^^^^^^^^ feature may import only shared, and itself in the same instance | = help: remove the import, or move the code to a file whose row permits the import = note: rule: app.feature = note: because: A feature changes alone. Code that two features need goes in shared. checked 5 files and 5 imports: 2 problems app.feature: 2 permitted by exceptions, 0 by the baseline For what a code means, run `archangel docs exception-unknown-rule` or `archangel docs may-import`. ``` **An exception must permit something.** When the import below it no longer breaks the rule, the exception is `exception-unused`. So an exception goes away when the code is fixed: src/features/cart/index.ts ```ts // archangel-allow app.feature: the cart reads the user import { formatMoney } from '../../shared/money'; export function cartTotal(prices: number[]): string { return formatMoney(prices.reduce((sum, price) => sum + price, 0)); } ``` Terminal ```ansi $ npx archangel check error[exception-unused]: the exception for "app.feature" permits nothing, because the next line does not break that rule; delete it --> src/features/cart/index.ts:1:1 checked 5 files and 4 imports: 1 problem app.feature: 2 permitted by exceptions, 0 by the baseline For what a code means, run `archangel docs exception-unused`. ``` ## Use the baseline for old problems When you start a rule on code that already breaks it, run `archangel baseline add`. It writes each current problem to `archangel.baseline.json`. A check then fails only for new problems. * An entry records the rule, the importing file, the import specifier and a count. It does not record a line, so an edit above the import does not break the match. * A check never writes the baseline. When the code fixes a problem, its entry is a `baseline-stale` problem. `archangel baseline prune` lowers or removes the entry. It never adds one. So the baseline can only get smaller. * An entry for a rule that the config does not have is also `baseline-stale`. A baseline that is not valid JSON stops the check with `baseline-invalid`. [Add archangel to an existing repository](/archangel/guides/existing-repository/) shows the full cycle, from `baseline add` to `baseline prune`, with the output of each step. A problem without an import An exception is a comment above an import. So it cannot permit a problem that has no import, for example a file name or a file with no row in a table. Use the baseline for these problems. ## Which one to use | You have | Use | | --------------------------------------------------------------------- | ---------------------------------------------- | | One import that breaks a rule on purpose, with a reason worth reading | An exception | | Many problems from before the rule existed | The baseline | | A rule that is wrong for part of the code | Change the rule or the parts, not an exception | | Files that archangel must not check, such as generated code | The `ignore` key of the config | | A rule that you do not want, from a preset | The `off` key of the config | `ignore` takes lines in `.gitignore` syntax. A file that `ignore` removes is not in the check. ## Next steps * [Rule options](/archangel/reference/rule-options/). * [Config file](/archangel/reference/config/) lists `ignore` and `off`.