Skip to content

check-boundaries generic RULES do not fail closed on computed dynamic imports #642

Description

@BorisTyshkevich

Status

The original backtick/no-substitution-template bypass reported here was fixed in #646:

import(`@altinity/clickhouse-http`)

is now recognized, and the package-boundary rules that needed stronger syntax handling were moved to the real TypeScript-parser-backed helpers during #646/#653.

The remaining problem is narrower: the generic regex-backed RULES in build/check-boundaries.mjs still do not fail closed on dynamic imports whose argument is not a statically provable string literal.

Problem

extractSpecifiers() only returns module specifiers it can recognize textually. That is sufficient for ordinary static imports/exports and for quoted or no-substitution-template dynamic imports, but it silently skips computed dynamic imports such as:

import(specifier)
import(`../${name}.js`)
import(prefix + '/module.js')

For a source tree guarded only by the generic RULES loop, a computed dynamic import therefore receives no boundary decision at all. The check does not prove that the import is allowed; it simply fails to see a specifier.

This is no longer the broad package-boundary hole described in the original filing: later #630 phases moved Rule C/Rule D and the final package guards to the real parser. The remaining scope is the generic internal-layer rules that still intentionally use extractSpecifiers().

Why it matters

These guards are intended to enforce source-level dependency direction. A rule that silently ignores an import form it cannot classify is fail-open: a future dynamic import can cross a forbidden layer without check:arch noticing.

There is no known production use of computed dynamic imports under the currently guarded trees, so this is architecture-hardening rather than a live product regression.

Intended fix

For directories covered by the generic RULES, make dynamic-import handling fail closed:

  • allow a dynamic import only when its argument is statically provable as a string-like module specifier supported by the checker;
  • continue to accept single-quoted, double-quoted, and no-substitution-template literals;
  • reject computed/non-static import(...) expressions in guarded source instead of silently skipping them;
  • preserve the existing fast path for ordinary static import/export forms unless moving the generic rules to the shared real-parser helper is simpler and measurably acceptable.

The diagnostic should identify the file and explain that the dynamic import cannot be statically checked against the architecture boundary.

Tests

Add sabotage coverage for at least:

  • quoted dynamic import crossing a forbidden boundary;
  • no-substitution-template dynamic import crossing a forbidden boundary;
  • computed template dynamic import, e.g. import(`../${name}.js`);
  • identifier argument, e.g. import(specifier);
  • concatenated argument, e.g. import('../' + name);
  • existing export * from, named re-export, and namespace re-export coverage remains intact.

The production checker and its policy mirror should prove the same fail-closed behavior.

Acceptance criteria

  • A guarded source file cannot use a computed/non-static dynamic import without check:arch failing.
  • Single-quoted, double-quoted, and no-substitution-template dynamic imports remain analyzable.
  • A statically analyzable dynamic import crossing a forbidden RULES boundary is rejected.
  • Existing legal imports remain accepted.
  • Sabotage tests cover computed-template, identifier, and concatenated dynamic-import arguments.
  • The change does not reintroduce a second unsound hand-rolled parser where the shared TypeScript-parser helper is the more appropriate mechanism.

History

Originally filed during review of #641 after finding that the then-current dynamic-import regex matched only single- and double-quoted arguments. That exact bypass was fixed in #646. This issue now tracks only the remaining fail-open behavior for non-static dynamic imports in the generic regex-backed architecture rules.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions