diff --git a/.changeset/oxlint-ignorer-config-debut.md b/.changeset/oxlint-ignorer-config-debut.md new file mode 100644 index 000000000..eb91da36e --- /dev/null +++ b/.changeset/oxlint-ignorer-config-debut.md @@ -0,0 +1,11 @@ +--- +"@systemfsoftware/oxlint-ignorer-config": minor +--- + +First release. This is the preset an ignorer package is graded by: it holds a +decision to one path, refuses type assertions and `any` on data read from +outside, and rejects any import that would put the StrykerJS runtime behind an +ignorer — Effect and the family presets included. + +It extends nothing, so adopting it is a single `extends: [preset]` entry in +your lint configuration. diff --git a/.changeset/stryker-ignorer-effect-schema-declarations-debut.md b/.changeset/stryker-ignorer-effect-schema-declarations-debut.md new file mode 100644 index 000000000..ce41a84bb --- /dev/null +++ b/.changeset/stryker-ignorer-effect-schema-declarations-debut.md @@ -0,0 +1,10 @@ +--- +"@systemfsoftware/stryker-ignorer-effect-schema-declarations": minor +--- + +First release. This ignorer keeps mutants inside Effect Schema declarations out of +a mutation run: a symbol, a tagged error, an annotation or a class body only +declares a type, so no run can observe the change. + +It has no runtime dependencies, and migrating from the plugin you use today is a +rename in your plugins list — the ignorer keeps the name it already had. diff --git a/.changeset/stryker-ignorer-in-source-vitest-block-debut.md b/.changeset/stryker-ignorer-in-source-vitest-block-debut.md new file mode 100644 index 000000000..d5ee5581a --- /dev/null +++ b/.changeset/stryker-ignorer-in-source-vitest-block-debut.md @@ -0,0 +1,10 @@ +--- +"@systemfsoftware/stryker-ignorer-in-source-vitest-block": minor +--- + +First release. This ignorer keeps mutants inside a test-only block out of a +mutation run: those statements never reach the production path, so no run can +observe the change. + +It has no runtime dependencies, and migrating from the plugin you use today is a +rename in your plugins list — the ignorer keeps the name it already had. diff --git a/.changeset/stryker-ignorer-interface-ancestors.md b/.changeset/stryker-ignorer-interface-ancestors.md new file mode 100644 index 000000000..740987bff --- /dev/null +++ b/.changeset/stryker-ignorer-interface-ancestors.md @@ -0,0 +1,9 @@ +--- +"@systemfsoftware/stryker-ignorer-interface": minor +--- + +An ignorer's path is now `{ node, ancestors }`: the ancestors arrive as data, nearest first, so a +`shouldIgnore` implementation no longer walks a parent chain itself. The parent-path member is gone. + +The AST vocabulary is now re-exported from `@oxc-project/types` under that package's own names and +bundled into this package's declarations, so nothing extra needs installing. diff --git a/.changeset/stryker-ignorer-interface-debut.md b/.changeset/stryker-ignorer-interface-debut.md new file mode 100644 index 000000000..aa200dfb0 --- /dev/null +++ b/.changeset/stryker-ignorer-interface-debut.md @@ -0,0 +1,11 @@ +--- +"@systemfsoftware/stryker-ignorer-interface": minor +--- + +First release. This is the contract an ignorer is written against: the +`PlainIgnorer` descriptor (`{ name, shouldIgnore }`), the structural `NodePath` +an entry is called with, `UnknownNode`, `AstNodeType`, and the canonical AST +node vocabulary, aliased from the parser that produces those nodes. + +The package ships no runtime value and no runtime dependencies, so adopting it +costs nothing beyond the types. diff --git a/.changeset/stryker-js-engine-plain-ignorer-loader.md b/.changeset/stryker-js-engine-plain-ignorer-loader.md new file mode 100644 index 000000000..0fbc61d48 --- /dev/null +++ b/.changeset/stryker-js-engine-plain-ignorer-loader.md @@ -0,0 +1,5 @@ +--- +"@systemfsoftware/stryker-js-engine": minor +--- + +the plugin loader accepts modules exporting `strykerIgnorers` (`{ name, shouldIgnore }`, each entry registered as an `Ignore` contribution) — no `effect` peer, no plugin-interface import needed for an ignorer module; a plain entry whose `name` is not a string or whose `shouldIgnore` is not callable is rejected at load with `PluginLoadFailedError` naming the module; native `strykerPlugins` modules load unchanged, and the default `plugins:` preset now names `@systemfsoftware/stryker-ignorer-effect-schema-declarations` and `@systemfsoftware/stryker-ignorer-workflow-make-boundary` instead of the `@systemfsoftware/stryker-plugins` subpaths, so configs using the old names must move to the new packages diff --git a/.changeset/stryker-plugins-home-migration.md b/.changeset/stryker-plugins-home-migration.md deleted file mode 100644 index a12c1b838..000000000 --- a/.changeset/stryker-plugins-home-migration.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@systemfsoftware/stryker-plugins": patch ---- - -the types condition of every export subpath now points at the emitted dist/index.d.ts (the api-extractor rollup filename is gone), the effect peer range widens to ^4.0.0-rc.112, and repository metadata now points at stryker-js-effect diff --git a/.gitattributes b/.gitattributes index d65f86214..b311c290b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -13,3 +13,11 @@ *.dll binary *.so binary *.dylib binary + +# API Extractor writes its reports with CRLF. Keep the bytes it produces: a +# checkout that normalises them to LF makes `api:check` see a spurious diff. +**/etc/*.api.md -text + +# The preset's rolled-up declaration is written by API Extractor with CRLF, and +# it is committed (unlike the other packages' dist declarations). Keep its bytes. +packages/toolchain/oxlint-ignorer-config/lib/base.d.ts -text diff --git a/.gitignore b/.gitignore index cc57584af..1cb2d49c7 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ coverage/ .direnv/ result result-* +temp/ diff --git a/README.md b/README.md index 4165c5235..d19c76e5a 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ Code coverage only tells you which lines ran during tests; it cannot tell you if | **Interrupted Runs** | `Ctrl+C` / cancel yields 0 reports | Emits partial report up to last completed mutant | | **Incremental State** | `stryker-incremental.json` frequently goes stale | Reliable state validation surviving aborted runs | | **Agent / CI Mode** | Generic CLI exit codes (`0` or `1`) | Machine mode auto-detection (`AGENT`, `CLAUDECODE`, `CODEX_SANDBOX`) + classified exit codes | -| **Effect-TS Ecosystem** | Equivalent mutant noise on Schema & Brand types | Dedicated `@systemfsoftware/stryker-plugins` ignorers | +| **Effect-TS Ecosystem** | Equivalent mutant noise on Schema & Brand types | Dedicated `@systemfsoftware/stryker-ignorer-*` ignorers | | **Runtime Engine** | Procedural JavaScript with mutable state | Pure functional Effect 4 architecture with typed errors | --- @@ -131,18 +131,18 @@ pnpm exec stryker run This monorepo publishes a modular ecosystem of packages under the `@systemfsoftware` scope: -| Package | Role | -| ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------ | -| [`@systemfsoftware/stryker-js-cli`](packages/stryker-js-cli) | Terminal & CI runner binary with NDJSON streaming output | -| [`@systemfsoftware/stryker-js-language`](packages/stryker-js-language) | Pure domain core: mutant models, schemas, and exit classifications | -| [`@systemfsoftware/stryker-js-plugin-interface`](packages/stryker-js-plugin-interface) | Shared plugin contracts (`declarePlugin`, `composePlugins`) | -| [`@systemfsoftware/stryker-js-engine`](packages/stryker-js-engine) | Mutation run lifecycle engine and test orchestration | -| [`@systemfsoftware/stryker-js-instrumenter`](packages/stryker-js-instrumenter) | AST mutation engine powered by OXC parser | -| [`@systemfsoftware/stryker-js-vitest-runner`](packages/stryker-js-vitest-runner) | Vitest runner integration for mutation sandboxes | -| [`@systemfsoftware/stryker-js-typescript-checker`](packages/stryker-js-typescript-checker) | TypeScript type-checker plugin validating mutants pre-execution | -| [`@systemfsoftware/stryker-js-html-reporter`](packages/stryker-js-html-reporter) | Interactive HTML mutation report generator | -| [`@systemfsoftware/stryker-plugins`](packages/stryker-plugins) | Domain ignorers for Effect Schema brands & tagged unions | -| [`@systemfsoftware/stryker-test-contribution`](packages/stryker-test-contribution) | Suite hygiene plugin enforcing unique mutant kills per test file | +| Package | Role | +| ------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------- | +| [`@systemfsoftware/stryker-js-cli`](packages/stryker-js-cli) | Terminal & CI runner binary with NDJSON streaming output | +| [`@systemfsoftware/stryker-js-language`](packages/stryker-js-language) | Pure domain core: mutant models, schemas, and exit classifications | +| [`@systemfsoftware/stryker-js-plugin-interface`](packages/stryker-js-plugin-interface) | Shared plugin contracts (`declarePlugin`, `composePlugins`) | +| [`@systemfsoftware/stryker-js-engine`](packages/stryker-js-engine) | Mutation run lifecycle engine and test orchestration | +| [`@systemfsoftware/stryker-js-instrumenter`](packages/stryker-js-instrumenter) | AST mutation engine powered by OXC parser | +| [`@systemfsoftware/stryker-js-vitest-runner`](packages/stryker-js-vitest-runner) | Vitest runner integration for mutation sandboxes | +| [`@systemfsoftware/stryker-js-typescript-checker`](packages/stryker-js-typescript-checker) | TypeScript type-checker plugin validating mutants pre-execution | +| [`@systemfsoftware/stryker-js-html-reporter`](packages/stryker-js-html-reporter) | Interactive HTML mutation report generator | +| [`@systemfsoftware/stryker-ignorer-*`](packages/ignorers) | Decoupled domain ignorers (Schema declarations, vitest blocks, Workflow.make boundaries) | +| [`@systemfsoftware/stryker-test-contribution`](packages/stryker-test-contribution) | Suite hygiene plugin enforcing unique mutant kills per test file | --- @@ -158,7 +158,7 @@ This monorepo publishes a modular ecosystem of packages under the `@systemfsoftw
Why do mutants survive on my Effect Schema definitions? -Schema definitions and branded type markers often produce equivalent mutants that cannot be observed or failed at runtime. Install `@systemfsoftware/stryker-plugins` and enable `effect-schema-ignorer` in your `stryker.config.json` to filter them out automatically. +Schema definitions and branded type markers often produce equivalent mutants that cannot be observed or failed at runtime. Install `@systemfsoftware/stryker-ignorer-effect-schema-declarations` and enable `effect-schema-declarations` in your `stryker.config.json` to filter them out automatically.
diff --git a/docs/plans/2026-09-14-0520-refactor-ignorer-plain-entry-plan.md b/docs/plans/2026-09-14-0520-refactor-ignorer-plain-entry-plan.md new file mode 100644 index 000000000..7d9f62951 --- /dev/null +++ b/docs/plans/2026-09-14-0520-refactor-ignorer-plain-entry-plan.md @@ -0,0 +1,60 @@ +--- +title: Ignorer Plain Entry - Corrected Contract +type: refactor +date: 2026-09-14 +artifact_contract: ce-unified-plan/v1 +artifact_readiness: implementation-ready +execution: code +supersedes: docs/plans/2026-09-14-0150-refactor-ignorer-standard-schema-plan.md +--- + +# Ignorer Plain Entry - Corrected Contract + +## Goal Capsule + +**Objective.** A plain ignorer module exports `strykerIgnorers: PlainIgnorer[]` where +`PlainIgnorer = { name, shouldIgnore(path: NodePath): string | undefined }`. The host +registers each entry as an `Ignore` contribution and fails the load by name when an entry's +`name` is not a string or its `shouldIgnore` is not callable. + +**Correction.** The earlier plan required a `schema` member on every entry and had the engine +re-declare that member's shape in `effect/Schema`. Both are removed at the user's direction: +the descriptor carries no schema, and no shape is mirrored across the boundary. + +**Authority.** User direction in session (2026-09-14), then `AGENTS.md` and `CONSTITUTION.md`. + +**Verification.** `pnpm check:ci`; each package's `pnpm typecheck`, `pnpm run build`, +`pnpm exec vitest run`, `pnpm run lint`; `attw` per package; engine loader suite proves the +rejection path. + +## Requirements + +- R1. `PlainIgnorer` is `{ name, shouldIgnore }`; the interface publishes no descriptor schema. +- R2. The engine validates exactly that shape (`S.Struct({ name: S.String, shouldIgnore: S.declare(isShouldIgnore) })`) + and registers entries as `Ignore` contributions. +- R3. Standard Schema types come from `@standard-schema/spec` (jsr, catalog-pinned), imported + as a devDependency and inlined into the published declarations — no runtime dependency, no + hand-copied spec source. +- R4. The interface keeps the AST vocabulary, `NodePath`, `ancestorsOf`, and the toolkit as the + ignorers' internal implementation. +- R5. Docs, changesets, and the carried case tables state this contract. + +## Unit Map + +| Unit | Change | Verification | +| ---- | ----------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | +| C1 | Interface: drop `PlainIgnorerSchema` and the `schema` field; import spec types from the package | interface typecheck/build/tests/lint; `dist/*.d.ts` free of `standard-schema/spec` | +| C2 | Two ignorers: entries are `{ name, shouldIgnore }` | each package's suite (48/14 cases) | +| C3 | Engine: `PlainIgnorerSchema` = name + callable `shouldIgnore`; fixture set re-aimed | engine suite; the invalid-entry fixture fails the load by name | +| C4 | Docs/changesets | prose states the two-field contract | + +## Destructive Review + +**Assumptions.** (1) A host can meaningfully validate an ignorer without a declared schema; +(2) the interface's AST types suffice for authoring; (3) the toolkit remains worth shipping +internally. **Lens (inversion).** How would this be gamed? An entry could ignore nothing and +load green — accepted: ignoring nothing is visible as an unmutated run, and the case tables +pin each ignorer's real decisions. **Radical alternative.** Have the engine pass its own +Effect schema converted with `Schema.toStandardSchemaV1`. Rejected: it makes the host dictate +the input contract and re-couples authors to the host's toolkit. **Reconciliation.** No delta; +the corrected contract stands. diff --git a/docs/solutions/tooling-decisions/plain-entry-contract-without-a-declared-schema.md b/docs/solutions/tooling-decisions/plain-entry-contract-without-a-declared-schema.md new file mode 100644 index 000000000..af42e318a --- /dev/null +++ b/docs/solutions/tooling-decisions/plain-entry-contract-without-a-declared-schema.md @@ -0,0 +1,67 @@ +--- +title: A boundary contract needs the function it calls, not a schema it cannot use +date: 2026-09-14 +category: tooling-decisions +module: ignorer-family +problem_type: tooling_decision +component: tooling +severity: medium +applies_when: + - "Defining the descriptor a host loads from a plugin module" + - "Tempted to re-declare a foreign spec's types so a package can stay dependency-free" + - "Debating whether a loaded entry should carry a validator the host already calls" +tags: [standard-schema, boundary, plugin-contract, inlining, types] +--- + +# A boundary contract needs the function it calls, not a schema it cannot use + +## Context + +The plain-ignorer family had the host decode every loaded entry against a `schema` member the +ignorer declared, described as a Standard Schema validator (KTD1/KTD5 of the superseded plan). +Two costs surfaced in review: + +- the host re-declared the member's shape in its own toolkit (`effect/Schema`), so the same + contract lived twice and could drift; +- the spec's types were hand-copied into the interface package, which then had to stay in step + with `@standard-schema/spec` by hand. + +The host never used that validator for anything the decision function did not already do: a +loaded entry is registered as an `Ignore` contribution and `shouldIgnore(path)` is called. + +## Guidance + +Ship the smallest contract the host actually consumes — `{ name, shouldIgnore }` — and let the +package that owns the AST supply its types: + +```ts +export interface PlainIgnorer { + readonly name: string + shouldIgnore(path: NodePath): string | undefined +} +``` + +The host then owns its own load check and mirrors nothing: + +```ts +export const PlainIgnorerSchema = S.Struct({ + name: S.String, + shouldIgnore: S.declare(isShouldIgnore), +}) +``` + +Take the spec's types from the spec package, never from a hand copy, and inline them so the +published declarations stay dependency-free: `@standard-schema/spec` (jsr, catalog-pinned) as a +devDependency, resolved into the package's emitted declarations by the build (build output, not +tracked). Verified after `pnpm run build` in `packages/ignorers/interface` by reading its emitted +declarations — no `@standard-schema/spec` reference survives there. + +The corollary for a host that wants a validator: convert the host's own schema where it needs +one (`Schema.toStandardSchemaV1`), rather than pushing a validator requirement onto the plugin +author. + +## Applicability + +Applies to any host/plugin boundary where the host validates an entry and then calls a function +on it. Where the host genuinely decides on the declared validator (filtering, generating, or +composing from it), keep the member — and keep it sourced from the spec package, inlined. diff --git a/dprint.json b/dprint.json index edd9b321a..c99b20785 100644 --- a/dprint.json +++ b/dprint.json @@ -16,10 +16,12 @@ "**/dist", "**/*-lock.json", "pnpm-lock.yaml", + "**/etc/*.api.md", "coverage/**", "**/snapshots/*.md", "repos/**", - "packages/stryker-js-typescript-checker/testResources/**" + "packages/stryker-js-typescript-checker/testResources/**", + "packages/toolchain/oxlint-ignorer-config/lib/base.d.ts" ], "plugins": [ "https://plugins.dprint.dev/typescript-0.96.1.wasm", diff --git a/package.json b/package.json index 553533482..259f42562 100644 --- a/package.json +++ b/package.json @@ -23,8 +23,8 @@ "clean": "turbo clean", "format": "./bin/dprint fmt", "format:check": "./bin/dprint check", - "gate:dist": "turbo --concurrency=${TURBO_CONCURRENCY:-100%} build", - "gate:tasks": "turbo --concurrency=${TURBO_CONCURRENCY:-50%} --continue lint typecheck typecheck:node test", + "gate:dist": "turbo --concurrency=${TURBO_CONCURRENCY:-100%} build ${TURBO_BASE:+--filter=...[$TURBO_BASE]}", + "gate:tasks": "turbo --concurrency=${TURBO_CONCURRENCY:-50%} --continue lint typecheck test ${TURBO_BASE:+--filter=...[$TURBO_BASE]}", "lint": "turbo --concurrency=${TURBO_CONCURRENCY:-50%} lint", "precommit": "pnpm exec lint-staged --no-revert --config .lintstagedrc.js", "prepare": "husky", diff --git a/packages/stryker-plugins/.attw.json b/packages/ignorers/effect-schema-declarations/.attw.json similarity index 100% rename from packages/stryker-plugins/.attw.json rename to packages/ignorers/effect-schema-declarations/.attw.json diff --git a/packages/ignorers/effect-schema-declarations/.gitignore b/packages/ignorers/effect-schema-declarations/.gitignore new file mode 100644 index 000000000..09c81c214 --- /dev/null +++ b/packages/ignorers/effect-schema-declarations/.gitignore @@ -0,0 +1,4 @@ +dist +temp +coverage +reports diff --git a/packages/ignorers/effect-schema-declarations/AGENTS.md b/packages/ignorers/effect-schema-declarations/AGENTS.md new file mode 100644 index 000000000..1024ced4c --- /dev/null +++ b/packages/ignorers/effect-schema-declarations/AGENTS.md @@ -0,0 +1,32 @@ +# AGENTS.md — `@systemfsoftware/stryker-ignorer-effect-schema-declarations` + +Stryker ignorer for Effect Schema declarations: removes proven-equivalent mutants on brands, `TaggedClass`/`TaggedError` tags and field schemas, `optionalWith` defaults, and documentation annotations. The AST guards are this package's own predicates over the node shapes, whose types come from `@systemfsoftware/stryker-ignorer-interface` — the package's one dependency, and it ships types only; no Effect is imported, bundled, or depended on. Root `AGENTS.md` governs. + +## Rules + +| ID | Rule | Gate | +| ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | +| **SP1** | An ignored mutant is proven-equivalent: mutating the tag/brand field produces identical behavior. | `pnpm --filter @systemfsoftware/stryker-ignorer-effect-schema-declarations test` | +| **SP2** | Every new ignore pattern arrives with a test demonstrating the equivalent mutant. | `pnpm --filter @systemfsoftware/stryker-ignorer-effect-schema-declarations test` | +| **SP3** | Register through the plain ignorer protocol only; never bypass other mutation stages. | `review` | +| **SP4** | No `effect` or `@effect/*` import in any source or built file, and no `effect` or `@effect/*` key in any dependency block — the preset's `no-restricted-imports` ban is the import gate. | `pnpm --filter @systemfsoftware/stryker-ignorer-effect-schema-declarations lint` | +| **SP5** | Every decision is pinned by a case table — a committed test carries no FastCheck generator. | `pnpm --filter @systemfsoftware/stryker-ignorer-effect-schema-declarations lint` | +| **SP6** | A committed test asserts intended behavior, never a stored-output snapshot. | `review` | + +SP3's reviewer decides one thing: whether the module reaches mutation through the plain ignorer protocol alone. + +- `wrong:` exporting a StrykerJS plugin object, a reporter, or a mutator. +- `right:` exporting `strykerIgnorers`, the protocol the loader decodes. + +SP6's reviewer decides one thing: whether every expected value is an oracle the decision under test did not produce. + +- `wrong:` `expect(shouldIgnore(path)).toMatchSnapshot()` — the snapshot re-records whatever the rule currently answers. +- `right:` `expect(shouldIgnore(path)).toBe(SYMBOL_DESCRIPTION_IGNORED)` — the exported reason constant, written from the rule's intent. + +## Verification + +```bash +pnpm --filter @systemfsoftware/stryker-ignorer-effect-schema-declarations typecheck +pnpm --filter @systemfsoftware/stryker-ignorer-effect-schema-declarations test +pnpm --filter @systemfsoftware/stryker-ignorer-effect-schema-declarations lint +``` diff --git a/packages/stryker-plugins/LICENSE b/packages/ignorers/effect-schema-declarations/LICENSE similarity index 100% rename from packages/stryker-plugins/LICENSE rename to packages/ignorers/effect-schema-declarations/LICENSE diff --git a/packages/stryker-plugins/README.md b/packages/ignorers/effect-schema-declarations/README.md similarity index 55% rename from packages/stryker-plugins/README.md rename to packages/ignorers/effect-schema-declarations/README.md index 43830a6d3..5967180b4 100644 --- a/packages/stryker-plugins/README.md +++ b/packages/ignorers/effect-schema-declarations/README.md @@ -1,26 +1,31 @@ -# @systemfsoftware/stryker-plugins +# @systemfsoftware/stryker-ignorer-effect-schema-declarations -![version](https://img.shields.io/npm/v/@systemfsoftware/stryker-plugins) -![license](https://img.shields.io/npm/l/@systemfsoftware/stryker-plugins) +![version](https://img.shields.io/npm/v/@systemfsoftware/stryker-ignorer-effect-schema-declarations) +![license](https://img.shields.io/npm/l/@systemfsoftware/stryker-ignorer-effect-schema-declarations) > Stop Effect `Schema` declarations from dragging your Stryker score below 100%. -A brand description, a `_tag`, a `title` — mutate any of them and the source changes but the behaviour does not, so no test can ever kill the mutant. Those unkillable mutants sit in your report forever, indistinguishable from real coverage gaps. This [Stryker](https://stryker-mutator.io) Ignore plugin removes them, so the score that remains is behaviour. +A brand description, a `_tag`, a `title` — mutate any of them and the source changes but the behaviour does not, so no test can ever kill the mutant. Those unkillable mutants sit in your report forever, indistinguishable from real coverage gaps. This [Stryker](https://stryker-mutator.io) ignorer removes them, so the score that remains is behaviour. ## Install ```bash -pnpm add -D @systemfsoftware/stryker-plugins +pnpm add -D @systemfsoftware/stryker-ignorer-effect-schema-declarations ``` -In `stryker.config.json`: +In `stryker.config.json`, the plugin module and the ignorer name travel as a pair: + +| Config key | Value | +| ---------- | ------------------------------------------------------------- | +| `plugins` | `@systemfsoftware/stryker-ignorer-effect-schema-declarations` | +| `ignorers` | `effect-schema-declarations` | ```jsonc { "plugins": [ "@systemfsoftware/stryker-js-vitest-runner", "@systemfsoftware/stryker-js-typescript-checker", - "@systemfsoftware/stryker-plugins" + "@systemfsoftware/stryker-ignorer-effect-schema-declarations" ], "ignorers": ["effect-schema-declarations"] } @@ -28,8 +33,18 @@ In `stryker.config.json`: Mutants it recognizes are reported as `Ignored`, each carrying the reason it was safe to skip. +> [!WARNING] +> A name in `ignorers` that no loaded plugin answers is silently skipped — the run proceeds and the mutants stay in your score. Copy the name from the table, not from memory. + > [!NOTE] -> `@systemfsoftware/stryker-js-plugin-api` is a peer dependency — your Stryker install provides it. `effect` is a direct dependency (the plugin decodes AST nodes with `Schema`). +> Requires `@systemfsoftware/stryker-js-engine` 4.1.0 or later (the first version whose plugin loader speaks the plain ignorer protocol). The package has no peer dependencies, exactly one dependency — `@systemfsoftware/stryker-ignorer-interface`, which supplies AST node types only — and no Effect anywhere, not in `dependencies`, not in `devDependencies`, and not on its published surface. The guards are this package's own predicates over those node shapes. + +## Migrating from `@systemfsoftware/stryker-plugins` + +| Before | After | +| ------------------------------------------------- | ---------------------------------------------------------------------------- | +| `"plugins": ["@systemfsoftware/stryker-plugins"]` | `"plugins": ["@systemfsoftware/stryker-ignorer-effect-schema-declarations"]` | +| `"ignorers": ["effect-schema-declarations"]` | unchanged | ## What it ignores diff --git a/packages/ignorers/effect-schema-declarations/api-extractor.json b/packages/ignorers/effect-schema-declarations/api-extractor.json new file mode 100644 index 000000000..c2811a0b5 --- /dev/null +++ b/packages/ignorers/effect-schema-declarations/api-extractor.json @@ -0,0 +1,55 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "projectFolder": ".", + "mainEntryPointFilePath": "/dist/index.d.ts", + "bundledPackages": [], + "compiler": { + "tsconfigFilePath": "/tsconfig.json" + }, + "apiReport": { + "enabled": true, + "reportFolder": "/etc/", + "reportTempFolder": "/temp/main/", + "reportFileName": "stryker-ignorer-effect-schema-declarations.api.md" + }, + "docModel": { + "enabled": false + }, + "dtsRollup": { + "enabled": true, + "untrimmedFilePath": "/dist/index.d.ts" + }, + "tsdocMetadata": { + "enabled": false + }, + "messages": { + "extractorMessageReporting": { + "default": { + "logLevel": "warning" + }, + "ae-forgotten-export": { + "logLevel": "error" + }, + "ae-missing-release-tag": { + "logLevel": "none" + }, + "ae-internal-missing-underscore": { + "logLevel": "none" + } + }, + "tsdocMessageReporting": { + "default": { + "logLevel": "warning" + }, + "tsdoc-undefined-tag": { + "logLevel": "none" + }, + "tsdoc-unsupported-tag": { + "logLevel": "none" + }, + "tsdoc-param-tag-missing-hyphen": { + "logLevel": "none" + } + } + } +} diff --git a/packages/ignorers/effect-schema-declarations/etc/stryker-ignorer-effect-schema-declarations.api.md b/packages/ignorers/effect-schema-declarations/etc/stryker-ignorer-effect-schema-declarations.api.md new file mode 100644 index 000000000..ef601e671 --- /dev/null +++ b/packages/ignorers/effect-schema-declarations/etc/stryker-ignorer-effect-schema-declarations.api.md @@ -0,0 +1,50 @@ +## API Report File for "@systemfsoftware/stryker-ignorer-effect-schema-declarations" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { NodePath } from '@systemfsoftware/stryker-ignorer-interface'; + +// @public (undocumented) +export const ANNOTATION_OBJECT_IGNORED: 'annotations object holding only documentation is a declaration, not behaviour'; + +// @public (undocumented) +export const ANNOTATION_TEXT_IGNORED: 'annotation documentation value is declaration data, not behaviour'; + +// @public (undocumented) +export const BRAND_NAME_IGNORED: 'Schema.brand name is identity-only data, not behaviour'; + +// @public (undocumented) +export const CLASS_FIELDS_IGNORED: 'Schema.Class field schema is a declaration, not behaviour'; + +// @public (undocumented) +export const CLASS_ID_IGNORED: 'Schema.Class identifier is a declaration name, not behaviour'; + +// @public (undocumented) +export const decideSchemaDeclarationIgnore: (node: unknown, parent: unknown, grandparent?: unknown, ancestor?: unknown) => string | undefined; + +// @public (undocumented) +export const firstIgnoreReason: (path: NodePath) => string | undefined; + +// @public (undocumented) +export const OPTIONAL_DEFAULT_IGNORED: 'optionalWith default value is config, not behaviour'; + +// @public (undocumented) +export const strykerIgnorers: { + name: string; + shouldIgnore: typeof firstIgnoreReason; +}[]; + +// @public (undocumented) +export const SYMBOL_DESCRIPTION_IGNORED: 'Symbol.for() brand description is identity-only data, not behaviour'; + +// @public (undocumented) +export const TAGGED_FIELDS_IGNORED: 'TaggedClass/TaggedError field schema is a declaration, not behaviour'; + +// @public (undocumented) +export const TAGGED_TAG_IGNORED: 'TaggedClass/TaggedError _tag is a declaration discriminant, not behaviour'; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/ignorers/effect-schema-declarations/oxlint.config.ts b/packages/ignorers/effect-schema-declarations/oxlint.config.ts new file mode 100644 index 000000000..879f8a966 --- /dev/null +++ b/packages/ignorers/effect-schema-declarations/oxlint.config.ts @@ -0,0 +1,6 @@ +import preset from '@systemfsoftware/oxlint-ignorer-config' +import { defineConfig } from 'oxlint' + +export default defineConfig({ + extends: [preset], +}) diff --git a/packages/ignorers/effect-schema-declarations/package.json b/packages/ignorers/effect-schema-declarations/package.json new file mode 100644 index 000000000..584ef370a --- /dev/null +++ b/packages/ignorers/effect-schema-declarations/package.json @@ -0,0 +1,71 @@ +{ + "name": "@systemfsoftware/stryker-ignorer-effect-schema-declarations", + "license": "Apache-2.0", + "version": "0.0.0", + "author": "Ryan Lee ", + "repository": { + "type": "git", + "url": "git+https://github.com/systemfsoftware/stryker-js-effect.git", + "directory": "packages/ignorers/effect-schema-declarations" + }, + "homepage": "https://github.com/systemfsoftware/stryker-js-effect/tree/main/packages/ignorers/effect-schema-declarations#readme", + "bugs": { + "url": "https://github.com/systemfsoftware/stryker-js-effect/issues" + }, + "description": "Stryker ignorer that removes proven-equivalent mutants on Effect Schema declarations — brands, tags, field schemas, defaults, and documentation annotations.", + "keywords": [ + "stryker", + "stryker-mutator", + "mutation-testing", + "effect-schema", + "ignorer" + ], + "type": "module", + "files": [ + "dist" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.mjs" + }, + "./package.json": "./package.json" + }, + "scripts": { + "build": "tsdown && pnpm api:check", + "clean": "rimraf dist", + "typecheck": "tsc --noEmit --incremental && tsc -p tsconfig.node.json --noEmit", + "test": "vitest run", + "lint": "f=${OXLINT_FORMAT:-${AGENT:+agent}}; oxlint . --format=${f:-default}", + "attw": "attw --pack .", + "api:check": "tsdown && api-extractor run", + "api:update": "api-extractor run --local" + }, + "dependencies": { + "@systemfsoftware/stryker-ignorer-interface": "workspace:^" + }, + "devDependencies": { + "@microsoft/api-extractor": "catalog:", + "@systemfsoftware/arethetypeswrong-cli": "catalog:", + "@systemfsoftware/oxlint-ignorer-config": "workspace:^", + "@systemfsoftware/tsconfig": "catalog:", + "@systemfsoftware/vitest-config": "workspace:^", + "@types/node": "catalog:", + "oxlint": "catalog:", + "rimraf": "catalog:", + "tsdown": "catalog:", + "typescript": "catalog:", + "vitest": "catalog:" + }, + "publishConfig": { + "access": "public", + "provenance": true, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.mjs" + }, + "./package.json": "./package.json" + } + } +} diff --git a/packages/ignorers/effect-schema-declarations/src/SchemaDeclarationIgnore.ts b/packages/ignorers/effect-schema-declarations/src/SchemaDeclarationIgnore.ts new file mode 100644 index 000000000..1c14ff1ba --- /dev/null +++ b/packages/ignorers/effect-schema-declarations/src/SchemaDeclarationIgnore.ts @@ -0,0 +1,225 @@ +import type { + ArrowFunctionExpression, + CallExpression, + Expression, + IdentifierReference, + MemberExpression, + ObjectExpression, + ObjectProperty, + StringLiteral, +} from '@systemfsoftware/stryker-ignorer-interface' + +const DOCUMENTATION_KEYS: Record = { + identifier: true, + description: true, + title: true, + documentation: true, + examples: true, +} + +export type DocumentationKey = 'identifier' | 'description' | 'title' | 'documentation' | 'examples' + +interface TypedNode { + readonly type: string +} + +const isObject = (value: unknown): value is object => typeof value === 'object' && value !== null + +const hasStringType = (value: object): value is TypedNode => 'type' in value && typeof value.type === 'string' + +const isAstNode = (value: unknown): value is TypedNode => isObject(value) && hasStringType(value) + +const isNodeOfType = (value: unknown, type: string): value is TypedNode => isAstNode(value) && value.type === type + +const hasStringName = (value: TypedNode): boolean => 'name' in value && typeof value.name === 'string' + +const hasStringValue = (value: TypedNode): boolean => 'value' in value && typeof value.value === 'string' + +const isDocumentationKey = (value: unknown): value is DocumentationKey => + typeof value === 'string' && DOCUMENTATION_KEYS[value] === true + +const hasDocumentationName = (value: TypedNode): boolean => 'name' in value && isDocumentationKey(value.name) + +const hasDocumentationValue = (value: TypedNode): boolean => 'value' in value && isDocumentationKey(value.value) + +const isIdentifierKeyNode = (value: unknown): boolean => + isNodeOfType(value, 'Identifier') && hasDocumentationName(value) + +const isLiteralKeyNode = (value: unknown): boolean => isNodeOfType(value, 'Literal') && hasDocumentationValue(value) + +const isDocumentationKeyNode = (value: unknown): boolean => isIdentifierKeyNode(value) || isLiteralKeyNode(value) + +const hasObjectNode = (value: TypedNode): boolean => 'object' in value && isAstNode(value.object) + +const hasPropertyNode = (value: TypedNode): boolean => 'property' in value && isAstNode(value.property) + +const hasMemberEnds = (value: TypedNode): boolean => hasObjectNode(value) && hasPropertyNode(value) + +const isAstNodeArray = (value: unknown): value is ReadonlyArray => + Array.isArray(value) && value.every(isAstNode) + +const hasCalleeNode = (value: TypedNode): boolean => 'callee' in value && isAstNode(value.callee) + +const hasArgumentNodes = (value: TypedNode): boolean => 'arguments' in value && isAstNodeArray(value.arguments) + +const hasCallEnds = (value: TypedNode): boolean => hasCalleeNode(value) && hasArgumentNodes(value) + +const hasDocumentationKeyNode = (value: TypedNode): boolean => 'key' in value && isDocumentationKeyNode(value.key) + +const hasComputedFalse = (value: TypedNode): boolean => 'computed' in value && value.computed === false + +const hasDocumentationPropertyFields = (value: TypedNode): boolean => + hasComputedFalse(value) && hasDocumentationKeyNode(value) + +const hasDocumentationEntries = (value: ReadonlyArray): boolean => + value.length > 0 && value.every(isDocumentationProperty) + +const isDocumentationArray = (value: unknown): value is ReadonlyArray => + Array.isArray(value) && hasDocumentationEntries(value) + +const hasPropertiesField = (value: TypedNode): boolean => + 'properties' in value && isDocumentationArray(value.properties) + +export const isIdentifier = (value: unknown): value is IdentifierReference => + isNodeOfType(value, 'Identifier') && hasStringName(value) + +export const isStringLiteral = (value: unknown): value is StringLiteral => + isNodeOfType(value, 'Literal') && hasStringValue(value) + +export const isObjectExpression = (value: unknown): value is ObjectExpression => isNodeOfType(value, 'ObjectExpression') + +export const isArrowFunctionExpression = (value: unknown): value is ArrowFunctionExpression => + isNodeOfType(value, 'ArrowFunctionExpression') + +export const isMemberExpression = (value: unknown): value is MemberExpression => + isNodeOfType(value, 'MemberExpression') && hasMemberEnds(value) + +export const isCallExpression = (value: unknown): value is CallExpression => + isNodeOfType(value, 'CallExpression') && hasCallEnds(value) + +export const isDocumentationProperty = (value: unknown): value is ObjectProperty => + isNodeOfType(value, 'Property') && hasDocumentationPropertyFields(value) + +export const isDocumentationObject = (value: unknown): value is ObjectExpression => + isNodeOfType(value, 'ObjectExpression') && hasPropertiesField(value) + +export const SYMBOL_DESCRIPTION_IGNORED = 'Symbol.for() brand description is identity-only data, not behaviour' as const +export const TAGGED_TAG_IGNORED = 'TaggedClass/TaggedError _tag is a declaration discriminant, not behaviour' as const +export const TAGGED_FIELDS_IGNORED = 'TaggedClass/TaggedError field schema is a declaration, not behaviour' as const +export const CLASS_ID_IGNORED = 'Schema.Class identifier is a declaration name, not behaviour' as const +export const CLASS_FIELDS_IGNORED = 'Schema.Class field schema is a declaration, not behaviour' as const +export const BRAND_NAME_IGNORED = 'Schema.brand name is identity-only data, not behaviour' as const +export const OPTIONAL_DEFAULT_IGNORED = 'optionalWith default value is config, not behaviour' as const +export const ANNOTATION_OBJECT_IGNORED = + 'annotations object holding only documentation is a declaration, not behaviour' as const +export const ANNOTATION_TEXT_IGNORED = 'annotation documentation value is declaration data, not behaviour' as const + +const TAGGED_FACTORIES: readonly string[] = ['TaggedClass', 'TaggedError'] + +/** + * `Schema.Class` is curried the other way round from `Schema.TaggedClass`. + * + * `S.TaggedClass()('tag', fields)` puts both the discriminant and the fields on the outer + * call, so one callee predicate reaches both. `S.Class('Id')(fields)` puts the identifier on + * the *inner* call and the fields on the outer one, so the same declaration data needs two + * predicates. Missing that shape is why a class-shaped schema kept fourteen mutants a + * tag-shaped one never had. + */ +const CLASS_FACTORY = 'Class' + +const isIdentifierNamed = (node: TypedNode, name: string): boolean => isIdentifier(node) && node.name === name + +const isIdentifierIn = (node: TypedNode, names: readonly string[]): boolean => + isIdentifier(node) && names.includes(node.name) + +const isMemberNamed = (member: MemberExpression, object: string, property: string): boolean => + isIdentifierNamed(member.object, object) && isIdentifierNamed(member.property, property) + +const isNamedMember = (node: Expression, object: string, property: string): boolean => + isMemberExpression(node) && isMemberNamed(node, object, property) + +const isSymbolForCallee = (callee: Expression): boolean => isNamedMember(callee, 'Symbol', 'for') + +const isNamedFactoryReference = (reference: Expression, names: readonly string[]): boolean => + isMemberExpression(reference) && isIdentifierIn(reference.property, names) + +const isTaggedFactoryReference = (reference: Expression): boolean => + isNamedFactoryReference(reference, TAGGED_FACTORIES) + +const isClassFactoryReference = (reference: Expression): boolean => isNamedFactoryReference(reference, [CLASS_FACTORY]) + +const isBrandCallee = (callee: Expression): boolean => + isMemberExpression(callee) && isIdentifierNamed(callee.property, 'brand') + +const isTaggedFactoryCallee = (callee: Expression): boolean => + isCallExpression(callee) && isTaggedFactoryReference(callee.callee) + +const isArgumentAt = ( + node: unknown, + call: CallExpression, + index: number, + calleeMatches: (callee: Expression) => boolean, +): boolean => calleeMatches(call.callee) && call.arguments[index] === node + +const isArgumentOf = ( + node: unknown, + parent: unknown, + index: number, + calleeMatches: (callee: Expression) => boolean, +): boolean => isCallExpression(parent) && isArgumentAt(node, parent, index, calleeMatches) + +interface IgnoreRule { + readonly matches: (node: unknown, parent: unknown, grandparent: unknown, ancestor: unknown) => boolean + readonly reason: string +} + +const isOptionalWithCallee = (callee: Expression): boolean => isNamedMember(callee, 'S', 'optionalWith') + +const isAnnotationsCallee = (callee: Expression): boolean => + isMemberExpression(callee) && isIdentifierNamed(callee.property, 'annotations') + +const argumentRule = ( + is: (node: unknown) => node is TypedNode, + argumentIndex: number, + calleeMatches: (callee: Expression) => boolean, + reason: string, +): IgnoreRule => ({ + matches: (node, parent) => is(node) && isArgumentOf(node, parent, argumentIndex, calleeMatches), + reason, +}) + +/** + * A documentation-keyed entry of an `annotations` call. Unlike the object rule + * this does not care what sits beside it: `title` is documentation whether or + * not an `arbitrary` shares the object, because replacing the title cannot + * change what the schema does. Emptying the whole object could, which is why + * that rule is the stricter of the two. + */ +const isDocumentationValue = (node: unknown, parent: ObjectProperty): boolean => parent.value === node + +const isDocumentationPropertyValue = (node: unknown, parent: unknown): boolean => + isDocumentationProperty(parent) && isDocumentationValue(node, parent) + +const documentationValueRule: IgnoreRule = { + matches: (node, parent, grandparent, ancestor) => + isDocumentationPropertyValue(node, parent) && isArgumentOf(grandparent, ancestor, 0, isAnnotationsCallee), + reason: ANNOTATION_TEXT_IGNORED, +} + +const RULES: readonly IgnoreRule[] = [ + argumentRule(isStringLiteral, 0, isSymbolForCallee, SYMBOL_DESCRIPTION_IGNORED), + argumentRule(isStringLiteral, 0, isTaggedFactoryCallee, TAGGED_TAG_IGNORED), + argumentRule(isObjectExpression, 1, isTaggedFactoryCallee, TAGGED_FIELDS_IGNORED), + argumentRule(isStringLiteral, 0, isClassFactoryReference, CLASS_ID_IGNORED), + argumentRule(isStringLiteral, 0, isBrandCallee, BRAND_NAME_IGNORED), + argumentRule(isArrowFunctionExpression, 1, isOptionalWithCallee, OPTIONAL_DEFAULT_IGNORED), + argumentRule(isDocumentationObject, 0, isAnnotationsCallee, ANNOTATION_OBJECT_IGNORED), + documentationValueRule, +] + +export const decideSchemaDeclarationIgnore = ( + node: unknown, + parent: unknown, + grandparent?: unknown, + ancestor?: unknown, +): string | undefined => RULES.find((rule) => rule.matches(node, parent, grandparent, ancestor))?.reason diff --git a/packages/ignorers/effect-schema-declarations/src/mod.ts b/packages/ignorers/effect-schema-declarations/src/mod.ts new file mode 100644 index 000000000..8c78fba33 --- /dev/null +++ b/packages/ignorers/effect-schema-declarations/src/mod.ts @@ -0,0 +1,28 @@ +import type { NodePath } from '@systemfsoftware/stryker-ignorer-interface' + +import { decideSchemaDeclarationIgnore } from './SchemaDeclarationIgnore.js' + +const decisionAt = (chain: readonly unknown[], position: number): string | undefined => + decideSchemaDeclarationIgnore(chain[position], chain[position + 1], chain[position + 2], chain[position + 3]) + +const firstIgnoreReason = (path: NodePath): string | undefined => { + const chain = [path.node, ...path.ancestors] + return chain.reduce((found, _, position) => found ?? decisionAt(chain, position), undefined) +} + +export const strykerIgnorers = [ + { name: 'effect-schema-declarations', shouldIgnore: firstIgnoreReason }, +] + +export { + ANNOTATION_OBJECT_IGNORED, + ANNOTATION_TEXT_IGNORED, + BRAND_NAME_IGNORED, + CLASS_FIELDS_IGNORED, + CLASS_ID_IGNORED, + decideSchemaDeclarationIgnore, + OPTIONAL_DEFAULT_IGNORED, + SYMBOL_DESCRIPTION_IGNORED, + TAGGED_FIELDS_IGNORED, + TAGGED_TAG_IGNORED, +} from './SchemaDeclarationIgnore.js' diff --git a/packages/stryker-plugins/tests/__fixtures__/EffectSchemaAst.fixtures.ts b/packages/ignorers/effect-schema-declarations/tests/__fixtures__/EffectSchemaAst.fixtures.ts similarity index 67% rename from packages/stryker-plugins/tests/__fixtures__/EffectSchemaAst.fixtures.ts rename to packages/ignorers/effect-schema-declarations/tests/__fixtures__/EffectSchemaAst.fixtures.ts index 760d3cc5d..c1f5e56b4 100644 --- a/packages/stryker-plugins/tests/__fixtures__/EffectSchemaAst.fixtures.ts +++ b/packages/ignorers/effect-schema-declarations/tests/__fixtures__/EffectSchemaAst.fixtures.ts @@ -1,5 +1,3 @@ -import { FastCheck as fc } from 'effect/testing' - interface Identifier { readonly type: 'Identifier' readonly name: string @@ -46,27 +44,11 @@ export const taggedCall = (factory: string, tag: AstNode, fields: AstNode): Call export const bareFactoryCall = (factory: string, tag: AstNode, fields: AstNode): CallExpression => callOf(callOf(identifier(factory), []), [tag, fields]) -/** - * `Schema.Class('Id')(fields)` - the identifier rides the inner call and the fields the - * outer one, which is the opposite of `taggedCall`'s arrangement and the shape the ignorer - * missed until the class rules were added. - */ export const classCall = (id: AstNode, fields: AstNode): CallExpression => callOf(callOf(memberOf('Schema', 'Class'), [id]), [fields]) -/** `X.pipe(S.brand('Name'))`'s inner call - the brand name is identity data. */ export const brandCall = (name: AstNode): CallExpression => callOf(memberOf('S', 'brand'), [name]) -export const taggedFactory = fc.constantFrom('TaggedClass', 'TaggedError') - -export const nonTaggedFactory = fc.constantFrom('Struct', 'Class', 'Union', 'TaggedRequest', 'tag', 'Literal') - -export const nonSymbolForMember = fc.oneof( - fc.tuple(fc.constant('Symbol'), fc.constantFrom('iterator', 'keyFor', 'description')), - fc.tuple(fc.constantFrom('Reflect', 'Object', 'globalThis'), fc.constant('for')), - fc.tuple(fc.constantFrom('Reflect', 'Match', 'Effect'), fc.constantFrom('tag', 'gen', 'sync')), -) - export interface PropertyNode { readonly type: 'Property' readonly computed: boolean @@ -101,9 +83,3 @@ export const objectExpression = (properties: readonly PropertyNode[] = []): Obje }) export const annotationsCall = (argument: AstNode): CallExpression => callOf(memberOf('S', 'annotations'), [argument]) - -export const documentationKey = fc.constantFrom('identifier', 'description', 'title', 'documentation', 'examples') - -export const behaviourKey = fc.constantFrom('arbitrary', 'pretty', 'equivalence', 'message', 'jsonSchema') - -export const nonAnnotationsMethod = fc.constantFrom('filter', 'transform', 'pipe', 'brand', 'annotate') diff --git a/packages/ignorers/effect-schema-declarations/tests/effect-schema-declarations.test.ts b/packages/ignorers/effect-schema-declarations/tests/effect-schema-declarations.test.ts new file mode 100644 index 000000000..be4632f8f --- /dev/null +++ b/packages/ignorers/effect-schema-declarations/tests/effect-schema-declarations.test.ts @@ -0,0 +1,408 @@ +import { type NodePath } from '@systemfsoftware/stryker-ignorer-interface' +import { describe, expect, it } from 'vitest' + +import { + ANNOTATION_OBJECT_IGNORED, + ANNOTATION_TEXT_IGNORED, + BRAND_NAME_IGNORED, + CLASS_ID_IGNORED, + OPTIONAL_DEFAULT_IGNORED, + strykerIgnorers, + SYMBOL_DESCRIPTION_IGNORED, + TAGGED_FIELDS_IGNORED, + TAGGED_TAG_IGNORED, +} from '@systemfsoftware/stryker-ignorer-effect-schema-declarations' + +import { + annotationsCall, + bareFactoryCall, + brandCall, + callOf, + classCall, + identifier, + memberOf, + namedProperty, + objectExpression, + objectOf, + propertyOf, + stringLiteral, + symbolForCall, + taggedCall, +} from './__fixtures__/EffectSchemaAst.fixtures.js' + +const brandDescription = stringLiteral('MyBrand') +const brandDescriptionCall = symbolForCall(brandDescription) + +const keyForDescription = stringLiteral('desc') +const keyForCall = callOf(memberOf('Symbol', 'keyFor'), [keyForDescription]) + +const objectForDescription = stringLiteral('desc') +const objectForCall = callOf(memberOf('Object', 'for'), [objectForDescription]) + +const iteratorDescription = stringLiteral('desc') +const iteratorCall = callOf(memberOf('Symbol', 'iterator'), [iteratorDescription]) + +const unheldDescription = stringLiteral('MyBrand') +const otherDescription = stringLiteral('Other') +const unheldDescriptionCall = symbolForCall(otherDescription) + +const objectAtSymbolFor = objectExpression() +const objectAtSymbolForCall = symbolForCall(objectAtSymbolFor) + +const taggedTag = stringLiteral('myTag') +const taggedFields = objectExpression() +const taggedClassCall = taggedCall('TaggedClass', taggedTag, taggedFields) + +const taggedErrorTag = stringLiteral('err') +const taggedErrorFields = objectExpression() +const taggedErrorCall = taggedCall('TaggedError', taggedErrorTag, taggedErrorFields) + +const structTag = stringLiteral('tag') +const structFields = objectExpression() +const structCall = taggedCall('Struct', structTag, structFields) + +const swappedFields = objectExpression() +const swappedTag = stringLiteral('tag') +const swappedCall = taggedCall('TaggedClass', swappedFields, swappedTag) + +const bareTag = stringLiteral('someTag') +const bareFields = objectExpression() +const bareFactoryCallNode = bareFactoryCall('TaggedClass', bareTag, bareFields) + +const classId = stringLiteral('ChildPolicyConfig') +const classFields = objectExpression() +const classOuterCall = classCall(classId, classFields) +const classInnerCall = classOuterCall.callee + +const acceptedLiteralMember = stringLiteral('permanent') +const acceptedLiteralCall = callOf(memberOf('Schema', 'Literal'), [ + acceptedLiteralMember, + { type: 'Literal', value: 'transient' }, +]) + +const brandName = stringLiteral('MaxChildren') +const brandNameCall = brandCall(brandName) + +const optionalDefault = { type: 'ArrowFunctionExpression' } +const optionalSchema = memberOf('S', 'String') +const optionalWithCall = callOf(memberOf('S', 'optionalWith'), [optionalSchema, optionalDefault]) +const stringDefault = stringLiteral('x') +const optionalWithStringCall = callOf(memberOf('S', 'optionalWith'), [optionalSchema, stringDefault]) +const optionalCall = callOf(memberOf('S', 'optional'), [optionalSchema, optionalDefault]) + +const matchTag = stringLiteral('a') +const matchTagCall = callOf(memberOf('Match', 'tag'), [matchTag]) + +const orphanIdentifier = identifier('x') + +const identifierEntry = namedProperty('identifier', stringLiteral('HexBytes')) +const descriptionEntry = namedProperty('description', { + type: 'Literal', + value: 'Uint8Array encoded as a lowercase hex string', +}) +const titleEntry = namedProperty('title', stringLiteral('Hex Bytes')) +const documentationOnly = objectOf([identifierEntry, descriptionEntry, titleEntry]) +const documentationOnlyCall = annotationsCall(documentationOnly) + +const singleIdentifierEntry = namedProperty('identifier', stringLiteral('HexBytes')) +const identifierOnly = objectOf([singleIdentifierEntry]) +const identifierOnlyCall = annotationsCall(identifierOnly) + +const quotedKeyObject = objectOf([propertyOf(stringLiteral('identifier'), stringLiteral('HexBytes'))]) +const quotedKeyCall = annotationsCall(quotedKeyObject) + +const computedKeyEntry = propertyOf(identifier('identifier'), stringLiteral('x'), true) +const computedKeyObject = objectOf([computedKeyEntry]) +const computedKeyCall = annotationsCall(computedKeyObject) + +const mixedBehaviourEntry = namedProperty('arbitrary', { type: 'ArrowFunctionExpression' }) +const mixedDocumentationEntry = namedProperty('identifier', stringLiteral('HexStringInput')) +const mixedAnnotations = objectOf([mixedBehaviourEntry, mixedDocumentationEntry]) +const mixedAnnotationsCall = annotationsCall(mixedAnnotations) + +const behaviourOnlyEntry = namedProperty('arbitrary', { type: 'ArrowFunctionExpression' }) +const behaviourOnlyObject = objectOf([behaviourOnlyEntry]) +const behaviourOnlyCall = annotationsCall(behaviourOnlyObject) + +const documentationBesideBehaviourEntry = namedProperty('description', stringLiteral('x')) +const behaviourBesideDocumentationEntry = namedProperty('arbitrary', { type: 'ArrowFunctionExpression' }) +const documentationAndBehaviour = objectOf([documentationBesideBehaviourEntry, behaviourBesideDocumentationEntry]) +const documentationAndBehaviourCall = annotationsCall(documentationAndBehaviour) + +const emptyAnnotations = objectOf([]) +const emptyAnnotationsCall = annotationsCall(emptyAnnotations) + +const displacedDocumentation = objectOf([namedProperty('title', stringLiteral('Hex Bytes'))]) +const displacedDocumentationCall = callOf(memberOf('S', 'annotations'), [ + stringLiteral('other'), + displacedDocumentation, +]) + +const bareCalleeDocumentation = objectOf([namedProperty('title', stringLiteral('Hex Bytes'))]) +const bareCalleeCall = callOf(identifier('annotations'), [bareCalleeDocumentation]) + +const filteredDocumentationEntry = namedProperty('identifier', stringLiteral('x')) +const filteredDocumentation = objectOf([filteredDocumentationEntry]) +const filteredDocumentationCall = callOf(memberOf('S', 'filter'), [filteredDocumentation]) + +const uncalledDocumentationEntry = namedProperty('identifier', stringLiteral('x')) +const uncalledDocumentation = objectOf([uncalledDocumentationEntry]) + +const secondArgumentDocumentationEntry = namedProperty('identifier', stringLiteral('x')) +const secondArgumentDocumentation = objectOf([secondArgumentDocumentationEntry]) +const secondArgumentCall = callOf(memberOf('S', 'annotations'), [ + stringLiteral('other'), + secondArgumentDocumentation, +]) + +const descriptor = strykerIgnorers[0] +if (descriptor === undefined) throw new Error('the package publishes one ignorer descriptor') + +const CASES = { + ignored: [ + { + name: 'a `Symbol.for` description, which names a brand and carries no behaviour', + path: { node: brandDescription, ancestors: [brandDescriptionCall] }, + reason: SYMBOL_DESCRIPTION_IGNORED, + }, + { + name: 'a `TaggedClass` tag, which only labels the declaration', + path: { node: taggedTag, ancestors: [taggedClassCall] }, + reason: TAGGED_TAG_IGNORED, + }, + { + name: 'a `TaggedError` tag', + path: { node: taggedErrorTag, ancestors: [taggedErrorCall] }, + reason: TAGGED_TAG_IGNORED, + }, + { + name: 'a `TaggedClass` fields object, whose schema declares rather than decides', + path: { node: taggedFields, ancestors: [taggedClassCall] }, + reason: TAGGED_FIELDS_IGNORED, + }, + { + name: 'a `TaggedError` fields object', + path: { node: taggedErrorFields, ancestors: [taggedErrorCall] }, + reason: TAGGED_FIELDS_IGNORED, + }, + { + name: 'the arrow function `S.optionalWith` holds as a default', + path: { node: optionalDefault, ancestors: [optionalWithCall] }, + reason: OPTIONAL_DEFAULT_IGNORED, + }, + { + name: 'the identifier of a `Schema.Class`, which rides the inner call', + path: { node: classId, ancestors: [classInnerCall] }, + reason: CLASS_ID_IGNORED, + }, + { + name: 'a brand name, which is identity data like a description', + path: { node: brandName, ancestors: [brandNameCall] }, + reason: BRAND_NAME_IGNORED, + }, + { + name: 'an `annotations` object whose every entry documents', + path: { node: documentationOnly, ancestors: [documentationOnlyCall] }, + reason: ANNOTATION_OBJECT_IGNORED, + }, + { + name: 'the identifier value inside a documentation-only object', + path: { node: identifierEntry.value, ancestors: [identifierEntry, documentationOnly, documentationOnlyCall] }, + reason: ANNOTATION_TEXT_IGNORED, + }, + { + name: 'the description value inside a documentation-only object', + path: { node: descriptionEntry.value, ancestors: [descriptionEntry, documentationOnly, documentationOnlyCall] }, + reason: ANNOTATION_TEXT_IGNORED, + }, + { + name: 'the title value inside a documentation-only object', + path: { node: titleEntry.value, ancestors: [titleEntry, documentationOnly, documentationOnlyCall] }, + reason: ANNOTATION_TEXT_IGNORED, + }, + { + name: 'a documentation entry that shares its object with a generator', + path: { + node: mixedDocumentationEntry.value, + ancestors: [mixedDocumentationEntry, mixedAnnotations, mixedAnnotationsCall], + }, + reason: ANNOTATION_TEXT_IGNORED, + }, + { + name: 'an `annotations` object holding one identifier entry', + path: { node: identifierOnly, ancestors: [identifierOnlyCall] }, + reason: ANNOTATION_OBJECT_IGNORED, + }, + { + name: 'the value of that single identifier entry', + path: { + node: singleIdentifierEntry.value, + ancestors: [singleIdentifierEntry, identifierOnly, identifierOnlyCall], + }, + reason: ANNOTATION_TEXT_IGNORED, + }, + { + name: 'an `annotations` object keyed by string literals', + path: { node: quotedKeyObject, ancestors: [quotedKeyCall] }, + reason: ANNOTATION_OBJECT_IGNORED, + }, + { + name: 'a description entry beside an `arbitrary` generator', + path: { + node: documentationBesideBehaviourEntry.value, + ancestors: [documentationBesideBehaviourEntry, documentationAndBehaviour, documentationAndBehaviourCall], + }, + reason: ANNOTATION_TEXT_IGNORED, + }, + ], + kept: [ + { + name: 'a bare `TaggedClass` factory call keeps its tag', + path: { node: bareTag, ancestors: [bareFactoryCallNode] }, + }, + { + name: 'a bare `TaggedClass` factory call keeps its fields', + path: { node: bareFields, ancestors: [bareFactoryCallNode] }, + }, + { + name: 'a description in `Symbol.keyFor`, whose member is not `for`', + path: { node: keyForDescription, ancestors: [keyForCall] }, + }, + { + name: 'a description in `Object.for`, whose object is not `Symbol`', + path: { node: objectForDescription, ancestors: [objectForCall] }, + }, + { + name: 'a description in `Symbol.iterator`, whose member name differs', + path: { node: iteratorDescription, ancestors: [iteratorCall] }, + }, + { + name: 'a string literal default under `S.optionalWith`', + path: { node: stringDefault, ancestors: [optionalWithStringCall] }, + }, + { + name: 'an arrow function default under `S.optional`, whose callee differs', + path: { node: optionalDefault, ancestors: [optionalCall] }, + }, + { + name: 'an `annotations` object mixing a generator with documentation', + path: { node: mixedAnnotations, ancestors: [mixedAnnotationsCall] }, + }, + { + name: 'the generator value in a mixed `annotations` object', + path: { + node: mixedBehaviourEntry.value, + ancestors: [mixedBehaviourEntry, mixedAnnotations, mixedAnnotationsCall], + }, + }, + { + name: 'an `annotations` object holding only a generator', + path: { node: behaviourOnlyObject, ancestors: [behaviourOnlyCall] }, + }, + { + name: 'the generator value in a generator-only `annotations` object', + path: { node: behaviourOnlyEntry.value, ancestors: [behaviourOnlyEntry, behaviourOnlyObject, behaviourOnlyCall] }, + }, + { + name: 'an empty `annotations` object, which has no documentation entry', + path: { node: emptyAnnotations, ancestors: [emptyAnnotationsCall] }, + }, + { + name: 'an `annotations` object holding documentation beside a generator', + path: { node: documentationAndBehaviour, ancestors: [documentationAndBehaviourCall] }, + }, + { + name: 'a documentation object at the second argument of `S.annotations`', + path: { node: displacedDocumentation, ancestors: [displacedDocumentationCall] }, + }, + { + name: 'a documentation object under a bare `annotations` callee', + path: { node: bareCalleeDocumentation, ancestors: [bareCalleeCall] }, + }, + { + name: 'a `Schema.Class` fields object, which carries accepted value sets', + path: { node: classFields, ancestors: [classOuterCall] }, + }, + { + name: 'an accepted literal inside a `Schema.Literal` call', + path: { node: acceptedLiteralMember, ancestors: [acceptedLiteralCall] }, + }, + { + name: 'a `Schema.Struct` tag, which is not a tagged factory', + path: { node: structTag, ancestors: [structCall] }, + }, + { + name: 'a `Schema.Struct` fields object', + path: { node: structFields, ancestors: [structCall] }, + }, + { + name: 'an object expression at a `TaggedClass` tag slot', + path: { node: swappedFields, ancestors: [swappedCall] }, + }, + { + name: 'an object expression at a `Symbol.for` argument slot', + path: { node: objectAtSymbolFor, ancestors: [objectAtSymbolForCall] }, + }, + { + name: 'a string that is not the `Symbol.for` argument', + path: { node: unheldDescription, ancestors: [unheldDescriptionCall] }, + }, + { + name: 'a `Match.tag` argument, which is a runtime discriminator', + path: { node: matchTag, ancestors: [matchTagCall] }, + }, + { + name: 'an orphan identifier with no parent', + path: { node: orphanIdentifier }, + }, + { + name: 'a documentation object under `S.filter`', + path: { node: filteredDocumentation, ancestors: [filteredDocumentationCall] }, + }, + { + name: 'a documentation value under `S.filter`', + path: { + node: filteredDocumentationEntry.value, + ancestors: [filteredDocumentationEntry, filteredDocumentation, filteredDocumentationCall], + }, + }, + { + name: 'a documentation value whose enclosing call is absent', + path: { node: uncalledDocumentationEntry.value, ancestors: [uncalledDocumentationEntry, uncalledDocumentation] }, + }, + { + name: 'a documentation value at the second argument of `S.annotations`', + path: { + node: secondArgumentDocumentationEntry.value, + ancestors: [secondArgumentDocumentationEntry, secondArgumentDocumentation, secondArgumentCall], + }, + }, + { + name: 'an `annotations` object whose documentation key is computed', + path: { node: computedKeyObject, ancestors: [computedKeyCall] }, + }, + { + name: 'the value under a computed documentation key', + path: { node: computedKeyEntry.value, ancestors: [computedKeyEntry, computedKeyObject, computedKeyCall] }, + }, + ], +} + +interface CasePath { + readonly node: unknown + readonly ancestors?: readonly unknown[] | undefined +} + +const pathOf = (spec: CasePath): NodePath => ({ node: spec.node, ancestors: spec.ancestors ?? [] }) + +describe('effect-schema-declarations', () => { + it('Should_Register_The_Descriptor', () => { + expect(descriptor.name).toBe('effect-schema-declarations') + }) + it.each(CASES.ignored)('ignores: $name', (testCase) => { + expect(descriptor.shouldIgnore(pathOf(testCase.path))).toBe(testCase.reason) + }) + it.each(CASES.kept)('keeps: $name', (testCase) => { + expect(descriptor.shouldIgnore(pathOf(testCase.path))).toBeUndefined() + }) +}) diff --git a/packages/stryker-plugins/tsconfig.build.json b/packages/ignorers/effect-schema-declarations/tsconfig.build.json similarity index 100% rename from packages/stryker-plugins/tsconfig.build.json rename to packages/ignorers/effect-schema-declarations/tsconfig.build.json diff --git a/packages/stryker-plugins/tsconfig.json b/packages/ignorers/effect-schema-declarations/tsconfig.json similarity index 88% rename from packages/stryker-plugins/tsconfig.json rename to packages/ignorers/effect-schema-declarations/tsconfig.json index e3e0889bc..04c392e87 100644 --- a/packages/stryker-plugins/tsconfig.json +++ b/packages/ignorers/effect-schema-declarations/tsconfig.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/tsconfig", - "extends": ["@systemfsoftware/tsconfig/tsc/dom/library-monorepo", "@systemfsoftware/tsconfig/effect"], + "extends": ["@systemfsoftware/tsconfig/tsc/dom/library-monorepo"], "compilerOptions": { "types": [] }, diff --git a/packages/stryker-plugins/tsconfig.node.json b/packages/ignorers/effect-schema-declarations/tsconfig.node.json similarity index 79% rename from packages/stryker-plugins/tsconfig.node.json rename to packages/ignorers/effect-schema-declarations/tsconfig.node.json index 6895d764b..4d32e1ec2 100644 --- a/packages/stryker-plugins/tsconfig.node.json +++ b/packages/ignorers/effect-schema-declarations/tsconfig.node.json @@ -1,4 +1,4 @@ { "extends": "@systemfsoftware/tsconfig/node", - "include": ["oxlint.config.ts", "tsdown.config.ts", "vitest.config.ts", "vitest-setup.ts"] + "include": ["oxlint.config.ts", "tsdown.config.ts", "vitest.config.ts"] } diff --git a/packages/stryker-plugins/tsdown.config.ts b/packages/ignorers/effect-schema-declarations/tsdown.config.ts similarity index 50% rename from packages/stryker-plugins/tsdown.config.ts rename to packages/ignorers/effect-schema-declarations/tsdown.config.ts index 988226568..6a4bc1e03 100644 --- a/packages/stryker-plugins/tsdown.config.ts +++ b/packages/ignorers/effect-schema-declarations/tsdown.config.ts @@ -3,16 +3,10 @@ import { defineConfig } from 'tsdown' export default defineConfig({ entry: { index: './src/mod.ts', - 'effect-schema-ignorer': './src/effect-schema-ignorer/index.ts', - 'in-source-test-ignorer': './src/in-source-test-ignorer/index.ts', - 'workflow-make-ignorer': './src/workflow-make-ignorer/index.ts', }, format: 'esm', dts: true, tsconfig: './tsconfig.build.json', clean: true, outExtensions: () => ({ js: '.mjs', dts: '.d.ts' }), - deps: { - onlyBundle: false, - }, }) diff --git a/packages/ignorers/effect-schema-declarations/vitest.config.ts b/packages/ignorers/effect-schema-declarations/vitest.config.ts new file mode 100644 index 000000000..6536c4494 --- /dev/null +++ b/packages/ignorers/effect-schema-declarations/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig, sharedConfig } from '@systemfsoftware/vitest-config' + +export default defineConfig({ + ...sharedConfig, + test: { + ...sharedConfig.test, + include: ['src/**/*.test.ts', 'tests/**/*.test.ts'], + }, +}) diff --git a/packages/ignorers/in-source-vitest-block/.attw.json b/packages/ignorers/in-source-vitest-block/.attw.json new file mode 100644 index 000000000..76f3418da --- /dev/null +++ b/packages/ignorers/in-source-vitest-block/.attw.json @@ -0,0 +1 @@ +{ "ignoreRules": ["no-resolution", "cjs-resolves-to-esm"] } diff --git a/packages/ignorers/in-source-vitest-block/.gitignore b/packages/ignorers/in-source-vitest-block/.gitignore new file mode 100644 index 000000000..09c81c214 --- /dev/null +++ b/packages/ignorers/in-source-vitest-block/.gitignore @@ -0,0 +1,4 @@ +dist +temp +coverage +reports diff --git a/packages/ignorers/in-source-vitest-block/AGENTS.md b/packages/ignorers/in-source-vitest-block/AGENTS.md new file mode 100644 index 000000000..6e3b15576 --- /dev/null +++ b/packages/ignorers/in-source-vitest-block/AGENTS.md @@ -0,0 +1,27 @@ +# AGENTS.md — `@systemfsoftware/stryker-ignorer-in-source-vitest-block` + +Stryker ignorer for in-source Vitest blocks: removes mutants inside an `if (import.meta.vitest)` guard. The guards are hand-written predicates declared beside the decisions, and the package's only dependency is `@systemfsoftware/stryker-ignorer-interface`, which ships the node types and nothing that runs — zero Effect is part of its identity. Root `AGENTS.md` governs. + +## Rules + +| ID | Rule | Gate | +| ------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | +| **SP1** | An ignored mutant is unreachable in a mutation run: it lives inside an `if (import.meta.vitest)` block — test code, not production behaviour. | `pnpm --filter @systemfsoftware/stryker-ignorer-in-source-vitest-block test` | +| **SP2** | Every new ignore pattern arrives with a test demonstrating the unreachable mutant. | `pnpm --filter @systemfsoftware/stryker-ignorer-in-source-vitest-block test` | +| **SP3** | Register through the plain ignorer protocol only; never bypass other mutation stages. | `review` | +| **SP4** | No `effect` or `@effect/*` import in any source or built file — the preset's `no-restricted-imports` ban is the gate. | `pnpm --filter @systemfsoftware/stryker-ignorer-in-source-vitest-block lint` | +| **SP5** | No `effect` or `@effect/*` key in any dependency block; the interface package is the only runtime dependency. | `review` | +| **SP6** | Every behavior is pinned by a case table — no FastCheck generator and no snapshot call in a committed test. | `pnpm --filter @systemfsoftware/stryker-ignorer-in-source-vitest-block lint` | + +The reviewer's decision on each `review`-gated row, shown as `wrong:`/`right:`: + +- **SP3** — `wrong:` registering a checker or runner contribution beside the ignorer; `right:` `src/mod.ts` exports `strykerIgnorers` and nothing that answers another mutation stage. +- **SP5** — `wrong:` `"effect": "catalog:"` in `dependencies` or anywhere under `devDependencies`; `right:` `"@systemfsoftware/stryker-ignorer-interface": "workspace:^"` as the only `dependencies` entry. + +## Verification + +```bash +pnpm --filter @systemfsoftware/stryker-ignorer-in-source-vitest-block typecheck +pnpm --filter @systemfsoftware/stryker-ignorer-in-source-vitest-block test +pnpm --filter @systemfsoftware/stryker-ignorer-in-source-vitest-block lint +``` diff --git a/packages/ignorers/in-source-vitest-block/LICENSE b/packages/ignorers/in-source-vitest-block/LICENSE new file mode 100644 index 000000000..896c3747c --- /dev/null +++ b/packages/ignorers/in-source-vitest-block/LICENSE @@ -0,0 +1,203 @@ +Copyright (c) 2026 Ryan Lee (systemfsoftware) + +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2026 Ryan Lee (systemfsoftware) + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/ignorers/in-source-vitest-block/README.md b/packages/ignorers/in-source-vitest-block/README.md new file mode 100644 index 000000000..c3de8ffb2 --- /dev/null +++ b/packages/ignorers/in-source-vitest-block/README.md @@ -0,0 +1,71 @@ +# @systemfsoftware/stryker-ignorer-in-source-vitest-block + +![version](https://img.shields.io/npm/v/@systemfsoftware/stryker-ignorer-in-source-vitest-block) +![license](https://img.shields.io/npm/l/@systemfsoftware/stryker-ignorer-in-source-vitest-block) + +> In-source tests are not production behaviour. Stop their mutants from dragging your Stryker score below 100%. + +Your `if (import.meta.vitest)` blocks are stripped before production, so nothing that runs in a mutation run can ever reach their mutants. They sit in the report forever, indistinguishable from real coverage gaps. This [Stryker](https://stryker-mutator.io) ignorer removes them, so the score that remains is production behaviour. + +## Install + +```bash +pnpm add -D @systemfsoftware/stryker-ignorer-in-source-vitest-block +``` + +In `stryker.config.json`, the plugin module and the ignorer name travel as a pair: + +| Config key | Value | +| ---------- | --------------------------------------------------------- | +| `plugins` | `@systemfsoftware/stryker-ignorer-in-source-vitest-block` | +| `ignorers` | `in-source-vitest-block` | + +```jsonc +{ + "plugins": [ + "@systemfsoftware/stryker-js-vitest-runner", + "@systemfsoftware/stryker-js-typescript-checker", + "@systemfsoftware/stryker-ignorer-in-source-vitest-block" + ], + "ignorers": ["in-source-vitest-block"] +} +``` + +Mutants it recognizes are reported as `Ignored`, each carrying the reason it was safe to skip. + +> [!WARNING] +> A name in `ignorers` that no loaded plugin answers is silently skipped — the run proceeds and the mutants stay in your score. Copy the name from the table, not from memory. + +> [!NOTE] +> Requires `@systemfsoftware/stryker-js-engine` 4.1.0 or later (the first version whose plugin loader speaks the plain ignorer protocol). The package has no peer dependencies, no Effect dependency at all, and exactly one dependency — `@systemfsoftware/stryker-ignorer-interface`, whose node types declare the AST shapes the guards read. + +## Migrating from `@systemfsoftware/stryker-plugins` + +| Before | After | +| ------------------------------------------------- | ------------------------------------------------------------------------ | +| `"plugins": ["@systemfsoftware/stryker-plugins"]` | `"plugins": ["@systemfsoftware/stryker-ignorer-in-source-vitest-block"]` | +| `"ignorers": ["in-source-vitest-block"]` | unchanged | + +## What it ignores + +| Guard shape | Example | +| ---------------------------------------------------------- | ----------------------------------------------- | +| The bare Vitest flag | `if (import.meta.vitest) { … }` | +| The flag on either side of a comparison | `if (import.meta.vitest === undefined) { … }` | +| Any mutant whose ancestors include one of the guards above | the whole guarded block, test fixtures included | + +## Where the line is + +The ignored mutants are _unreachable_, not merely believed equivalent: the guard is false under the mutation run, so no test can exercise what is inside it. That is a different claim from a proven-equivalent mutant, and it is why the reason constant says so. + +- A flag on a different meta property (`import.meta.env`) or on a non-import meta (`require.meta.vitest`) is **not** a guard — its mutants stay live. +- A bare `import.meta.vitest` expression without an enclosing `if` is **not** a guard. +- Only the guard shape is recognized; ordinary `if` statements keep every mutant, because their branches are reached in production. + +## Contributing + +Development setup and workflow: [AGENTS.md](AGENTS.md). + +## License + +[Apache 2.0](LICENSE) diff --git a/packages/ignorers/in-source-vitest-block/api-extractor.json b/packages/ignorers/in-source-vitest-block/api-extractor.json new file mode 100644 index 000000000..f2ca4f1f0 --- /dev/null +++ b/packages/ignorers/in-source-vitest-block/api-extractor.json @@ -0,0 +1,55 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "projectFolder": ".", + "mainEntryPointFilePath": "/dist/index.d.ts", + "bundledPackages": [], + "compiler": { + "tsconfigFilePath": "/tsconfig.json" + }, + "apiReport": { + "enabled": true, + "reportFolder": "/etc/", + "reportTempFolder": "/temp/main/", + "reportFileName": "stryker-ignorer-in-source-vitest-block.api.md" + }, + "docModel": { + "enabled": false + }, + "dtsRollup": { + "enabled": true, + "untrimmedFilePath": "/dist/index.d.ts" + }, + "tsdocMetadata": { + "enabled": false + }, + "messages": { + "extractorMessageReporting": { + "default": { + "logLevel": "warning" + }, + "ae-forgotten-export": { + "logLevel": "error" + }, + "ae-missing-release-tag": { + "logLevel": "none" + }, + "ae-internal-missing-underscore": { + "logLevel": "none" + } + }, + "tsdocMessageReporting": { + "default": { + "logLevel": "warning" + }, + "tsdoc-undefined-tag": { + "logLevel": "none" + }, + "tsdoc-unsupported-tag": { + "logLevel": "none" + }, + "tsdoc-param-tag-missing-hyphen": { + "logLevel": "none" + } + } + } +} diff --git a/packages/ignorers/in-source-vitest-block/etc/stryker-ignorer-in-source-vitest-block.api.md b/packages/ignorers/in-source-vitest-block/etc/stryker-ignorer-in-source-vitest-block.api.md new file mode 100644 index 000000000..1b1ccc70a --- /dev/null +++ b/packages/ignorers/in-source-vitest-block/etc/stryker-ignorer-in-source-vitest-block.api.md @@ -0,0 +1,29 @@ +## API Report File for "@systemfsoftware/stryker-ignorer-in-source-vitest-block" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { NodePath } from '@systemfsoftware/stryker-ignorer-interface'; + +// @public (undocumented) +export const decideInSourceTestIgnore: (ancestors: Iterable) => string | undefined; + +// @public (undocumented) +export const firstIgnoreReason: (path: NodePath) => string | undefined; + +// @public (undocumented) +export const IN_SOURCE_TEST_IGNORED: 'inside an `if (import.meta.vitest)` block — test code, not production behaviour'; + +// @public (undocumented) +export const isInSourceTestGuard: (node: unknown) => boolean; + +// @public (undocumented) +export const strykerIgnorers: { + name: string; + shouldIgnore: typeof firstIgnoreReason; +}[]; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/ignorers/in-source-vitest-block/oxlint.config.ts b/packages/ignorers/in-source-vitest-block/oxlint.config.ts new file mode 100644 index 000000000..879f8a966 --- /dev/null +++ b/packages/ignorers/in-source-vitest-block/oxlint.config.ts @@ -0,0 +1,6 @@ +import preset from '@systemfsoftware/oxlint-ignorer-config' +import { defineConfig } from 'oxlint' + +export default defineConfig({ + extends: [preset], +}) diff --git a/packages/ignorers/in-source-vitest-block/package.json b/packages/ignorers/in-source-vitest-block/package.json new file mode 100644 index 000000000..4223ac6f9 --- /dev/null +++ b/packages/ignorers/in-source-vitest-block/package.json @@ -0,0 +1,70 @@ +{ + "name": "@systemfsoftware/stryker-ignorer-in-source-vitest-block", + "license": "Apache-2.0", + "version": "0.0.0", + "author": "Ryan Lee ", + "repository": { + "type": "git", + "url": "git+https://github.com/systemfsoftware/stryker-js-effect.git", + "directory": "packages/ignorers/in-source-vitest-block" + }, + "homepage": "https://github.com/systemfsoftware/stryker-js-effect/tree/main/packages/ignorers/in-source-vitest-block#readme", + "bugs": { + "url": "https://github.com/systemfsoftware/stryker-js-effect/issues" + }, + "description": "Stryker ignorer that removes mutants inside `if (import.meta.vitest)` guards — test code unreachable in a mutation run.", + "keywords": [ + "stryker", + "stryker-mutator", + "mutation-testing", + "ignorer" + ], + "type": "module", + "files": [ + "dist" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.mjs" + }, + "./package.json": "./package.json" + }, + "scripts": { + "build": "tsdown && pnpm api:check", + "clean": "rimraf dist", + "typecheck": "tsc --noEmit --incremental && tsc -p tsconfig.node.json --noEmit", + "test": "vitest run", + "lint": "f=${OXLINT_FORMAT:-${AGENT:+agent}}; oxlint . --format=${f:-default}", + "attw": "attw --pack .", + "api:check": "tsdown && api-extractor run", + "api:update": "api-extractor run --local" + }, + "dependencies": { + "@systemfsoftware/stryker-ignorer-interface": "workspace:^" + }, + "devDependencies": { + "@microsoft/api-extractor": "catalog:", + "@systemfsoftware/arethetypeswrong-cli": "catalog:", + "@systemfsoftware/oxlint-ignorer-config": "workspace:^", + "@systemfsoftware/tsconfig": "catalog:", + "@systemfsoftware/vitest-config": "workspace:^", + "@types/node": "catalog:", + "oxlint": "catalog:", + "rimraf": "catalog:", + "tsdown": "catalog:", + "typescript": "catalog:", + "vitest": "catalog:" + }, + "publishConfig": { + "access": "public", + "provenance": true, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.mjs" + }, + "./package.json": "./package.json" + } + } +} diff --git a/packages/ignorers/in-source-vitest-block/src/InSourceTestIgnore.ts b/packages/ignorers/in-source-vitest-block/src/InSourceTestIgnore.ts new file mode 100644 index 000000000..9f3400554 --- /dev/null +++ b/packages/ignorers/in-source-vitest-block/src/InSourceTestIgnore.ts @@ -0,0 +1,88 @@ +import type { + BinaryExpression, + IdentifierName, + IfStatement, + MemberExpression, + MetaProperty, +} from '@systemfsoftware/stryker-ignorer-interface' + +export const IN_SOURCE_TEST_IGNORED = + 'inside an `if (import.meta.vitest)` block — test code, not production behaviour' as const + +export const VITEST_META_PROPERTY = 'vitest' as const + +interface TypedNode { + readonly type: string +} + +const isObject = (value: unknown): value is object => typeof value === 'object' && value !== null + +const hasStringType = (value: object): value is TypedNode => 'type' in value && typeof value.type === 'string' + +const isAstLike = (value: unknown): value is TypedNode => isObject(value) && hasStringType(value) + +const isNodeOfType = (value: unknown, type: string): value is TypedNode => isAstLike(value) && value.type === type + +const hasStringName = (value: TypedNode): boolean => 'name' in value && typeof value.name === 'string' + +export const isIdentifierName = (value: unknown): value is IdentifierName => + isNodeOfType(value, 'Identifier') && hasStringName(value) + +const isNamed = (value: unknown, name: string): value is IdentifierName => + isIdentifierName(value) && value.name === name + +const hasMetaIdentifier = (value: TypedNode): boolean => 'meta' in value && isIdentifierName(value.meta) + +const hasPropertyIdentifier = (value: TypedNode): boolean => 'property' in value && isIdentifierName(value.property) + +const hasIdentifierPair = (value: TypedNode): boolean => hasMetaIdentifier(value) && hasPropertyIdentifier(value) + +const isMetaProperty = (value: unknown): value is MetaProperty => + isNodeOfType(value, 'MetaProperty') && hasIdentifierPair(value) + +const hasObjectMetaProperty = (value: TypedNode): boolean => 'object' in value && isMetaProperty(value.object) + +const hasImportMetaPair = (value: TypedNode): boolean => hasObjectMetaProperty(value) && hasPropertyIdentifier(value) + +export const isImportMetaMember = (value: unknown): value is MemberExpression => + isNodeOfType(value, 'MemberExpression') && hasImportMetaPair(value) + +const hasLeftAst = (value: TypedNode): boolean => 'left' in value && isAstLike(value.left) + +const hasRightAst = (value: TypedNode): boolean => 'right' in value && isAstLike(value.right) + +const hasBinaryOperands = (value: TypedNode): boolean => hasLeftAst(value) && hasRightAst(value) + +export const isBinaryExpression = (value: unknown): value is BinaryExpression => + isNodeOfType(value, 'BinaryExpression') && hasBinaryOperands(value) + +const hasTestAst = (value: TypedNode): boolean => 'test' in value && isAstLike(value.test) + +export const isIfStatement = (value: unknown): value is IfStatement => + isNodeOfType(value, 'IfStatement') && hasTestAst(value) + +const isImportMetaObject = (node: MetaProperty): boolean => + isNamed(node.meta, 'import') && isNamed(node.property, 'meta') + +const hasImportMetaObject = (node: MemberExpression): boolean => + isMetaProperty(node.object) && isImportMetaObject(node.object) + +const hasVitestProperty = (node: MemberExpression): boolean => isNamed(node.property, VITEST_META_PROPERTY) + +const hasImportMetaVitest = (node: MemberExpression): boolean => hasImportMetaObject(node) && hasVitestProperty(node) + +const isImportMetaVitest = (node: unknown): boolean => isImportMetaMember(node) && hasImportMetaVitest(node) + +const hasImportMetaVitestOperand = (test: BinaryExpression): boolean => + isImportMetaVitest(test.left) || isImportMetaVitest(test.right) + +const isBinaryImportMetaVitest = (test: unknown): boolean => + isBinaryExpression(test) && hasImportMetaVitestOperand(test) + +const guardsOnImportMetaVitest = (test: unknown): boolean => isImportMetaVitest(test) || isBinaryImportMetaVitest(test) + +export const isInSourceTestGuard = (node: unknown): boolean => + isIfStatement(node) && guardsOnImportMetaVitest(node.test) + +export const decideInSourceTestIgnore = (ancestors: Iterable): string | undefined => + [...ancestors].some(isInSourceTestGuard) ? IN_SOURCE_TEST_IGNORED : undefined diff --git a/packages/ignorers/in-source-vitest-block/src/mod.ts b/packages/ignorers/in-source-vitest-block/src/mod.ts new file mode 100644 index 000000000..37a3a7657 --- /dev/null +++ b/packages/ignorers/in-source-vitest-block/src/mod.ts @@ -0,0 +1,9 @@ +import type { NodePath } from '@systemfsoftware/stryker-ignorer-interface' + +import { decideInSourceTestIgnore, IN_SOURCE_TEST_IGNORED, isInSourceTestGuard } from './InSourceTestIgnore.js' + +const firstIgnoreReason = (path: NodePath): string | undefined => decideInSourceTestIgnore(path.ancestors) + +export const strykerIgnorers = [{ name: 'in-source-vitest-block', shouldIgnore: firstIgnoreReason }] + +export { decideInSourceTestIgnore, IN_SOURCE_TEST_IGNORED, isInSourceTestGuard } diff --git a/packages/stryker-plugins/tests/__fixtures__/InSourceTestAst.fixtures.ts b/packages/ignorers/in-source-vitest-block/tests/__fixtures__/InSourceTestAst.fixtures.ts similarity index 100% rename from packages/stryker-plugins/tests/__fixtures__/InSourceTestAst.fixtures.ts rename to packages/ignorers/in-source-vitest-block/tests/__fixtures__/InSourceTestAst.fixtures.ts diff --git a/packages/ignorers/in-source-vitest-block/tests/in-source-vitest-block.test.ts b/packages/ignorers/in-source-vitest-block/tests/in-source-vitest-block.test.ts new file mode 100644 index 000000000..6c88e555f --- /dev/null +++ b/packages/ignorers/in-source-vitest-block/tests/in-source-vitest-block.test.ts @@ -0,0 +1,123 @@ +import { describe, expect, it } from 'vitest' + +import { IN_SOURCE_TEST_IGNORED, strykerIgnorers } from '@systemfsoftware/stryker-ignorer-in-source-vitest-block' +import { type NodePath, type PlainIgnorer } from '@systemfsoftware/stryker-ignorer-interface' + +import { binaryOf, guardOf, identifier, importMetaMember, metaOf } from './__fixtures__/InSourceTestAst.fixtures.js' + +const CASES = { + ignored: [ + { + name: 'A bare vitest flag as the guard condition matches', + path: { node: identifier('x'), ancestors: [guardOf(importMetaMember('vitest'))] }, + reason: IN_SOURCE_TEST_IGNORED, + }, + { + name: 'A vitest flag on the left side of a comparison matches', + path: { + node: identifier('x'), + ancestors: [guardOf(binaryOf(importMetaMember('vitest'), identifier('undefined')))], + }, + reason: IN_SOURCE_TEST_IGNORED, + }, + { + name: 'A vitest flag on the right side of a comparison matches', + path: { + node: identifier('x'), + ancestors: [guardOf(binaryOf(identifier('undefined'), importMetaMember('vitest')))], + }, + reason: IN_SOURCE_TEST_IGNORED, + }, + { + name: 'A mutant guarded by an ancestor vitest check is ignored', + path: { + node: identifier('mutant'), + ancestors: [identifier('x'), binaryOf(identifier('a'), identifier('b')), guardOf(importMetaMember('vitest'))], + }, + reason: IN_SOURCE_TEST_IGNORED, + }, + { + name: + 'The exported descriptor registers under the in-source-vitest-block name and answers like the decision function', + path: { node: identifier('x'), ancestors: [guardOf(importMetaMember('vitest'))] }, + reason: IN_SOURCE_TEST_IGNORED, + }, + ], + kept: [ + { + name: 'A flag for a different meta property does not match', + path: { node: identifier('x'), ancestors: [guardOf(importMetaMember('env'))] }, + }, + { + name: 'A vitest flag on a non-import meta does not match', + path: { + node: identifier('x'), + ancestors: [ + guardOf({ type: 'MemberExpression', object: metaOf('require', 'meta'), property: identifier('vitest') }), + ], + }, + }, + { + name: 'A vitest property on a non-meta object does not match', + path: { + node: identifier('x'), + ancestors: [ + guardOf({ type: 'MemberExpression', object: metaOf('import', 'cache'), property: identifier('vitest') }), + ], + }, + }, + { + name: 'A bare vitest expression without an if statement does not match', + path: { node: importMetaMember('vitest') }, + }, + { + name: 'A plain comparison without a vitest flag does not match', + path: { node: identifier('x'), ancestors: [guardOf(binaryOf(identifier('a'), identifier('b')))] }, + }, + { + name: 'A mutant with no guard in its ancestors stays live', + path: { node: identifier('mutant'), ancestors: [identifier('x'), guardOf(importMetaMember('env'))] }, + }, + { + name: 'A mutant with no ancestors at all stays live', + path: { node: identifier('mutant') }, + }, + { + name: 'A non-IfStatement parent is not a guard', + path: { node: identifier('x'), ancestors: [importMetaMember('vitest')] }, + }, + ], +} + +const ignorer: PlainIgnorer | undefined = strykerIgnorers[0] +if (ignorer === undefined) throw new Error('@systemfsoftware/stryker-ignorer-in-source-vitest-block exports no ignorer') + +interface CasePath { + readonly node: unknown + readonly ancestors?: readonly unknown[] | undefined +} + +const pathOf = (spec: CasePath): NodePath => ({ node: spec.node, ancestors: spec.ancestors ?? [] }) + +describe('in-source-vitest-block', () => { + it('Should_Register_The_Descriptor', () => { + expect(ignorer.name).toBe('in-source-vitest-block') + }) + it.each(CASES.ignored)('ignores: $name', (testCase) => { + expect(ignorer.shouldIgnore(pathOf(testCase.path))).toBe(testCase.reason) + }) + it.each(CASES.kept)('keeps: $name', (testCase) => { + expect(ignorer.shouldIgnore(pathOf(testCase.path))).toBeUndefined() + }) + it('ignores a literal whose ancestors carry the guard', () => { + expect( + ignorer.shouldIgnore({ + node: { type: 'Literal', value: 'production' }, + ancestors: [ + binaryOf(importMetaMember('vitest'), identifier('undefined')), + guardOf(importMetaMember('vitest')), + ], + }), + ).toBe(IN_SOURCE_TEST_IGNORED) + }) +}) diff --git a/packages/ignorers/in-source-vitest-block/tsconfig.build.json b/packages/ignorers/in-source-vitest-block/tsconfig.build.json new file mode 100644 index 000000000..869b9aaa9 --- /dev/null +++ b/packages/ignorers/in-source-vitest-block/tsconfig.build.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declarationMap": false, + "sourceMap": false, + "stripInternal": true + }, + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.spec.ts", "src/**/*.test.ts", "src/**/*.tst.ts"] +} diff --git a/packages/ignorers/in-source-vitest-block/tsconfig.json b/packages/ignorers/in-source-vitest-block/tsconfig.json new file mode 100644 index 000000000..04c392e87 --- /dev/null +++ b/packages/ignorers/in-source-vitest-block/tsconfig.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": ["@systemfsoftware/tsconfig/tsc/dom/library-monorepo"], + "compilerOptions": { + "types": [] + }, + "include": [ + "src", + "tests" + ], + "exclude": [ + "node_modules", + "dist" + ], + "references": [ + { + "path": "./tsconfig.node.json" + } + ] +} diff --git a/packages/ignorers/in-source-vitest-block/tsconfig.node.json b/packages/ignorers/in-source-vitest-block/tsconfig.node.json new file mode 100644 index 000000000..4d32e1ec2 --- /dev/null +++ b/packages/ignorers/in-source-vitest-block/tsconfig.node.json @@ -0,0 +1,4 @@ +{ + "extends": "@systemfsoftware/tsconfig/node", + "include": ["oxlint.config.ts", "tsdown.config.ts", "vitest.config.ts"] +} diff --git a/packages/ignorers/in-source-vitest-block/tsdown.config.ts b/packages/ignorers/in-source-vitest-block/tsdown.config.ts new file mode 100644 index 000000000..6a4bc1e03 --- /dev/null +++ b/packages/ignorers/in-source-vitest-block/tsdown.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'tsdown' + +export default defineConfig({ + entry: { + index: './src/mod.ts', + }, + format: 'esm', + dts: true, + tsconfig: './tsconfig.build.json', + clean: true, + outExtensions: () => ({ js: '.mjs', dts: '.d.ts' }), +}) diff --git a/packages/ignorers/in-source-vitest-block/vitest.config.ts b/packages/ignorers/in-source-vitest-block/vitest.config.ts new file mode 100644 index 000000000..6536c4494 --- /dev/null +++ b/packages/ignorers/in-source-vitest-block/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig, sharedConfig } from '@systemfsoftware/vitest-config' + +export default defineConfig({ + ...sharedConfig, + test: { + ...sharedConfig.test, + include: ['src/**/*.test.ts', 'tests/**/*.test.ts'], + }, +}) diff --git a/packages/ignorers/interface/.attw.json b/packages/ignorers/interface/.attw.json new file mode 100644 index 000000000..76f3418da --- /dev/null +++ b/packages/ignorers/interface/.attw.json @@ -0,0 +1 @@ +{ "ignoreRules": ["no-resolution", "cjs-resolves-to-esm"] } diff --git a/packages/ignorers/interface/AGENTS.md b/packages/ignorers/interface/AGENTS.md new file mode 100644 index 000000000..816ade357 --- /dev/null +++ b/packages/ignorers/interface/AGENTS.md @@ -0,0 +1,35 @@ +# AGENTS.md — `@systemfsoftware/stryker-ignorer-interface` + +The ignorer interface as types only: `PlainIgnorer`, the AST vocabulary +re-exported from `@oxc-project/types` and bundled into this package's own +declarations, and `NodePath`. The package publishes no value, no function, and +no schema — a guard and a reason string belong to the ignorer that needs them. +Zero runtime dependencies and zero Effect is the package's identity — nothing +that imports effect or a host package, at run time or in `devDependencies`, may +land here. Root `AGENTS.md` governs. + +## Rules + +| ID | Rule | Gate | +| ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | +| **SI1** | No `effect` or `@effect/*` import in any source or built file — the preset's `no-restricted-imports` ban is the gate. | `pnpm --filter @systemfsoftware/stryker-ignorer-interface lint` | +| **SI2** | No `effect` or `@effect/*` key in any dependency block of `package.json`. | `review` — the reviewer reads the dependency blocks | +| **SI3** | Types only: no source or built file exports a value, a function, or a class — `dist/index.mjs` stays empty and every export is a type. | `pnpm --filter @systemfsoftware/stryker-ignorer-interface build` | +| **SI4** | An ignorer's absence is never a reason: `shouldIgnore` returns a reason string or `undefined`, nothing else. | `review` — the reviewer reads the declared signature | +| **SI5** | The published surface is exactly what `etc/stryker-ignorer-interface.api.md` lists; the report is regenerated with `pnpm api:update`, never hand-edited. | `pnpm --filter @systemfsoftware/stryker-ignorer-interface build` | +| **SI6** | No test suite ships here: the package has no behaviour to pin, so the API report and the type-checker are its verification. | `pnpm --filter @systemfsoftware/stryker-ignorer-interface test` | + +The reviewer's decision on each `review`-gated row, shown as `wrong:`/`right:`: + +- **SI2** — `wrong:` `"effect": "catalog:"` in `dependencies` or anywhere under `devDependencies`; `right:` an empty `dependencies` and a `devDependencies` block holding only build tooling. +- **SI4** — `wrong:` `shouldIgnore(path: NodePath): boolean | string`; `right:` `shouldIgnore(path: NodePath): string | undefined`. + +## Verification + +```bash +pnpm --filter @systemfsoftware/stryker-ignorer-interface build +pnpm --filter @systemfsoftware/stryker-ignorer-interface typecheck +pnpm --filter @systemfsoftware/stryker-ignorer-interface test +pnpm --filter @systemfsoftware/stryker-ignorer-interface lint +pnpm --filter @systemfsoftware/stryker-ignorer-interface attw +``` diff --git a/packages/ignorers/interface/LICENSE b/packages/ignorers/interface/LICENSE new file mode 100644 index 000000000..896c3747c --- /dev/null +++ b/packages/ignorers/interface/LICENSE @@ -0,0 +1,203 @@ +Copyright (c) 2026 Ryan Lee (systemfsoftware) + +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2026 Ryan Lee (systemfsoftware) + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/ignorers/interface/README.md b/packages/ignorers/interface/README.md new file mode 100644 index 000000000..af62c63ad --- /dev/null +++ b/packages/ignorers/interface/README.md @@ -0,0 +1,72 @@ +# @systemfsoftware/stryker-ignorer-interface + +![version](https://img.shields.io/npm/v/@systemfsoftware/stryker-ignorer-interface) +![license](https://img.shields.io/npm/l/@systemfsoftware/stryker-ignorer-interface) + +> The ignorer interface: the descriptor and the AST vocabulary an ignorer reasons +> about, as types only — the package ships no runtime code. + +An ignorer is the smallest thing a StrykerJS-style mutation tool can load: a +name and a synchronous decision over a path the host hands it. This package +carries the shapes of that contract and nothing that runs. It has **no** Effect +dependency, runtime or development: `dependencies` is empty, no value is +exported, and the kitchen-sink family aggregate preset is replaced by +`@systemfsoftware/oxlint-ignorer-config`, which bans Effect imports outright. + +| Export | What it is | +| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `PlainIgnorer` | The descriptor: `{ name, shouldIgnore(path): string \| undefined }` | +| vocabulary | the AST vocabulary, re-exported from [`@oxc-project/types`](https://www.npmjs.com/package/@oxc-project/types) and bundled into this package's own declarations, so a consumer installs nothing else — `Node`, `Expression`, `Statement`, `Program`, `Span`, and every concrete node interface | +| `NodePath` | the path shape the host hands `shouldIgnore`: the node and its ancestors, nearest first | + +Everything the package publishes is a type. A guard and a reason string are the +ignorer's own code: the host never interprets a schema, it calls +`shouldIgnore(path)` and uses what comes back. + +## Install + +```bash +pnpm add -D @systemfsoftware/stryker-ignorer-interface +``` + +## Write an ignorer + +```ts +import type { NodePath, PlainIgnorer } from '@systemfsoftware/stryker-ignorer-interface' + +const isGenerated = (node: unknown): boolean => + typeof node === 'object' && node !== null && 'name' in node && node.name === '__generated' + +const inGeneratedCode = (path: NodePath): boolean => path.ancestors.some(isGenerated) + +const myIgnorer: PlainIgnorer = { + name: 'generated-code', + shouldIgnore: (path) => (inGeneratedCode(path) ? 'the enclosing member is generated' : undefined), +} + +export const strykerIgnorers = [myIgnorer] +``` + +An entry is just `{ name, shouldIgnore }`: the host registers each one as an +`Ignore` contribution and validates at load that `name` is a string and +`shouldIgnore` is callable — an entry that fails fails the load by name. + +A module exporting `strykerIgnorers` loads as `Ignore` plugin contributions in +any engine carrying the plain-ignorer loader. Configure the pair — they take +different strings: + +| Config key | Value | +| ---------- | ----------------------------------------------- | +| `plugins` | the module specifier (this package or your own) | +| `ignorers` | the contribution name (`generated-code` above) | + +A mismatched pair fails silent-green: the run completes with nothing ignored +and only a `Cannot find plugin` warning. + +## Contributing + +Development setup and workflow: [AGENTS.md](AGENTS.md). + +## License + +[Apache 2.0](LICENSE) diff --git a/packages/ignorers/interface/api-extractor.json b/packages/ignorers/interface/api-extractor.json new file mode 100644 index 000000000..3c8aaa7bf --- /dev/null +++ b/packages/ignorers/interface/api-extractor.json @@ -0,0 +1,55 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "projectFolder": ".", + "mainEntryPointFilePath": "/dist/index.d.ts", + "bundledPackages": [], + "compiler": { + "tsconfigFilePath": "/tsconfig.json" + }, + "apiReport": { + "enabled": true, + "reportFolder": "/etc/", + "reportTempFolder": "/temp/main/", + "reportFileName": "stryker-ignorer-interface.api.md" + }, + "docModel": { + "enabled": false + }, + "dtsRollup": { + "enabled": true, + "untrimmedFilePath": "/dist/index.d.ts" + }, + "tsdocMetadata": { + "enabled": false + }, + "messages": { + "extractorMessageReporting": { + "default": { + "logLevel": "warning" + }, + "ae-forgotten-export": { + "logLevel": "error" + }, + "ae-missing-release-tag": { + "logLevel": "none" + }, + "ae-internal-missing-underscore": { + "logLevel": "none" + } + }, + "tsdocMessageReporting": { + "default": { + "logLevel": "warning" + }, + "tsdoc-undefined-tag": { + "logLevel": "none" + }, + "tsdoc-unsupported-tag": { + "logLevel": "none" + }, + "tsdoc-param-tag-missing-hyphen": { + "logLevel": "none" + } + } + } +} diff --git a/packages/ignorers/interface/etc/stryker-ignorer-interface.api.md b/packages/ignorers/interface/etc/stryker-ignorer-interface.api.md new file mode 100644 index 000000000..6c165e11d --- /dev/null +++ b/packages/ignorers/interface/etc/stryker-ignorer-interface.api.md @@ -0,0 +1,2678 @@ +## API Report File for "@systemfsoftware/stryker-ignorer-interface" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +// @public (undocumented) +export interface AccessorProperty extends Span { + // (undocumented) + accessibility?: TSAccessibility | null; + // (undocumented) + computed: boolean; + // (undocumented) + declare?: false; + // (undocumented) + decorators: Array; + // (undocumented) + definite?: boolean; + // (undocumented) + key: PropertyKey_2; + // (undocumented) + optional?: false; + // (undocumented) + override?: boolean; + // (undocumented) + parent?: Node_2; + // (undocumented) + readonly?: false; + // (undocumented) + static: boolean; + // (undocumented) + type: AccessorPropertyType; + // (undocumented) + typeAnnotation?: TSTypeAnnotation | null; + // (undocumented) + value: Expression | null; +} + +// @public (undocumented) +export type AccessorPropertyType = "AccessorProperty" | "TSAbstractAccessorProperty"; + +// @public (undocumented) +export type Argument = SpreadElement | Expression; + +// @public (undocumented) +export interface ArrayAssignmentTarget extends Span { + // (undocumented) + decorators?: []; + // (undocumented) + elements: Array; + // (undocumented) + optional?: false; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "ArrayPattern"; + // (undocumented) + typeAnnotation?: null; +} + +// @public (undocumented) +export interface ArrayExpression extends Span { + // (undocumented) + elements: Array; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "ArrayExpression"; +} + +// @public (undocumented) +export type ArrayExpressionElement = SpreadElement | null | Expression; + +// @public (undocumented) +export interface ArrayPattern extends Span { + // (undocumented) + decorators?: []; + // (undocumented) + elements: Array; + // (undocumented) + optional?: false; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "ArrayPattern"; + // (undocumented) + typeAnnotation?: TSTypeAnnotation | null; +} + +// @public (undocumented) +export interface ArrowFunctionExpression extends Span { + // (undocumented) + async: boolean; + // (undocumented) + body: FunctionBody | Expression; + // (undocumented) + expression: boolean; + // (undocumented) + generator: false; + // (undocumented) + id: null; + // (undocumented) + params: ParamPattern[]; + // (undocumented) + parent?: Node_2; + // (undocumented) + returnType?: TSTypeAnnotation | null; + // (undocumented) + type: "ArrowFunctionExpression"; + // (undocumented) + typeParameters?: TSTypeParameterDeclaration | null; +} + +// @public (undocumented) +export interface AssignmentExpression extends Span { + // (undocumented) + left: AssignmentTarget; + // (undocumented) + operator: AssignmentOperator; + // (undocumented) + parent?: Node_2; + // (undocumented) + right: Expression; + // (undocumented) + type: "AssignmentExpression"; +} + +// @public (undocumented) +export type AssignmentOperator = "=" | "+=" | "-=" | "*=" | "/=" | "%=" | "**=" | "<<=" | ">>=" | ">>>=" | "|=" | "^=" | "&=" | "||=" | "&&=" | "??="; + +// @public (undocumented) +export interface AssignmentPattern extends Span { + // (undocumented) + decorators?: []; + // (undocumented) + left: BindingPattern; + // (undocumented) + optional?: false; + // (undocumented) + parent?: Node_2; + // (undocumented) + right: Expression; + // (undocumented) + type: "AssignmentPattern"; + // (undocumented) + typeAnnotation?: null; +} + +// @public (undocumented) +export type AssignmentTarget = SimpleAssignmentTarget | AssignmentTargetPattern; + +// @public (undocumented) +export type AssignmentTargetMaybeDefault = AssignmentTargetWithDefault | AssignmentTarget; + +// @public (undocumented) +export type AssignmentTargetPattern = ArrayAssignmentTarget | ObjectAssignmentTarget; + +// @public (undocumented) +export type AssignmentTargetProperty = AssignmentTargetPropertyIdentifier | AssignmentTargetPropertyProperty; + +// @public (undocumented) +export interface AssignmentTargetPropertyIdentifier extends Span { + // (undocumented) + computed: false; + // (undocumented) + key: IdentifierReference; + // (undocumented) + kind: "init"; + // (undocumented) + method: false; + // (undocumented) + optional?: false; + // (undocumented) + parent?: Node_2; + // (undocumented) + shorthand: true; + // (undocumented) + type: "Property"; + // (undocumented) + value: IdentifierReference | AssignmentTargetWithDefault; +} + +// @public (undocumented) +export interface AssignmentTargetPropertyProperty extends Span { + // (undocumented) + computed: boolean; + // (undocumented) + key: PropertyKey_2; + // (undocumented) + kind: "init"; + // (undocumented) + method: false; + // (undocumented) + optional?: false; + // (undocumented) + parent?: Node_2; + // (undocumented) + shorthand: false; + // (undocumented) + type: "Property"; + // (undocumented) + value: AssignmentTargetMaybeDefault; +} + +// @public (undocumented) +export interface AssignmentTargetRest extends Span { + // (undocumented) + argument: AssignmentTarget; + // (undocumented) + decorators?: []; + // (undocumented) + optional?: false; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "RestElement"; + // (undocumented) + typeAnnotation?: null; + // (undocumented) + value?: null; +} + +// @public (undocumented) +export interface AssignmentTargetWithDefault extends Span { + // (undocumented) + decorators?: []; + // (undocumented) + left: AssignmentTarget; + // (undocumented) + optional?: false; + // (undocumented) + parent?: Node_2; + // (undocumented) + right: Expression; + // (undocumented) + type: "AssignmentPattern"; + // (undocumented) + typeAnnotation?: null; +} + +// @public (undocumented) +export interface AwaitExpression extends Span { + // (undocumented) + argument: Expression; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "AwaitExpression"; +} + +// @public (undocumented) +export interface BigIntLiteral extends Span { + // (undocumented) + bigint: string; + // (undocumented) + parent?: Node_2; + // (undocumented) + raw: string | null; + // (undocumented) + type: "Literal"; + // (undocumented) + value: bigint; +} + +// @public (undocumented) +export interface BinaryExpression extends Span { + // (undocumented) + left: Expression; + // (undocumented) + operator: BinaryOperator; + // (undocumented) + parent?: Node_2; + // (undocumented) + right: Expression; + // (undocumented) + type: "BinaryExpression"; +} + +// @public (undocumented) +export type BinaryOperator = "==" | "!=" | "===" | "!==" | "<" | "<=" | ">" | ">=" | "+" | "-" | "*" | "/" | "%" | "**" | "<<" | ">>" | ">>>" | "|" | "^" | "&" | "in" | "instanceof"; + +// @public (undocumented) +export interface BindingIdentifier extends Span { + // (undocumented) + decorators?: []; + // (undocumented) + name: string; + // (undocumented) + optional?: false; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "Identifier"; + // (undocumented) + typeAnnotation?: TSTypeAnnotation | null; +} + +// @public (undocumented) +export type BindingPattern = BindingIdentifier | ObjectPattern | ArrayPattern | AssignmentPattern; + +// @public (undocumented) +export interface BindingProperty extends Span { + // (undocumented) + computed: boolean; + // (undocumented) + key: PropertyKey_2; + // (undocumented) + kind: "init"; + // (undocumented) + method: false; + // (undocumented) + optional?: false; + // (undocumented) + parent?: Node_2; + // (undocumented) + shorthand: boolean; + // (undocumented) + type: "Property"; + // (undocumented) + value: BindingPattern; +} + +// @public (undocumented) +export interface BindingRestElement extends Span { + // (undocumented) + argument: BindingPattern; + // (undocumented) + decorators?: []; + // (undocumented) + optional?: false; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "RestElement"; + // (undocumented) + typeAnnotation?: TSTypeAnnotation | null; + // (undocumented) + value?: null; +} + +// @public (undocumented) +export interface BlockStatement extends Span { + // (undocumented) + body: Array; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "BlockStatement"; +} + +// @public (undocumented) +export interface BooleanLiteral extends Span { + // (undocumented) + parent?: Node_2; + // (undocumented) + raw: string | null; + // (undocumented) + type: "Literal"; + // (undocumented) + value: boolean; +} + +// @public (undocumented) +export interface BreakStatement extends Span { + // (undocumented) + label: LabelIdentifier | null; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "BreakStatement"; +} + +// @public (undocumented) +export interface CallExpression extends Span { + // (undocumented) + arguments: Array; + // (undocumented) + callee: Expression; + // (undocumented) + optional: boolean; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "CallExpression"; + // (undocumented) + typeArguments?: TSTypeParameterInstantiation | null; +} + +// @public (undocumented) +export interface CatchClause extends Span { + // (undocumented) + body: BlockStatement; + // (undocumented) + param: BindingPattern | null; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "CatchClause"; +} + +// @public (undocumented) +export type ChainElement = CallExpression | TSNonNullExpression | MemberExpression; + +// @public (undocumented) +export interface ChainExpression extends Span { + // (undocumented) + expression: ChainElement; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "ChainExpression"; +} + +// @public (undocumented) +export interface Class extends Span { + // (undocumented) + abstract?: boolean; + // (undocumented) + body: ClassBody; + // (undocumented) + declare?: boolean; + // (undocumented) + decorators: Array; + // (undocumented) + id: BindingIdentifier | null; + // (undocumented) + implements?: Array; + // (undocumented) + parent?: Node_2; + // (undocumented) + superClass: Expression | null; + // (undocumented) + superTypeArguments?: TSTypeParameterInstantiation | null; + // (undocumented) + type: ClassType; + // (undocumented) + typeParameters?: TSTypeParameterDeclaration | null; +} + +// @public (undocumented) +export interface ClassBody extends Span { + // (undocumented) + body: Array; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "ClassBody"; +} + +// @public (undocumented) +export type ClassElement = StaticBlock | MethodDefinition | PropertyDefinition_2 | AccessorProperty | TSIndexSignature; + +// @public (undocumented) +export type ClassType = "ClassDeclaration" | "ClassExpression"; + +// @public (undocumented) +export interface ComputedMemberExpression extends Span { + // (undocumented) + computed: true; + // (undocumented) + object: Expression; + // (undocumented) + optional: boolean; + // (undocumented) + parent?: Node_2; + // (undocumented) + property: Expression; + // (undocumented) + type: "MemberExpression"; +} + +// @public (undocumented) +export interface ConditionalExpression extends Span { + // (undocumented) + alternate: Expression; + // (undocumented) + consequent: Expression; + // (undocumented) + parent?: Node_2; + // (undocumented) + test: Expression; + // (undocumented) + type: "ConditionalExpression"; +} + +// @public (undocumented) +export interface ContinueStatement extends Span { + // (undocumented) + label: LabelIdentifier | null; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "ContinueStatement"; +} + +// @public (undocumented) +export interface DebuggerStatement extends Span { + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "DebuggerStatement"; +} + +// @public (undocumented) +export type Declaration = VariableDeclaration | Function_2 | Class | TSTypeAliasDeclaration | TSInterfaceDeclaration | TSEnumDeclaration | TSModuleDeclaration | TSGlobalDeclaration | TSImportEqualsDeclaration; + +// @public (undocumented) +export interface Decorator extends Span { + // (undocumented) + expression: Expression; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "Decorator"; +} + +// @public (undocumented) +export interface Directive extends Span { + // (undocumented) + directive: string; + // (undocumented) + expression: StringLiteral; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "ExpressionStatement"; +} + +// @public (undocumented) +export interface DoWhileStatement extends Span { + // (undocumented) + body: Statement; + // (undocumented) + parent?: Node_2; + // (undocumented) + test: Expression; + // (undocumented) + type: "DoWhileStatement"; +} + +// @public (undocumented) +export interface EmptyStatement extends Span { + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "EmptyStatement"; +} + +// @public (undocumented) +export interface ExportAllDeclaration extends Span { + // (undocumented) + attributes: Array; + // (undocumented) + exported: ModuleExportName | null; + // (undocumented) + exportKind?: ImportOrExportKind; + // (undocumented) + parent?: Node_2; + // (undocumented) + source: StringLiteral; + // (undocumented) + type: "ExportAllDeclaration"; +} + +// @public (undocumented) +export interface ExportDefaultDeclaration extends Span { + // (undocumented) + declaration: ExportDefaultDeclarationKind; + // (undocumented) + exportKind?: "value"; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "ExportDefaultDeclaration"; +} + +// @public (undocumented) +export type ExportDefaultDeclarationKind = Function_2 | Class | TSInterfaceDeclaration | Expression; + +// @public (undocumented) +export interface ExportNamedDeclaration extends Span { + // (undocumented) + attributes: Array; + // (undocumented) + declaration: Declaration | null; + // (undocumented) + exportKind?: ImportOrExportKind; + // (undocumented) + parent?: Node_2; + // (undocumented) + source: StringLiteral | null; + // (undocumented) + specifiers: Array; + // (undocumented) + type: "ExportNamedDeclaration"; +} + +// @public (undocumented) +export interface ExportSpecifier extends Span { + // (undocumented) + exported: ModuleExportName; + // (undocumented) + exportKind?: ImportOrExportKind; + // (undocumented) + local: ModuleExportName; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "ExportSpecifier"; +} + +// @public (undocumented) +export type Expression = BooleanLiteral | NullLiteral | NumericLiteral | BigIntLiteral | RegExpLiteral | StringLiteral | TemplateLiteral | IdentifierReference | MetaProperty | Super | ArrayExpression | ArrowFunctionExpression | AssignmentExpression | AwaitExpression | BinaryExpression | CallExpression | ChainExpression | Class | ConditionalExpression | Function_2 | ImportExpression | LogicalExpression | NewExpression | ObjectExpression | ParenthesizedExpression | SequenceExpression | TaggedTemplateExpression | ThisExpression | UnaryExpression | UpdateExpression | YieldExpression | PrivateInExpression | JSXElement | JSXFragment | TSAsExpression | TSSatisfiesExpression | TSTypeAssertion | TSNonNullExpression | TSInstantiationExpression | V8IntrinsicExpression | MemberExpression; + +// @public (undocumented) +export interface ExpressionStatement extends Span { + // (undocumented) + directive?: string | null; + // (undocumented) + expression: Expression; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "ExpressionStatement"; +} + +// @public (undocumented) +export interface ForInStatement extends Span { + // (undocumented) + body: Statement; + // (undocumented) + left: ForStatementLeft; + // (undocumented) + parent?: Node_2; + // (undocumented) + right: Expression; + // (undocumented) + type: "ForInStatement"; +} + +// @public (undocumented) +export type FormalParameter = { + decorators?: Array; +} & BindingPattern; + +// @public (undocumented) +export interface FormalParameterRest extends Span { + // (undocumented) + argument: BindingPattern; + // (undocumented) + decorators?: []; + // (undocumented) + optional?: boolean; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "RestElement"; + // (undocumented) + typeAnnotation?: TSTypeAnnotation | null; + // (undocumented) + value?: null; +} + +// @public (undocumented) +export interface ForOfStatement extends Span { + // (undocumented) + await: boolean; + // (undocumented) + body: Statement; + // (undocumented) + left: ForStatementLeft; + // (undocumented) + parent?: Node_2; + // (undocumented) + right: Expression; + // (undocumented) + type: "ForOfStatement"; +} + +// @public (undocumented) +export interface ForStatement extends Span { + // (undocumented) + body: Statement; + // (undocumented) + init: ForStatementInit | null; + // (undocumented) + parent?: Node_2; + // (undocumented) + test: Expression | null; + // (undocumented) + type: "ForStatement"; + // (undocumented) + update: Expression | null; +} + +// @public (undocumented) +export type ForStatementInit = VariableDeclaration | Expression; + +// @public (undocumented) +export type ForStatementLeft = VariableDeclaration | AssignmentTarget; + +// @public (undocumented) +interface Function_2 extends Span { + // (undocumented) + async: boolean; + // (undocumented) + body: FunctionBody | null; + // (undocumented) + declare?: boolean; + // (undocumented) + expression: false; + // (undocumented) + generator: boolean; + // (undocumented) + id: BindingIdentifier | null; + // (undocumented) + params: ParamPattern[]; + // (undocumented) + parent?: Node_2; + // (undocumented) + returnType?: TSTypeAnnotation | null; + // (undocumented) + type: FunctionType; + // (undocumented) + typeParameters?: TSTypeParameterDeclaration | null; +} +export { Function_2 as Function } + +// @public (undocumented) +export interface FunctionBody extends Span { + // (undocumented) + body: Array; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "BlockStatement"; +} + +// @public (undocumented) +export type FunctionType = "FunctionDeclaration" | "FunctionExpression" | "TSDeclareFunction" | "TSEmptyBodyFunctionExpression"; + +// @public (undocumented) +export interface Hashbang extends Span { + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "Hashbang"; + // (undocumented) + value: string; +} + +// @public (undocumented) +export interface IdentifierName extends Span { + // (undocumented) + decorators?: []; + // (undocumented) + name: string; + // (undocumented) + optional?: false; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "Identifier"; + // (undocumented) + typeAnnotation?: null; +} + +// @public (undocumented) +export interface IdentifierReference extends Span { + // (undocumented) + decorators?: []; + // (undocumented) + name: string; + // (undocumented) + optional?: false; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "Identifier"; + // (undocumented) + typeAnnotation?: null; +} + +// @public (undocumented) +export interface IfStatement extends Span { + // (undocumented) + alternate: Statement | null; + // (undocumented) + consequent: Statement; + // (undocumented) + parent?: Node_2; + // (undocumented) + test: Expression; + // (undocumented) + type: "IfStatement"; +} + +// @public (undocumented) +export interface ImportAttribute extends Span { + // (undocumented) + key: ImportAttributeKey; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "ImportAttribute"; + // (undocumented) + value: StringLiteral; +} + +// @public (undocumented) +export type ImportAttributeKey = IdentifierName | StringLiteral; + +// @public (undocumented) +export interface ImportDeclaration extends Span { + // (undocumented) + attributes: Array; + // (undocumented) + importKind?: ImportOrExportKind; + // (undocumented) + parent?: Node_2; + // (undocumented) + phase: ImportPhase | null; + // (undocumented) + source: StringLiteral; + // (undocumented) + specifiers: Array; + // (undocumented) + type: "ImportDeclaration"; +} + +// @public (undocumented) +export type ImportDeclarationSpecifier = ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier; + +// @public (undocumented) +export interface ImportDefaultSpecifier extends Span { + // (undocumented) + local: BindingIdentifier; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "ImportDefaultSpecifier"; +} + +// @public (undocumented) +export interface ImportExpression extends Span { + // (undocumented) + options: Expression | null; + // (undocumented) + parent?: Node_2; + // (undocumented) + phase: ImportPhase | null; + // (undocumented) + source: Expression; + // (undocumented) + type: "ImportExpression"; +} + +// @public (undocumented) +export interface ImportNamespaceSpecifier extends Span { + // (undocumented) + local: BindingIdentifier; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "ImportNamespaceSpecifier"; +} + +// @public (undocumented) +export type ImportOrExportKind = "value" | "type"; + +// @public (undocumented) +export type ImportPhase = "source" | "defer"; + +// @public (undocumented) +export interface ImportSpecifier extends Span { + // (undocumented) + imported: ModuleExportName; + // (undocumented) + importKind?: ImportOrExportKind; + // (undocumented) + local: BindingIdentifier; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "ImportSpecifier"; +} + +// @public (undocumented) +export interface JSDocNonNullableType extends Span { + // (undocumented) + parent?: Node_2; + // (undocumented) + postfix: boolean; + // (undocumented) + type: "TSJSDocNonNullableType"; + // (undocumented) + typeAnnotation: TSType; +} + +// @public (undocumented) +export interface JSDocNullableType extends Span { + // (undocumented) + parent?: Node_2; + // (undocumented) + postfix: boolean; + // (undocumented) + type: "TSJSDocNullableType"; + // (undocumented) + typeAnnotation: TSType; +} + +// @public (undocumented) +export interface JSDocUnknownType extends Span { + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSJSDocUnknownType"; +} + +// @public (undocumented) +export interface JSXAttribute extends Span { + // (undocumented) + name: JSXAttributeName; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "JSXAttribute"; + // (undocumented) + value: JSXAttributeValue | null; +} + +// @public (undocumented) +export type JSXAttributeItem = JSXAttribute | JSXSpreadAttribute; + +// @public (undocumented) +export type JSXAttributeName = JSXIdentifier | JSXNamespacedName; + +// @public (undocumented) +export type JSXAttributeValue = StringLiteral | JSXExpressionContainer | JSXElement | JSXFragment; + +// @public (undocumented) +export type JSXChild = JSXText | JSXElement | JSXFragment | JSXExpressionContainer | JSXSpreadChild; + +// @public (undocumented) +export interface JSXClosingElement extends Span { + // (undocumented) + name: JSXElementName; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "JSXClosingElement"; +} + +// @public (undocumented) +export interface JSXClosingFragment extends Span { + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "JSXClosingFragment"; +} + +// @public (undocumented) +export interface JSXElement extends Span { + // (undocumented) + children: Array; + // (undocumented) + closingElement: JSXClosingElement | null; + // (undocumented) + openingElement: JSXOpeningElement; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "JSXElement"; +} + +// @public (undocumented) +export type JSXElementName = JSXIdentifier | JSXNamespacedName | JSXMemberExpression; + +// @public (undocumented) +export interface JSXEmptyExpression extends Span { + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "JSXEmptyExpression"; +} + +// @public (undocumented) +export type JSXExpression = JSXEmptyExpression | Expression; + +// @public (undocumented) +export interface JSXExpressionContainer extends Span { + // (undocumented) + expression: JSXExpression; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "JSXExpressionContainer"; +} + +// @public (undocumented) +export interface JSXFragment extends Span { + // (undocumented) + children: Array; + // (undocumented) + closingFragment: JSXClosingFragment; + // (undocumented) + openingFragment: JSXOpeningFragment; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "JSXFragment"; +} + +// @public (undocumented) +export interface JSXIdentifier extends Span { + // (undocumented) + name: string; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "JSXIdentifier"; +} + +// @public (undocumented) +export interface JSXMemberExpression extends Span { + // (undocumented) + object: JSXMemberExpressionObject; + // (undocumented) + parent?: Node_2; + // (undocumented) + property: JSXIdentifier; + // (undocumented) + type: "JSXMemberExpression"; +} + +// @public (undocumented) +export type JSXMemberExpressionObject = JSXIdentifier | JSXMemberExpression; + +// @public (undocumented) +export interface JSXNamespacedName extends Span { + // (undocumented) + name: JSXIdentifier; + // (undocumented) + namespace: JSXIdentifier; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "JSXNamespacedName"; +} + +// @public (undocumented) +export interface JSXOpeningElement extends Span { + // (undocumented) + attributes: Array; + // (undocumented) + name: JSXElementName; + // (undocumented) + parent?: Node_2; + // (undocumented) + selfClosing: boolean; + // (undocumented) + type: "JSXOpeningElement"; + // (undocumented) + typeArguments?: TSTypeParameterInstantiation | null; +} + +// @public (undocumented) +export interface JSXOpeningFragment extends Span { + // (undocumented) + attributes?: []; + // (undocumented) + parent?: Node_2; + // (undocumented) + selfClosing?: false; + // (undocumented) + type: "JSXOpeningFragment"; +} + +// @public (undocumented) +export interface JSXSpreadAttribute extends Span { + // (undocumented) + argument: Expression; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "JSXSpreadAttribute"; +} + +// @public (undocumented) +export interface JSXSpreadChild extends Span { + // (undocumented) + expression: Expression; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "JSXSpreadChild"; +} + +// @public (undocumented) +export interface JSXText extends Span { + // (undocumented) + parent?: Node_2; + // (undocumented) + raw: string | null; + // (undocumented) + type: "JSXText"; + // (undocumented) + value: string; +} + +// @public (undocumented) +export interface LabeledStatement extends Span { + // (undocumented) + body: Statement; + // (undocumented) + label: LabelIdentifier; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "LabeledStatement"; +} + +// @public (undocumented) +export interface LabelIdentifier extends Span { + // (undocumented) + decorators?: []; + // (undocumented) + name: string; + // (undocumented) + optional?: false; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "Identifier"; + // (undocumented) + typeAnnotation?: null; +} + +// @public (undocumented) +export interface LogicalExpression extends Span { + // (undocumented) + left: Expression; + // (undocumented) + operator: LogicalOperator; + // (undocumented) + parent?: Node_2; + // (undocumented) + right: Expression; + // (undocumented) + type: "LogicalExpression"; +} + +// @public (undocumented) +export type LogicalOperator = "||" | "&&" | "??"; + +// @public (undocumented) +export type MemberExpression = ComputedMemberExpression | StaticMemberExpression | PrivateFieldExpression; + +// @public (undocumented) +export interface MetaProperty extends Span { + // (undocumented) + meta: IdentifierName; + // (undocumented) + parent?: Node_2; + // (undocumented) + property: IdentifierName; + // (undocumented) + type: "MetaProperty"; +} + +// @public (undocumented) +export interface MethodDefinition extends Span { + // (undocumented) + accessibility?: TSAccessibility | null; + // (undocumented) + computed: boolean; + // (undocumented) + decorators: Array; + // (undocumented) + key: PropertyKey_2; + // (undocumented) + kind: MethodDefinitionKind; + // (undocumented) + optional?: boolean; + // (undocumented) + override?: boolean; + // (undocumented) + parent?: Node_2; + // (undocumented) + static: boolean; + // (undocumented) + type: MethodDefinitionType; + // (undocumented) + value: Function_2; +} + +// @public (undocumented) +export type MethodDefinitionKind = "constructor" | "method" | "get" | "set"; + +// @public (undocumented) +export type MethodDefinitionType = "MethodDefinition" | "TSAbstractMethodDefinition"; + +// @public (undocumented) +export type ModuleDeclaration = ImportDeclaration | ExportAllDeclaration | ExportDefaultDeclaration | ExportNamedDeclaration | TSExportAssignment | TSNamespaceExportDeclaration; + +// @public (undocumented) +export type ModuleExportName = IdentifierName | IdentifierReference | StringLiteral; + +// @public (undocumented) +export type ModuleKind = "script" | "module" | "commonjs"; + +// @public (undocumented) +export interface NewExpression extends Span { + // (undocumented) + arguments: Array; + // (undocumented) + callee: Expression; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "NewExpression"; + // (undocumented) + typeArguments?: TSTypeParameterInstantiation | null; +} + +// @public (undocumented) +type Node_2 = Program | IdentifierName | IdentifierReference | BindingIdentifier | LabelIdentifier | ThisExpression | ArrayExpression | ObjectExpression | ObjectProperty | TemplateLiteral | TaggedTemplateExpression | TemplateElement | ComputedMemberExpression | StaticMemberExpression | PrivateFieldExpression | CallExpression | NewExpression | MetaProperty | SpreadElement | UpdateExpression | UnaryExpression | BinaryExpression | PrivateInExpression | LogicalExpression | ConditionalExpression | AssignmentExpression | ArrayAssignmentTarget | ObjectAssignmentTarget | AssignmentTargetRest | AssignmentTargetWithDefault | AssignmentTargetPropertyIdentifier | AssignmentTargetPropertyProperty | SequenceExpression | Super | AwaitExpression | ChainExpression | ParenthesizedExpression | Directive | Hashbang | BlockStatement | VariableDeclaration | VariableDeclarator | EmptyStatement | ExpressionStatement | IfStatement | DoWhileStatement | WhileStatement | ForStatement | ForInStatement | ForOfStatement | ContinueStatement | BreakStatement | ReturnStatement | WithStatement | SwitchStatement | SwitchCase | LabeledStatement | ThrowStatement | TryStatement | CatchClause | DebuggerStatement | AssignmentPattern | ObjectPattern | BindingProperty | ArrayPattern | BindingRestElement | Function_2 | FunctionBody | ArrowFunctionExpression | YieldExpression | Class | ClassBody | MethodDefinition | PropertyDefinition_2 | PrivateIdentifier | StaticBlock | AccessorProperty | ImportExpression | ImportDeclaration | ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier | ImportAttribute | ExportNamedDeclaration | ExportDefaultDeclaration | ExportAllDeclaration | ExportSpecifier | V8IntrinsicExpression | BooleanLiteral | NullLiteral | NumericLiteral | StringLiteral | BigIntLiteral | RegExpLiteral | JSXElement | JSXOpeningElement | JSXClosingElement | JSXFragment | JSXOpeningFragment | JSXClosingFragment | JSXNamespacedName | JSXMemberExpression | JSXExpressionContainer | JSXEmptyExpression | JSXAttribute | JSXSpreadAttribute | JSXIdentifier | JSXSpreadChild | JSXText | TSThisParameter | TSEnumDeclaration | TSEnumBody | TSEnumMember | TSTypeAnnotation | TSLiteralType | TSConditionalType | TSUnionType | TSIntersectionType | TSParenthesizedType | TSTypeOperator | TSArrayType | TSIndexedAccessType | TSTupleType | TSNamedTupleMember | TSOptionalType | TSRestType | TSAnyKeyword | TSStringKeyword | TSBooleanKeyword | TSNumberKeyword | TSNeverKeyword | TSIntrinsicKeyword | TSUnknownKeyword | TSNullKeyword | TSUndefinedKeyword | TSVoidKeyword | TSSymbolKeyword | TSThisType | TSObjectKeyword | TSBigIntKeyword | TSTypeReference | TSQualifiedName | TSTypeParameterInstantiation | TSTypeParameter | TSTypeParameterDeclaration | TSTypeAliasDeclaration | TSClassImplements | TSInterfaceDeclaration | TSInterfaceBody | TSPropertySignature | TSIndexSignature | TSCallSignatureDeclaration | TSMethodSignature | TSConstructSignatureDeclaration | TSIndexSignatureName | TSInterfaceHeritage | TSTypePredicate | TSModuleDeclaration | TSGlobalDeclaration | TSModuleBlock | TSTypeLiteral | TSInferType | TSTypeQuery | TSImportType | TSImportTypeQualifiedName | TSFunctionType | TSConstructorType | TSMappedType | TSTemplateLiteralType | TSAsExpression | TSSatisfiesExpression | TSTypeAssertion | TSImportEqualsDeclaration | TSExternalModuleReference | TSNonNullExpression | Decorator | TSExportAssignment | TSNamespaceExportDeclaration | TSInstantiationExpression | JSDocNullableType | JSDocNonNullableType | JSDocUnknownType | ParamPattern; +export { Node_2 as Node } + +// @public (undocumented) +export interface NodePath { + // (undocumented) + readonly ancestors: readonly unknown[]; + // (undocumented) + readonly node: unknown; +} + +// @public (undocumented) +export interface NullLiteral extends Span { + // (undocumented) + parent?: Node_2; + // (undocumented) + raw: "null" | null; + // (undocumented) + type: "Literal"; + // (undocumented) + value: null; +} + +// @public (undocumented) +export interface NumericLiteral extends Span { + // (undocumented) + parent?: Node_2; + // (undocumented) + raw: string | null; + // (undocumented) + type: "Literal"; + // (undocumented) + value: number; +} + +// @public (undocumented) +export interface ObjectAssignmentTarget extends Span { + // (undocumented) + decorators?: []; + // (undocumented) + optional?: false; + // (undocumented) + parent?: Node_2; + // (undocumented) + properties: Array; + // (undocumented) + type: "ObjectPattern"; + // (undocumented) + typeAnnotation?: null; +} + +// @public (undocumented) +export interface ObjectExpression extends Span { + // (undocumented) + parent?: Node_2; + // (undocumented) + properties: Array; + // (undocumented) + type: "ObjectExpression"; +} + +// @public (undocumented) +export interface ObjectPattern extends Span { + // (undocumented) + decorators?: []; + // (undocumented) + optional?: false; + // (undocumented) + parent?: Node_2; + // (undocumented) + properties: Array; + // (undocumented) + type: "ObjectPattern"; + // (undocumented) + typeAnnotation?: TSTypeAnnotation | null; +} + +// @public (undocumented) +export interface ObjectProperty extends Span { + // (undocumented) + computed: boolean; + // (undocumented) + key: PropertyKey_2; + // (undocumented) + kind: PropertyKind; + // (undocumented) + method: boolean; + // (undocumented) + optional?: false; + // (undocumented) + parent?: Node_2; + // (undocumented) + shorthand: boolean; + // (undocumented) + type: "Property"; + // (undocumented) + value: Expression; +} + +// @public (undocumented) +export type ObjectPropertyKind = ObjectProperty | SpreadElement; + +// @public (undocumented) +export type ParamPattern = FormalParameter | TSParameterProperty | FormalParameterRest; + +// @public (undocumented) +export interface ParenthesizedExpression extends Span { + // (undocumented) + expression: Expression; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "ParenthesizedExpression"; +} + +// @public (undocumented) +export interface PlainIgnorer { + // (undocumented) + readonly name: string; + // (undocumented) + shouldIgnore(path: NodePath): string | undefined; +} + +// @public (undocumented) +export interface PrivateFieldExpression extends Span { + // (undocumented) + computed: false; + // (undocumented) + object: Expression; + // (undocumented) + optional: boolean; + // (undocumented) + parent?: Node_2; + // (undocumented) + property: PrivateIdentifier; + // (undocumented) + type: "MemberExpression"; +} + +// @public (undocumented) +export interface PrivateIdentifier extends Span { + // (undocumented) + name: string; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "PrivateIdentifier"; +} + +// @public (undocumented) +export interface PrivateInExpression extends Span { + // (undocumented) + left: PrivateIdentifier; + // (undocumented) + operator: "in"; + // (undocumented) + parent?: Node_2; + // (undocumented) + right: Expression; + // (undocumented) + type: "BinaryExpression"; +} + +// @public (undocumented) +export interface Program extends Span { + // (undocumented) + body: Array; + // (undocumented) + hashbang: Hashbang | null; + // (undocumented) + parent?: null; + // (undocumented) + sourceType: ModuleKind; + // (undocumented) + type: "Program"; +} + +// @public (undocumented) +interface PropertyDefinition_2 extends Span { + // (undocumented) + accessibility?: TSAccessibility | null; + // (undocumented) + computed: boolean; + // (undocumented) + declare?: boolean; + // (undocumented) + decorators: Array; + // (undocumented) + definite?: boolean; + // (undocumented) + key: PropertyKey_2; + // (undocumented) + optional?: boolean; + // (undocumented) + override?: boolean; + // (undocumented) + parent?: Node_2; + // (undocumented) + readonly?: boolean; + // (undocumented) + static: boolean; + // (undocumented) + type: PropertyDefinitionType; + // (undocumented) + typeAnnotation?: TSTypeAnnotation | null; + // (undocumented) + value: Expression | null; +} +export { PropertyDefinition_2 as PropertyDefinition } + +// @public (undocumented) +export type PropertyDefinitionType = "PropertyDefinition" | "TSAbstractPropertyDefinition"; + +// @public (undocumented) +type PropertyKey_2 = IdentifierName | PrivateIdentifier | Expression; +export { PropertyKey_2 as PropertyKey } + +// @public (undocumented) +export type PropertyKind = "init" | "get" | "set"; + +// @public (undocumented) +export interface RegExpLiteral extends Span { + // (undocumented) + parent?: Node_2; + // (undocumented) + raw: string | null; + // (undocumented) + regex: { + pattern: string; + flags: string; + }; + // (undocumented) + type: "Literal"; + // (undocumented) + value: RegExp | null; +} + +// @public (undocumented) +export interface ReturnStatement extends Span { + // (undocumented) + argument: Expression | null; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "ReturnStatement"; +} + +// @public (undocumented) +export interface SequenceExpression extends Span { + // (undocumented) + expressions: Array; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "SequenceExpression"; +} + +// @public (undocumented) +export type SimpleAssignmentTarget = IdentifierReference | TSAsExpression | TSSatisfiesExpression | TSNonNullExpression | TSTypeAssertion | MemberExpression; + +// @public (undocumented) +export interface Span { + // (undocumented) + end: number; + // (undocumented) + range?: [number, number]; + // (undocumented) + start: number; +} + +// @public (undocumented) +export interface SpreadElement extends Span { + // (undocumented) + argument: Expression; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "SpreadElement"; +} + +// @public (undocumented) +export type Statement = BlockStatement | BreakStatement | ContinueStatement | DebuggerStatement | DoWhileStatement | EmptyStatement | ExpressionStatement | ForInStatement | ForOfStatement | ForStatement | IfStatement | LabeledStatement | ReturnStatement | SwitchStatement | ThrowStatement | TryStatement | WhileStatement | WithStatement | Declaration | ModuleDeclaration; + +// @public (undocumented) +export interface StaticBlock extends Span { + // (undocumented) + body: Array; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "StaticBlock"; +} + +// @public (undocumented) +export interface StaticMemberExpression extends Span { + // (undocumented) + computed: false; + // (undocumented) + object: Expression; + // (undocumented) + optional: boolean; + // (undocumented) + parent?: Node_2; + // (undocumented) + property: IdentifierName; + // (undocumented) + type: "MemberExpression"; +} + +// @public (undocumented) +export interface StringLiteral extends Span { + // (undocumented) + parent?: Node_2; + // (undocumented) + raw: string | null; + // (undocumented) + type: "Literal"; + // (undocumented) + value: string; +} + +// @public (undocumented) +export interface Super extends Span { + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "Super"; +} + +// @public (undocumented) +export interface SwitchCase extends Span { + // (undocumented) + consequent: Array; + // (undocumented) + parent?: Node_2; + // (undocumented) + test: Expression | null; + // (undocumented) + type: "SwitchCase"; +} + +// @public (undocumented) +export interface SwitchStatement extends Span { + // (undocumented) + cases: Array; + // (undocumented) + discriminant: Expression; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "SwitchStatement"; +} + +// @public (undocumented) +export interface TaggedTemplateExpression extends Span { + // (undocumented) + parent?: Node_2; + // (undocumented) + quasi: TemplateLiteral; + // (undocumented) + tag: Expression; + // (undocumented) + type: "TaggedTemplateExpression"; + // (undocumented) + typeArguments?: TSTypeParameterInstantiation | null; +} + +// @public (undocumented) +export interface TemplateElement extends Span { + // (undocumented) + parent?: Node_2; + // (undocumented) + tail: boolean; + // (undocumented) + type: "TemplateElement"; + // (undocumented) + value: TemplateElementValue; +} + +// @public (undocumented) +export interface TemplateElementValue { + // (undocumented) + cooked: string | null; + // (undocumented) + raw: string; +} + +// @public (undocumented) +export interface TemplateLiteral extends Span { + // (undocumented) + expressions: Array; + // (undocumented) + parent?: Node_2; + // (undocumented) + quasis: Array; + // (undocumented) + type: "TemplateLiteral"; +} + +// @public (undocumented) +export interface ThisExpression extends Span { + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "ThisExpression"; +} + +// @public (undocumented) +export interface ThrowStatement extends Span { + // (undocumented) + argument: Expression; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "ThrowStatement"; +} + +// @public (undocumented) +export interface TryStatement extends Span { + // (undocumented) + block: BlockStatement; + // (undocumented) + finalizer: BlockStatement | null; + // (undocumented) + handler: CatchClause | null; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TryStatement"; +} + +// @public (undocumented) +export type TSAccessibility = "private" | "protected" | "public"; + +// @public (undocumented) +export interface TSAnyKeyword extends Span { + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSAnyKeyword"; +} + +// @public (undocumented) +export interface TSArrayType extends Span { + // (undocumented) + elementType: TSType; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSArrayType"; +} + +// @public (undocumented) +export interface TSAsExpression extends Span { + // (undocumented) + expression: Expression; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSAsExpression"; + // (undocumented) + typeAnnotation: TSType; +} + +// @public (undocumented) +export interface TSBigIntKeyword extends Span { + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSBigIntKeyword"; +} + +// @public (undocumented) +export interface TSBooleanKeyword extends Span { + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSBooleanKeyword"; +} + +// @public (undocumented) +export interface TSCallSignatureDeclaration extends Span { + // (undocumented) + params: ParamPattern[]; + // (undocumented) + parent?: Node_2; + // (undocumented) + returnType: TSTypeAnnotation | null; + // (undocumented) + type: "TSCallSignatureDeclaration"; + // (undocumented) + typeParameters: TSTypeParameterDeclaration | null; +} + +// @public (undocumented) +export interface TSClassImplements extends Span { + // (undocumented) + expression: IdentifierReference | ThisExpression | MemberExpression; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSClassImplements"; + // (undocumented) + typeArguments: TSTypeParameterInstantiation | null; +} + +// @public (undocumented) +export interface TSConditionalType extends Span { + // (undocumented) + checkType: TSType; + // (undocumented) + extendsType: TSType; + // (undocumented) + falseType: TSType; + // (undocumented) + parent?: Node_2; + // (undocumented) + trueType: TSType; + // (undocumented) + type: "TSConditionalType"; +} + +// @public (undocumented) +export interface TSConstructorType extends Span { + // (undocumented) + abstract: boolean; + // (undocumented) + params: ParamPattern[]; + // (undocumented) + parent?: Node_2; + // (undocumented) + returnType: TSTypeAnnotation; + // (undocumented) + type: "TSConstructorType"; + // (undocumented) + typeParameters: TSTypeParameterDeclaration | null; +} + +// @public (undocumented) +export interface TSConstructSignatureDeclaration extends Span { + // (undocumented) + params: ParamPattern[]; + // (undocumented) + parent?: Node_2; + // (undocumented) + returnType: TSTypeAnnotation | null; + // (undocumented) + type: "TSConstructSignatureDeclaration"; + // (undocumented) + typeParameters: TSTypeParameterDeclaration | null; +} + +// @public (undocumented) +export interface TSEnumBody extends Span { + // (undocumented) + members: Array; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSEnumBody"; +} + +// @public (undocumented) +export interface TSEnumDeclaration extends Span { + // (undocumented) + body: TSEnumBody; + // (undocumented) + const: boolean; + // (undocumented) + declare: boolean; + // (undocumented) + id: BindingIdentifier; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSEnumDeclaration"; +} + +// @public (undocumented) +export interface TSEnumMember extends Span { + // (undocumented) + computed: boolean; + // (undocumented) + id: TSEnumMemberName; + // (undocumented) + initializer: Expression | null; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSEnumMember"; +} + +// @public (undocumented) +export type TSEnumMemberName = IdentifierName | StringLiteral | TemplateLiteral; + +// @public (undocumented) +export interface TSExportAssignment extends Span { + // (undocumented) + expression: Expression; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSExportAssignment"; +} + +// @public (undocumented) +export interface TSExternalModuleReference extends Span { + // (undocumented) + expression: StringLiteral; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSExternalModuleReference"; +} + +// @public (undocumented) +export interface TSFunctionType extends Span { + // (undocumented) + params: ParamPattern[]; + // (undocumented) + parent?: Node_2; + // (undocumented) + returnType: TSTypeAnnotation; + // (undocumented) + type: "TSFunctionType"; + // (undocumented) + typeParameters: TSTypeParameterDeclaration | null; +} + +// @public (undocumented) +export interface TSGlobalDeclaration extends Span { + // (undocumented) + body: TSModuleBlock; + // (undocumented) + declare: boolean; + // (undocumented) + global: true; + // (undocumented) + id: IdentifierName; + // (undocumented) + kind: "global"; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSModuleDeclaration"; +} + +// @public (undocumented) +export interface TSImportEqualsDeclaration extends Span { + // (undocumented) + id: BindingIdentifier; + // (undocumented) + importKind: ImportOrExportKind; + // (undocumented) + moduleReference: TSModuleReference; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSImportEqualsDeclaration"; +} + +// @public (undocumented) +export interface TSImportType extends Span { + // (undocumented) + options: ObjectExpression | null; + // (undocumented) + parent?: Node_2; + // (undocumented) + qualifier: TSImportTypeQualifier | null; + // (undocumented) + source: StringLiteral; + // (undocumented) + type: "TSImportType"; + // (undocumented) + typeArguments: TSTypeParameterInstantiation | null; +} + +// @public (undocumented) +export interface TSImportTypeQualifiedName extends Span { + // (undocumented) + left: TSImportTypeQualifier; + // (undocumented) + parent?: Node_2; + // (undocumented) + right: IdentifierName; + // (undocumented) + type: "TSQualifiedName"; +} + +// @public (undocumented) +export type TSImportTypeQualifier = IdentifierName | TSImportTypeQualifiedName; + +// @public (undocumented) +export interface TSIndexedAccessType extends Span { + // (undocumented) + indexType: TSType; + // (undocumented) + objectType: TSType; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSIndexedAccessType"; +} + +// @public (undocumented) +export interface TSIndexSignature extends Span { + // (undocumented) + accessibility: null; + // (undocumented) + parameters: Array; + // (undocumented) + parent?: Node_2; + // (undocumented) + readonly: boolean; + // (undocumented) + static: boolean; + // (undocumented) + type: "TSIndexSignature"; + // (undocumented) + typeAnnotation: TSTypeAnnotation; +} + +// @public (undocumented) +export interface TSIndexSignatureName extends Span { + // (undocumented) + decorators: []; + // (undocumented) + name: string; + // (undocumented) + optional: false; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "Identifier"; + // (undocumented) + typeAnnotation: TSTypeAnnotation; +} + +// @public (undocumented) +export interface TSInferType extends Span { + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSInferType"; + // (undocumented) + typeParameter: TSTypeParameter; +} + +// @public (undocumented) +export interface TSInstantiationExpression extends Span { + // (undocumented) + expression: Expression; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSInstantiationExpression"; + // (undocumented) + typeArguments: TSTypeParameterInstantiation; +} + +// @public (undocumented) +export interface TSInterfaceBody extends Span { + // (undocumented) + body: Array; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSInterfaceBody"; +} + +// @public (undocumented) +export interface TSInterfaceDeclaration extends Span { + // (undocumented) + body: TSInterfaceBody; + // (undocumented) + declare: boolean; + // (undocumented) + extends: Array; + // (undocumented) + id: BindingIdentifier; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSInterfaceDeclaration"; + // (undocumented) + typeParameters: TSTypeParameterDeclaration | null; +} + +// @public (undocumented) +export interface TSInterfaceHeritage extends Span { + // (undocumented) + expression: Expression; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSInterfaceHeritage"; + // (undocumented) + typeArguments: TSTypeParameterInstantiation | null; +} + +// @public (undocumented) +export interface TSIntersectionType extends Span { + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSIntersectionType"; + // (undocumented) + types: Array; +} + +// @public (undocumented) +export interface TSIntrinsicKeyword extends Span { + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSIntrinsicKeyword"; +} + +// @public (undocumented) +export type TSLiteral = BooleanLiteral | NumericLiteral | BigIntLiteral | StringLiteral | TemplateLiteral | UnaryExpression; + +// @public (undocumented) +export interface TSLiteralType extends Span { + // (undocumented) + literal: TSLiteral; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSLiteralType"; +} + +// @public (undocumented) +export interface TSMappedType extends Span { + // (undocumented) + constraint: TSType; + // (undocumented) + key: BindingIdentifier; + // (undocumented) + nameType: TSType | null; + // (undocumented) + optional: TSMappedTypeModifierOperator | false; + // (undocumented) + parent?: Node_2; + // (undocumented) + readonly: TSMappedTypeModifierOperator | null; + // (undocumented) + type: "TSMappedType"; + // (undocumented) + typeAnnotation: TSType | null; +} + +// @public (undocumented) +export type TSMappedTypeModifierOperator = true | "+" | "-"; + +// @public (undocumented) +export interface TSMethodSignature extends Span { + // (undocumented) + accessibility: null; + // (undocumented) + computed: boolean; + // (undocumented) + key: PropertyKey_2; + // (undocumented) + kind: TSMethodSignatureKind; + // (undocumented) + optional: boolean; + // (undocumented) + params: ParamPattern[]; + // (undocumented) + parent?: Node_2; + // (undocumented) + readonly: false; + // (undocumented) + returnType: TSTypeAnnotation | null; + // (undocumented) + static: false; + // (undocumented) + type: "TSMethodSignature"; + // (undocumented) + typeParameters: TSTypeParameterDeclaration | null; +} + +// @public (undocumented) +export type TSMethodSignatureKind = "method" | "get" | "set"; + +// @public (undocumented) +export interface TSModuleBlock extends Span { + // (undocumented) + body: Array; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSModuleBlock"; +} + +// @public (undocumented) +export interface TSModuleDeclaration extends Span { + // (undocumented) + body: TSModuleBlock | null; + // (undocumented) + declare: boolean; + // (undocumented) + global: false; + // (undocumented) + id: BindingIdentifier | StringLiteral | TSQualifiedName; + // (undocumented) + kind: TSModuleDeclarationKind; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSModuleDeclaration"; +} + +// @public (undocumented) +export type TSModuleDeclarationKind = "module" | "namespace"; + +// @public (undocumented) +export type TSModuleReference = TSExternalModuleReference | IdentifierReference | TSQualifiedName; + +// @public (undocumented) +export interface TSNamedTupleMember extends Span { + // (undocumented) + elementType: TSTupleElement; + // (undocumented) + label: IdentifierName; + // (undocumented) + optional: boolean; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSNamedTupleMember"; +} + +// @public (undocumented) +export interface TSNamespaceExportDeclaration extends Span { + // (undocumented) + id: IdentifierName; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSNamespaceExportDeclaration"; +} + +// @public (undocumented) +export interface TSNeverKeyword extends Span { + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSNeverKeyword"; +} + +// @public (undocumented) +export interface TSNonNullExpression extends Span { + // (undocumented) + expression: Expression; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSNonNullExpression"; +} + +// @public (undocumented) +export interface TSNullKeyword extends Span { + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSNullKeyword"; +} + +// @public (undocumented) +export interface TSNumberKeyword extends Span { + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSNumberKeyword"; +} + +// @public (undocumented) +export interface TSObjectKeyword extends Span { + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSObjectKeyword"; +} + +// @public (undocumented) +export interface TSOptionalType extends Span { + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSOptionalType"; + // (undocumented) + typeAnnotation: TSType; +} + +// @public (undocumented) +export interface TSParameterProperty extends Span { + // (undocumented) + accessibility: TSAccessibility | null; + // (undocumented) + decorators: Array; + // (undocumented) + override: boolean; + // (undocumented) + parameter: FormalParameter; + // (undocumented) + parent?: Node_2; + // (undocumented) + readonly: boolean; + // (undocumented) + static: boolean; + // (undocumented) + type: "TSParameterProperty"; +} + +// @public (undocumented) +export interface TSParenthesizedType extends Span { + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSParenthesizedType"; + // (undocumented) + typeAnnotation: TSType; +} + +// @public (undocumented) +export interface TSPropertySignature extends Span { + // (undocumented) + accessibility: null; + // (undocumented) + computed: boolean; + // (undocumented) + key: PropertyKey_2; + // (undocumented) + optional: boolean; + // (undocumented) + parent?: Node_2; + // (undocumented) + readonly: boolean; + // (undocumented) + static: false; + // (undocumented) + type: "TSPropertySignature"; + // (undocumented) + typeAnnotation: TSTypeAnnotation | null; +} + +// @public (undocumented) +export interface TSQualifiedName extends Span { + // (undocumented) + left: TSTypeName; + // (undocumented) + parent?: Node_2; + // (undocumented) + right: IdentifierName; + // (undocumented) + type: "TSQualifiedName"; +} + +// @public (undocumented) +export interface TSRestType extends Span { + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSRestType"; + // (undocumented) + typeAnnotation: TSType; +} + +// @public (undocumented) +export interface TSSatisfiesExpression extends Span { + // (undocumented) + expression: Expression; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSSatisfiesExpression"; + // (undocumented) + typeAnnotation: TSType; +} + +// @public (undocumented) +export type TSSignature = TSIndexSignature | TSPropertySignature | TSCallSignatureDeclaration | TSConstructSignatureDeclaration | TSMethodSignature; + +// @public (undocumented) +export interface TSStringKeyword extends Span { + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSStringKeyword"; +} + +// @public (undocumented) +export interface TSSymbolKeyword extends Span { + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSSymbolKeyword"; +} + +// @public (undocumented) +export interface TSTemplateLiteralType extends Span { + // (undocumented) + parent?: Node_2; + // (undocumented) + quasis: Array; + // (undocumented) + type: "TSTemplateLiteralType"; + // (undocumented) + types: Array; +} + +// @public (undocumented) +export interface TSThisParameter extends Span { + // (undocumented) + decorators: []; + // (undocumented) + name: "this"; + // (undocumented) + optional: false; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "Identifier"; + // (undocumented) + typeAnnotation: TSTypeAnnotation | null; +} + +// @public (undocumented) +export interface TSThisType extends Span { + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSThisType"; +} + +// @public (undocumented) +export type TSTupleElement = TSOptionalType | TSRestType | TSType; + +// @public (undocumented) +export interface TSTupleType extends Span { + // (undocumented) + elementTypes: Array; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSTupleType"; +} + +// @public (undocumented) +export type TSType = TSAnyKeyword | TSBigIntKeyword | TSBooleanKeyword | TSIntrinsicKeyword | TSNeverKeyword | TSNullKeyword | TSNumberKeyword | TSObjectKeyword | TSStringKeyword | TSSymbolKeyword | TSUndefinedKeyword | TSUnknownKeyword | TSVoidKeyword | TSArrayType | TSConditionalType | TSConstructorType | TSFunctionType | TSImportType | TSIndexedAccessType | TSInferType | TSIntersectionType | TSLiteralType | TSMappedType | TSNamedTupleMember | TSTemplateLiteralType | TSThisType | TSTupleType | TSTypeLiteral | TSTypeOperator | TSTypePredicate | TSTypeQuery | TSTypeReference | TSUnionType | TSParenthesizedType | JSDocNullableType | JSDocNonNullableType | JSDocUnknownType; + +// @public (undocumented) +export interface TSTypeAliasDeclaration extends Span { + // (undocumented) + declare: boolean; + // (undocumented) + id: BindingIdentifier; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSTypeAliasDeclaration"; + // (undocumented) + typeAnnotation: TSType; + // (undocumented) + typeParameters: TSTypeParameterDeclaration | null; +} + +// @public (undocumented) +export interface TSTypeAnnotation extends Span { + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSTypeAnnotation"; + // (undocumented) + typeAnnotation: TSType; +} + +// @public (undocumented) +export interface TSTypeAssertion extends Span { + // (undocumented) + expression: Expression; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSTypeAssertion"; + // (undocumented) + typeAnnotation: TSType; +} + +// @public (undocumented) +export interface TSTypeLiteral extends Span { + // (undocumented) + members: Array; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSTypeLiteral"; +} + +// @public (undocumented) +export type TSTypeName = IdentifierReference | TSQualifiedName | ThisExpression; + +// @public (undocumented) +export interface TSTypeOperator extends Span { + // (undocumented) + operator: TSTypeOperatorOperator; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSTypeOperator"; + // (undocumented) + typeAnnotation: TSType; +} + +// @public (undocumented) +export type TSTypeOperatorOperator = "keyof" | "unique" | "readonly"; + +// @public (undocumented) +export interface TSTypeParameter extends Span { + // (undocumented) + const: boolean; + // (undocumented) + constraint: TSType | null; + // (undocumented) + default: TSType | null; + // (undocumented) + in: boolean; + // (undocumented) + name: BindingIdentifier; + // (undocumented) + out: boolean; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSTypeParameter"; +} + +// @public (undocumented) +export interface TSTypeParameterDeclaration extends Span { + // (undocumented) + params: Array; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSTypeParameterDeclaration"; +} + +// @public (undocumented) +export interface TSTypeParameterInstantiation extends Span { + // (undocumented) + params: Array; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSTypeParameterInstantiation"; +} + +// @public (undocumented) +export interface TSTypePredicate extends Span { + // (undocumented) + asserts: boolean; + // (undocumented) + parameterName: TSTypePredicateName; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSTypePredicate"; + // (undocumented) + typeAnnotation: TSTypeAnnotation | null; +} + +// @public (undocumented) +export type TSTypePredicateName = IdentifierName | TSThisType; + +// @public (undocumented) +export interface TSTypeQuery extends Span { + // (undocumented) + exprName: TSTypeQueryExprName; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSTypeQuery"; + // (undocumented) + typeArguments: TSTypeParameterInstantiation | null; +} + +// @public (undocumented) +export type TSTypeQueryExprName = TSImportType | TSTypeName; + +// @public (undocumented) +export interface TSTypeReference extends Span { + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSTypeReference"; + // (undocumented) + typeArguments: TSTypeParameterInstantiation | null; + // (undocumented) + typeName: TSTypeName; +} + +// @public (undocumented) +export interface TSUndefinedKeyword extends Span { + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSUndefinedKeyword"; +} + +// @public (undocumented) +export interface TSUnionType extends Span { + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSUnionType"; + // (undocumented) + types: Array; +} + +// @public (undocumented) +export interface TSUnknownKeyword extends Span { + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSUnknownKeyword"; +} + +// @public (undocumented) +export interface TSVoidKeyword extends Span { + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "TSVoidKeyword"; +} + +// @public (undocumented) +export interface UnaryExpression extends Span { + // (undocumented) + argument: Expression; + // (undocumented) + operator: UnaryOperator; + // (undocumented) + parent?: Node_2; + // (undocumented) + prefix: true; + // (undocumented) + type: "UnaryExpression"; +} + +// @public (undocumented) +export type UnaryOperator = "+" | "-" | "!" | "~" | "typeof" | "void" | "delete"; + +// @public (undocumented) +export interface UpdateExpression extends Span { + // (undocumented) + argument: SimpleAssignmentTarget; + // (undocumented) + operator: UpdateOperator; + // (undocumented) + parent?: Node_2; + // (undocumented) + prefix: boolean; + // (undocumented) + type: "UpdateExpression"; +} + +// @public (undocumented) +export type UpdateOperator = "++" | "--"; + +// @public (undocumented) +export interface V8IntrinsicExpression extends Span { + // (undocumented) + arguments: Array; + // (undocumented) + name: IdentifierName; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "V8IntrinsicExpression"; +} + +// @public (undocumented) +export interface VariableDeclaration extends Span { + // (undocumented) + declarations: Array; + // (undocumented) + declare?: boolean; + // (undocumented) + kind: VariableDeclarationKind; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "VariableDeclaration"; +} + +// @public (undocumented) +export type VariableDeclarationKind = "var" | "let" | "const" | "using" | "await using"; + +// @public (undocumented) +export interface VariableDeclarator extends Span { + // (undocumented) + definite?: boolean; + // (undocumented) + id: BindingPattern; + // (undocumented) + init: Expression | null; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "VariableDeclarator"; +} + +// @public (undocumented) +export interface WhileStatement extends Span { + // (undocumented) + body: Statement; + // (undocumented) + parent?: Node_2; + // (undocumented) + test: Expression; + // (undocumented) + type: "WhileStatement"; +} + +// @public (undocumented) +export interface WithStatement extends Span { + // (undocumented) + body: Statement; + // (undocumented) + object: Expression; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "WithStatement"; +} + +// @public (undocumented) +export interface YieldExpression extends Span { + // (undocumented) + argument: Expression | null; + // (undocumented) + delegate: boolean; + // (undocumented) + parent?: Node_2; + // (undocumented) + type: "YieldExpression"; +} + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/ignorers/interface/oxlint.config.ts b/packages/ignorers/interface/oxlint.config.ts new file mode 100644 index 000000000..879f8a966 --- /dev/null +++ b/packages/ignorers/interface/oxlint.config.ts @@ -0,0 +1,6 @@ +import preset from '@systemfsoftware/oxlint-ignorer-config' +import { defineConfig } from 'oxlint' + +export default defineConfig({ + extends: [preset], +}) diff --git a/packages/ignorers/interface/package.json b/packages/ignorers/interface/package.json new file mode 100644 index 000000000..d14e26bdb --- /dev/null +++ b/packages/ignorers/interface/package.json @@ -0,0 +1,69 @@ +{ + "name": "@systemfsoftware/stryker-ignorer-interface", + "license": "Apache-2.0", + "version": "0.0.0", + "author": "Ryan Lee ", + "repository": { + "type": "git", + "url": "git+https://github.com/systemfsoftware/stryker-js-effect.git", + "directory": "packages/ignorers/interface" + }, + "homepage": "https://github.com/systemfsoftware/stryker-js-effect/tree/main/packages/ignorers/interface#readme", + "bugs": "https://github.com/systemfsoftware/stryker-js-effect/issues", + "description": "The ignorer interface for StrykerJS-style mutation tooling — the PlainIgnorer descriptor, the canonical AST node vocabulary, and the NodePath shape. Types only, with no runtime exports and no Effect dependency.", + "keywords": [ + "stryker", + "mutation-testing", + "ignorer", + "plugin", + "ast", + "estree", + "test-quality" + ], + "type": "module", + "files": [ + "dist" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.mjs" + }, + "./package.json": "./package.json" + }, + "scripts": { + "build": "tsdown && pnpm api:check", + "clean": "rimraf dist", + "typecheck": "tsc --noEmit --incremental && tsc -p tsconfig.node.json --noEmit", + "test": "vitest run", + "lint": "f=${OXLINT_FORMAT:-${AGENT:+agent}}; oxlint . --format=${f:-default}", + "attw": "attw --pack .", + "api:check": "tsdown && api-extractor run", + "api:update": "api-extractor run --local" + }, + "devDependencies": { + "@microsoft/api-extractor": "catalog:", + "@oxc-project/types": "catalog:", + "@systemfsoftware/arethetypeswrong-cli": "catalog:", + "@systemfsoftware/oxlint-ignorer-config": "workspace:^", + "@systemfsoftware/tsconfig": "catalog:", + "@systemfsoftware/vitest-config": "workspace:^", + "@types/node": "catalog:", + "oxlint": "catalog:", + "rimraf": "catalog:", + "tsdown": "catalog:", + "typescript": "catalog:", + "vitest": "catalog:" + }, + "publishConfig": { + "access": "public", + "provenance": true, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.mjs" + }, + "./package.json": "./package.json" + } + } +} diff --git a/packages/ignorers/interface/src/mod.ts b/packages/ignorers/interface/src/mod.ts new file mode 100644 index 000000000..81e169b1e --- /dev/null +++ b/packages/ignorers/interface/src/mod.ts @@ -0,0 +1,11 @@ +export type * from '@oxc-project/types' + +export interface NodePath { + readonly node: unknown + readonly ancestors: readonly unknown[] +} + +export interface PlainIgnorer { + readonly name: string + shouldIgnore(path: NodePath): string | undefined +} diff --git a/packages/ignorers/interface/tsconfig.build.json b/packages/ignorers/interface/tsconfig.build.json new file mode 100644 index 000000000..869b9aaa9 --- /dev/null +++ b/packages/ignorers/interface/tsconfig.build.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declarationMap": false, + "sourceMap": false, + "stripInternal": true + }, + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.spec.ts", "src/**/*.test.ts", "src/**/*.tst.ts"] +} diff --git a/packages/ignorers/interface/tsconfig.json b/packages/ignorers/interface/tsconfig.json new file mode 100644 index 000000000..04c392e87 --- /dev/null +++ b/packages/ignorers/interface/tsconfig.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": ["@systemfsoftware/tsconfig/tsc/dom/library-monorepo"], + "compilerOptions": { + "types": [] + }, + "include": [ + "src", + "tests" + ], + "exclude": [ + "node_modules", + "dist" + ], + "references": [ + { + "path": "./tsconfig.node.json" + } + ] +} diff --git a/packages/ignorers/interface/tsconfig.node.json b/packages/ignorers/interface/tsconfig.node.json new file mode 100644 index 000000000..4d32e1ec2 --- /dev/null +++ b/packages/ignorers/interface/tsconfig.node.json @@ -0,0 +1,4 @@ +{ + "extends": "@systemfsoftware/tsconfig/node", + "include": ["oxlint.config.ts", "tsdown.config.ts", "vitest.config.ts"] +} diff --git a/packages/ignorers/interface/tsdown.config.ts b/packages/ignorers/interface/tsdown.config.ts new file mode 100644 index 000000000..6a4bc1e03 --- /dev/null +++ b/packages/ignorers/interface/tsdown.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'tsdown' + +export default defineConfig({ + entry: { + index: './src/mod.ts', + }, + format: 'esm', + dts: true, + tsconfig: './tsconfig.build.json', + clean: true, + outExtensions: () => ({ js: '.mjs', dts: '.d.ts' }), +}) diff --git a/packages/ignorers/interface/vitest.config.ts b/packages/ignorers/interface/vitest.config.ts new file mode 100644 index 000000000..6536c4494 --- /dev/null +++ b/packages/ignorers/interface/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig, sharedConfig } from '@systemfsoftware/vitest-config' + +export default defineConfig({ + ...sharedConfig, + test: { + ...sharedConfig.test, + include: ['src/**/*.test.ts', 'tests/**/*.test.ts'], + }, +}) diff --git a/packages/stryker-js-cli/package.json b/packages/stryker-js-cli/package.json index 81d507f0a..a403a7201 100644 --- a/packages/stryker-js-cli/package.json +++ b/packages/stryker-js-cli/package.json @@ -36,10 +36,6 @@ "@systemfsoftware/stryker-js-html-reporter": "workspace:^", "@systemfsoftware/stryker-js-language": "workspace:^", "@systemfsoftware/stryker-js-plugin-interface": "workspace:^", - "@systemfsoftware/stryker-js-typescript-checker": "workspace:^", - "@systemfsoftware/stryker-js-vitest-runner": "workspace:^", - "@systemfsoftware/stryker-plugins": "workspace:^", - "@systemfsoftware/stryker-test-contribution": "workspace:^", "@systemfsoftware/tsconfig": "catalog:", "@systemfsoftware/vitest-config": "workspace:^", "@types/node": "catalog:", @@ -50,7 +46,6 @@ "oxlint": "catalog:", "rimraf": "catalog:", "tsdown": "catalog:", - "vite-tsconfig-paths": "catalog:", "vitest": "catalog:" }, "engines": { diff --git a/packages/stryker-js-engine/api-extractor.base.json b/packages/stryker-js-engine/api-extractor.base.json new file mode 100644 index 000000000..cd855e92a --- /dev/null +++ b/packages/stryker-js-engine/api-extractor.base.json @@ -0,0 +1,55 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "projectFolder": ".", + "mainEntryPointFilePath": "/dist/config/base.d.mts", + "bundledPackages": [], + "compiler": { + "tsconfigFilePath": "/tsconfig.api-base.json" + }, + "apiReport": { + "enabled": true, + "reportFolder": "/etc/", + "reportTempFolder": "/temp/base/", + "reportFileName": "base.api.md" + }, + "docModel": { + "enabled": false + }, + "dtsRollup": { + "enabled": true, + "untrimmedFilePath": "/dist/config/base.d.mts" + }, + "tsdocMetadata": { + "enabled": false + }, + "messages": { + "extractorMessageReporting": { + "default": { + "logLevel": "warning" + }, + "ae-forgotten-export": { + "logLevel": "error" + }, + "ae-missing-release-tag": { + "logLevel": "none" + }, + "ae-internal-missing-underscore": { + "logLevel": "none" + } + }, + "tsdocMessageReporting": { + "default": { + "logLevel": "warning" + }, + "tsdoc-undefined-tag": { + "logLevel": "none" + }, + "tsdoc-unsupported-tag": { + "logLevel": "none" + }, + "tsdoc-param-tag-missing-hyphen": { + "logLevel": "none" + } + } + } +} diff --git a/packages/stryker-js-engine/api-extractor.builtin-reporters.json b/packages/stryker-js-engine/api-extractor.builtin-reporters.json new file mode 100644 index 000000000..2bde55f11 --- /dev/null +++ b/packages/stryker-js-engine/api-extractor.builtin-reporters.json @@ -0,0 +1,55 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "projectFolder": ".", + "mainEntryPointFilePath": "/dist/builtin-reporters.d.mts", + "bundledPackages": [], + "compiler": { + "tsconfigFilePath": "/tsconfig.api-builtin-reporters.json" + }, + "apiReport": { + "enabled": true, + "reportFolder": "/etc/", + "reportTempFolder": "/temp/builtin-reporters/", + "reportFileName": "builtin-reporters.api.md" + }, + "docModel": { + "enabled": false + }, + "dtsRollup": { + "enabled": true, + "untrimmedFilePath": "/dist/builtin-reporters.d.mts" + }, + "tsdocMetadata": { + "enabled": false + }, + "messages": { + "extractorMessageReporting": { + "default": { + "logLevel": "warning" + }, + "ae-forgotten-export": { + "logLevel": "error" + }, + "ae-missing-release-tag": { + "logLevel": "none" + }, + "ae-internal-missing-underscore": { + "logLevel": "none" + } + }, + "tsdocMessageReporting": { + "default": { + "logLevel": "warning" + }, + "tsdoc-undefined-tag": { + "logLevel": "none" + }, + "tsdoc-unsupported-tag": { + "logLevel": "none" + }, + "tsdoc-param-tag-missing-hyphen": { + "logLevel": "none" + } + } + } +} diff --git a/packages/stryker-js-engine/api-extractor.json b/packages/stryker-js-engine/api-extractor.json new file mode 100644 index 000000000..d5a9a6623 --- /dev/null +++ b/packages/stryker-js-engine/api-extractor.json @@ -0,0 +1,55 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "projectFolder": ".", + "mainEntryPointFilePath": "/dist/index.d.mts", + "bundledPackages": [], + "compiler": { + "tsconfigFilePath": "/tsconfig.json" + }, + "apiReport": { + "enabled": true, + "reportFolder": "/etc/", + "reportTempFolder": "/temp/main/", + "reportFileName": "stryker-js-engine.api.md" + }, + "docModel": { + "enabled": false + }, + "dtsRollup": { + "enabled": true, + "untrimmedFilePath": "/dist/index.d.mts" + }, + "tsdocMetadata": { + "enabled": false + }, + "messages": { + "extractorMessageReporting": { + "default": { + "logLevel": "warning" + }, + "ae-forgotten-export": { + "logLevel": "error" + }, + "ae-missing-release-tag": { + "logLevel": "none" + }, + "ae-internal-missing-underscore": { + "logLevel": "none" + } + }, + "tsdocMessageReporting": { + "default": { + "logLevel": "warning" + }, + "tsdoc-undefined-tag": { + "logLevel": "none" + }, + "tsdoc-unsupported-tag": { + "logLevel": "none" + }, + "tsdoc-param-tag-missing-hyphen": { + "logLevel": "none" + } + } + } +} diff --git a/packages/stryker-js-engine/api-extractor.plugin-loader.json b/packages/stryker-js-engine/api-extractor.plugin-loader.json new file mode 100644 index 000000000..9119095d8 --- /dev/null +++ b/packages/stryker-js-engine/api-extractor.plugin-loader.json @@ -0,0 +1,55 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "projectFolder": ".", + "mainEntryPointFilePath": "/dist/plugin-loader.d.mts", + "bundledPackages": [], + "compiler": { + "tsconfigFilePath": "/tsconfig.api-plugin-loader.json" + }, + "apiReport": { + "enabled": true, + "reportFolder": "/etc/", + "reportTempFolder": "/temp/plugin-loader/", + "reportFileName": "plugin-loader.api.md" + }, + "docModel": { + "enabled": false + }, + "dtsRollup": { + "enabled": true, + "untrimmedFilePath": "/dist/plugin-loader.d.mts" + }, + "tsdocMetadata": { + "enabled": false + }, + "messages": { + "extractorMessageReporting": { + "default": { + "logLevel": "warning" + }, + "ae-forgotten-export": { + "logLevel": "error" + }, + "ae-missing-release-tag": { + "logLevel": "none" + }, + "ae-internal-missing-underscore": { + "logLevel": "none" + } + }, + "tsdocMessageReporting": { + "default": { + "logLevel": "warning" + }, + "tsdoc-undefined-tag": { + "logLevel": "none" + }, + "tsdoc-unsupported-tag": { + "logLevel": "none" + }, + "tsdoc-param-tag-missing-hyphen": { + "logLevel": "none" + } + } + } +} diff --git a/packages/stryker-js-engine/api-extractor.worker.json b/packages/stryker-js-engine/api-extractor.worker.json new file mode 100644 index 000000000..8f789dfcd --- /dev/null +++ b/packages/stryker-js-engine/api-extractor.worker.json @@ -0,0 +1,55 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "projectFolder": ".", + "mainEntryPointFilePath": "/dist/worker.d.mts", + "bundledPackages": [], + "compiler": { + "tsconfigFilePath": "/tsconfig.api-worker.json" + }, + "apiReport": { + "enabled": true, + "reportFolder": "/etc/", + "reportTempFolder": "/temp/worker/", + "reportFileName": "worker.api.md" + }, + "docModel": { + "enabled": false + }, + "dtsRollup": { + "enabled": true, + "untrimmedFilePath": "/dist/worker.d.mts" + }, + "tsdocMetadata": { + "enabled": false + }, + "messages": { + "extractorMessageReporting": { + "default": { + "logLevel": "warning" + }, + "ae-forgotten-export": { + "logLevel": "error" + }, + "ae-missing-release-tag": { + "logLevel": "none" + }, + "ae-internal-missing-underscore": { + "logLevel": "none" + } + }, + "tsdocMessageReporting": { + "default": { + "logLevel": "warning" + }, + "tsdoc-undefined-tag": { + "logLevel": "none" + }, + "tsdoc-unsupported-tag": { + "logLevel": "none" + }, + "tsdoc-param-tag-missing-hyphen": { + "logLevel": "none" + } + } + } +} diff --git a/packages/stryker-js-engine/etc/base.api.md b/packages/stryker-js-engine/etc/base.api.md new file mode 100644 index 000000000..68396656d --- /dev/null +++ b/packages/stryker-js-engine/etc/base.api.md @@ -0,0 +1,18 @@ +## API Report File for "@systemfsoftware/stryker-js-engine" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import * as S from 'effect/Schema'; +import { StrykerOptionsSchema } from '@systemfsoftware/stryker-js-language'; + +// Warning: (ae-forgotten-export) The symbol "BasePreset" needs to be exported by the entry point base.d.mts +// +// @public (undocumented) +const basePreset: BasePreset; +export default basePreset; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/stryker-js-engine/etc/builtin-reporters.api.md b/packages/stryker-js-engine/etc/builtin-reporters.api.md new file mode 100644 index 000000000..55a165249 --- /dev/null +++ b/packages/stryker-js-engine/etc/builtin-reporters.api.md @@ -0,0 +1,24 @@ +## API Report File for "@systemfsoftware/stryker-js-engine" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import * as FileSystem from 'effect/FileSystem'; +import * as Path from 'effect/Path'; +import { ReporterFactory } from '@systemfsoftware/stryker-js-language'; + +// @public (undocumented) +export interface JsonReporterDeps { + // (undocumented) + readonly fileSystem: FileSystem.FileSystem; + // (undocumented) + readonly path: Path.Path; +} + +// @public (undocumented) +export const makeBuiltinReporterFactories: (services: JsonReporterDeps) => Record; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/stryker-js-engine/etc/plugin-loader.api.md b/packages/stryker-js-engine/etc/plugin-loader.api.md new file mode 100644 index 000000000..2e9e7f29f --- /dev/null +++ b/packages/stryker-js-engine/etc/plugin-loader.api.md @@ -0,0 +1,83 @@ +## API Report File for "@systemfsoftware/stryker-js-engine" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { AnyPluginContribution } from '@systemfsoftware/stryker-js-plugin-interface'; +import { ContributionOf } from '@systemfsoftware/stryker-js-plugin-interface'; +import * as Effect from 'effect/Effect'; +import * as FileSystem from 'effect/FileSystem'; +import * as HashMap from 'effect/HashMap'; +import { Module } from '@systemfsoftware/stryker-js-language'; +import * as Path from 'effect/Path'; +import { PluginContribution } from '@systemfsoftware/stryker-js-plugin-interface'; +import { PluginKind } from '@systemfsoftware/stryker-js-plugin-interface'; +import { Schema } from 'effect'; +import { YieldableError } from 'effect/Cause'; + +// @public (undocumented) +export function create(pluginsByKind: HashMap.HashMap, kind: K, name: string): Effect.Effect, PluginNotFoundError>; + +// @public (undocumented) +export function createAll(pluginsByKind: HashMap.HashMap, kind: K): Effect.Effect[]>; + +// @public (undocumented) +export interface LoadedPlugins { + // (undocumented) + readonly pluginModulePaths: readonly string[]; + // (undocumented) + readonly pluginsByKind: HashMap.HashMap[]>; + // (undocumented) + readonly schemaContributions: readonly Record[]; +} + +// @public (undocumented) +export function loadPlugins(pluginDescriptors: readonly string[], basePath: string): Effect.Effect; + +// @public (undocumented) +export interface PluginLoaderEntryLike { + // (undocumented) + readonly moduleName: string; + // (undocumented) + readonly plugins: readonly PluginContribution[] | undefined; + // (undocumented) + readonly schemaContribution: Record | undefined; +} + +// Warning: (ae-forgotten-export) The symbol "PluginLoadFailedError_base" needs to be exported by the entry point plugin-loader.d.mts +// +// @public (undocumented) +export class PluginLoadFailedError extends PluginLoadFailedError_base { + // (undocumented) + readonly exitClass: 'InternalError'; +} + +// @public (undocumented) +export interface PluginLoadPlan { + // (undocumented) + readonly pluginModulePaths: readonly string[]; + // (undocumented) + readonly pluginsByKind: HashMap.HashMap[]>; + // (undocumented) + readonly schemaContributions: readonly Record[]; + // (undocumented) + readonly shadowings: readonly { + readonly kind: PluginKind; + readonly name: string; + readonly shadowedIndex: number; + readonly winnerIndex: number; + }[]; +} + +// Warning: (ae-forgotten-export) The symbol "PluginNotFoundError_base" needs to be exported by the entry point plugin-loader.d.mts +// +// @public (undocumented) +export class PluginNotFoundError extends PluginNotFoundError_base { + // (undocumented) + readonly exitClass: 'ConfigError'; +} + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/stryker-js-engine/etc/stryker-js-engine.api.md b/packages/stryker-js-engine/etc/stryker-js-engine.api.md new file mode 100644 index 000000000..3c6ff1ab9 --- /dev/null +++ b/packages/stryker-js-engine/etc/stryker-js-engine.api.md @@ -0,0 +1,607 @@ +## API Report File for "@systemfsoftware/stryker-js-engine" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import * as Cause from 'effect/Cause'; +import { CheckResult } from '@systemfsoftware/stryker-js-language'; +import * as ChildProcessSpawner from 'effect/unstable/process/ChildProcessSpawner'; +import { CompleteDryRunResult } from '@systemfsoftware/stryker-js-language'; +import { ComposedPlugins } from '@systemfsoftware/stryker-js-plugin-interface'; +import * as Context from 'effect/Context'; +import { CoverageData } from '@systemfsoftware/stryker-js-language'; +import { Duration } from 'effect/Duration'; +import * as Duration_2 from 'effect/Duration'; +import * as Effect from 'effect/Effect'; +import * as Exit from 'effect/Exit'; +import { ExitClass } from '@systemfsoftware/stryker-js-language'; +import { FileDescription } from '@systemfsoftware/stryker-js-language'; +import { FileDescriptions } from '@systemfsoftware/stryker-js-language'; +import * as FileSystem from 'effect/FileSystem'; +import * as HashMap from 'effect/HashMap'; +import { IgnorerService } from '@systemfsoftware/stryker-js-language'; +import * as Layer from 'effect/Layer'; +import { Module } from '@systemfsoftware/stryker-js-language'; +import * as MutableHashMap from 'effect/MutableHashMap'; +import * as MutableHashSet from 'effect/MutableHashSet'; +import { Mutant } from '@systemfsoftware/stryker-js-language'; +import { MutantStatus } from '@systemfsoftware/stryker-js-language'; +import { MutateDescription } from '@systemfsoftware/stryker-js-language'; +import { MutationTestResult } from '@systemfsoftware/stryker-js-language'; +import { PartialStrykerOptions } from '@systemfsoftware/stryker-js-language'; +import * as Path from 'effect/Path'; +import { PluginContribution } from '@systemfsoftware/stryker-js-plugin-interface'; +import { PluginKind } from '@systemfsoftware/stryker-js-plugin-interface'; +import * as Queue from 'effect/Queue'; +import * as RpcClient from 'effect/unstable/rpc/RpcClient'; +import { RunEvent } from '@systemfsoftware/stryker-js-language'; +import { RunEvents } from '@systemfsoftware/stryker-js-language'; +import { RunMutantResult } from '@systemfsoftware/stryker-js-language'; +import { RunPlan } from '@systemfsoftware/stryker-js-language'; +import * as S from 'effect/Schema'; +import * as Schedule from 'effect/Schedule'; +import { Schema } from 'effect'; +import * as schema from '@systemfsoftware/stryker-js-language'; +import * as Scope from 'effect/Scope'; +import * as Socket from 'effect/unstable/socket/Socket'; +import { StrykerOptions } from '@systemfsoftware/stryker-js-language'; +import { TestResult } from '@systemfsoftware/stryker-js-language'; +import { YieldableError } from 'effect/Cause'; + +// @public (undocumented) +export const ACTIONABLE_STATUSES: readonly ['Survived', 'NoCoverage', 'Timeout', 'RuntimeError']; + +// @public (undocumented) +export function buildVerdictEnvelope(report: schema.MutationTestResult, mode: OutputMode, signal: ModeSignal, runId: string, basePath: string, pathService: Path.Path): VerdictEnvelope; + +// @public +export interface CheckerResourceService { + // Warning: (ae-forgotten-export) The symbol "CheckerCrash" needs to be exported by the entry point index.d.mts + // + // (undocumented) + readonly check: (checkerName: string, mutants: readonly Mutant[]) => Effect.Effect, CheckerCrash>; + readonly group: (checkerName: string, mutants: readonly Mutant[]) => Effect.Effect; +} + +// Warning: (ae-forgotten-export) The symbol "CheckerContractBroken" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export const checkGroupedPlans: (checker: CheckerResourceService, checkerName: string, plans: readonly RunPlan[]) => Effect.Effect; + +// Warning: (ae-forgotten-export) The symbol "ChildProcessCrashedError_base" needs to be exported by the entry point index.d.mts +// +// @public +export class ChildProcessCrashedError extends ChildProcessCrashedError_base { + // (undocumented) + readonly exitClass: 'InternalError'; +} + +// @public (undocumented) +export const CONFIG_SYNTAX_HELP: string; + +// @public (undocumented) +export const ConfigDocumentSchema: S.$Record; + +// Warning: (ae-forgotten-export) The symbol "ConfigError_base" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export class ConfigError extends ConfigError_base { + // (undocumented) + readonly exitClass: 'ConfigError'; +} + +// Warning: (ae-forgotten-export) The symbol "ConfigFileInvalidError_base" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export class ConfigFileInvalidError extends ConfigFileInvalidError_base { + // (undocumented) + readonly exitClass: 'ConfigError'; +} + +// Warning: (ae-forgotten-export) The symbol "ConfigFileNotFoundError_base" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export class ConfigFileNotFoundError extends ConfigFileNotFoundError_base { + // (undocumented) + readonly exitClass: 'ConfigError'; +} + +// Warning: (ae-forgotten-export) The symbol "ConfigFileUnreadableError_base" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export class ConfigFileUnreadableError extends ConfigFileUnreadableError_base { + // (undocumented) + readonly exitClass: 'ConfigError'; +} + +// @public +export const connectRetry: Schedule.Schedule; + +// @public (undocumented) +export function createDefaultOptions(): Effect.Effect; + +// @public (undocumented) +export function createFileMatcher(pattern: boolean | string, pathService: Path.Path, allowHiddenFiles?: boolean): (fileName: string) => boolean; + +// @public (undocumented) +export const decideExtendsStep: (state: ExtendsStepState, document: PartialStrykerOptions, file: string, pathService: Path.Path) => ExtendsStepDecision; + +// @public (undocumented) +export function deepFreeze(target: T): Immutable; + +// @public (undocumented) +export const DEFAULT_CONFIG_FILE_NAMES: Readonly<{ + readonly JSON: 'stryker.config.json'; + readonly JAVASCRIPT: 'stryker.config.mjs'; +}>; + +// @public (undocumented) +export const defaultOptions: Effect.Effect, never, never>; + +// @public (undocumented) +export function describeErrors(error: S.SchemaError): string[]; + +// @public (undocumented) +export interface DryRunDone extends InstrumentDone { + // (undocumented) + readonly dryRunResult: CompleteDryRunResult; + // Warning: (ae-forgotten-export) The symbol "TestCoverage" needs to be exported by the entry point index.d.mts + // + // (undocumented) + readonly testCoverage: TestCoverage; + // (undocumented) + readonly timeOverhead: Duration_2.Duration; +} + +// @public (undocumented) +export type EnginePorts = ChildProcessSpawner.ChildProcessSpawner | FileSystem.FileSystem | Module | Path.Path | WorkerEntries | WorkerLauncher; + +// @public (undocumented) +export const extendsPropertySchema: S.optionalKey; + +// @public (undocumented) +export type ExtendsRefusalReason = 'cycle' | 'non-string-extends'; + +// Warning: (ae-forgotten-export) The symbol "DoneTag" needs to be exported by the entry point index.d.mts +// Warning: (ae-forgotten-export) The symbol "ReadTag" needs to be exported by the entry point index.d.mts +// Warning: (ae-forgotten-export) The symbol "ResolveTag" needs to be exported by the entry point index.d.mts +// Warning: (ae-forgotten-export) The symbol "RefusedTag" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export type ExtendsStepDecision = (DoneTag & { + readonly options: PartialStrykerOptions; +}) | (ReadTag & { + readonly path: string; + readonly state: ExtendsStepState; +}) | (ResolveTag & { + readonly specifier: string; + readonly directory: string; + readonly state: ExtendsStepState; +}) | (RefusedTag & { + readonly reason: ExtendsRefusalReason; + readonly file: string; +}); + +// @public (undocumented) +export interface ExtendsStepDocument { + // (undocumented) + readonly options: PartialStrykerOptions; + // (undocumented) + readonly path: string; +} + +// @public (undocumented) +export interface ExtendsStepState { + // (undocumented) + readonly documents: readonly ExtendsStepDocument[]; + // (undocumented) + readonly visited: readonly string[]; +} + +// @public (undocumented) +export function findUnserializables(thing: unknown): UnserializableDescription[] | undefined; + +// @public (undocumented) +export const forkCoreSchema: Record; + +// @public (undocumented) +export const forkOptionsSchema: S.StructWithRest; + readonly extends: S.optionalKey; +}>, readonly [S.$Record]>; + +// @public (undocumented) +export function generateRunId(): string; + +// Warning: (ae-forgotten-export) The symbol "ImmutablePrimitive" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export type Immutable = T extends ImmutablePrimitive ? T : T extends Array ? ReadonlyArray> : T extends Map ? ReadonlyMap, Immutable> : T extends Set ? ReadonlySet> : T extends RegExp ? Readonly : { readonly [K in keyof T]: Immutable; }; + +// @public (undocumented) +export const ImportedModuleSchema: S.Struct<{ + readonly default: S.optional; +}>; + +// @public (undocumented) +export function importModule(moduleName: string, basePath: string): Effect.Effect; + +// @public (undocumented) +export const IncrementalReportSchema: S.StructWithRest; + readonly files: S.$Record; + readonly end: S.Struct<{ + readonly line: S.Finite; + readonly column: S.Finite; + }>; + }>; + readonly status: S.Literals; + readonly killedBy: S.optional>; + readonly coveredBy: S.optional>; + readonly static: S.optional; + readonly statusReason: S.optional; + readonly testsCompleted: S.optional; + readonly description: S.optional; + readonly duration: S.optional; + }>>; + }>>; + readonly testFiles: S.optional; + readonly tests: S.$Array; + readonly end: S.optional>; + }>>; + }>>; + }>>>; +}>, readonly [S.$Record]>; + +// @public (undocumented) +export const initialExtendsStepState: ExtendsStepState; + +// @public (undocumented) +export interface InstrumentDone extends PrepareDone { + // (undocumented) + readonly concurrency: { + readonly testRunners: number; + readonly checkers: number; + }; + // (undocumented) + readonly mutants: readonly Mutant[]; + // Warning: (ae-forgotten-export) The symbol "SandboxHandle" needs to be exported by the entry point index.d.mts + // + // (undocumented) + readonly sandbox: SandboxHandle; +} + +// @public (undocumented) +export function isActionableStatus(status: MutantStatus): boolean; + +// @public (undocumented) +export function isModuleSpecifier(value: string): boolean; + +// Warning: (ae-forgotten-export) The symbol "KnownKeys" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export function isWarningEnabled(warningType: KnownKeys, warningOptions: WarningOptions | boolean): boolean; + +// Warning: (ae-forgotten-export) The symbol "IdGenerator" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export const makeRunLayer: (env: RunEnvironmentShape, events?: Queue.Queue) => Layer.Layer; + +// @public (undocumented) +export function matchesFile(pattern: boolean | string, fileName: string, pathService: Path.Path, allowHiddenFiles?: boolean): boolean; + +// Warning: (ae-forgotten-export) The symbol "MergeCommand_base" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export class MergeCommand extends MergeCommand_base {} + +// @public (undocumented) +export function mergeConfigs(parent: PartialStrykerOptions, child: PartialStrykerOptions): PartialStrykerOptions; + +// @public (undocumented) +export function mergeRecords(base: object, overrides: object): Record; + +// Warning: (ae-forgotten-export) The symbol "MergeResult_base" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export class MergeResult extends MergeResult_base {} + +// @public (undocumented) +export type ModeSignal = 'flag' | 'env' | 'tty' | 'agent' | 'tool'; + +// @public (undocumented) +export const optionsPath: (...path: string[]) => string; + +// Warning: (ae-forgotten-export) The symbol "OutOfMemoryError_base" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export class OutOfMemoryError extends OutOfMemoryError_base { + // (undocumented) + readonly exitClass: 'RuntimeError'; +} + +// @public +export type OutputMode = 'human' | 'machine'; + +// @public (undocumented) +export interface PrepareDone { + // (undocumented) + readonly ignorers: readonly IgnorerService[]; + // Warning: (ae-forgotten-export) The symbol "LoadedPlugins" needs to be exported by the entry point index.d.mts + // + // (undocumented) + readonly loadedPlugins: LoadedPlugins; + // (undocumented) + readonly options: StrykerOptions; + // (undocumented) + readonly plugins: ComposedPlugins; + // Warning: (ae-forgotten-export) The symbol "Project" needs to be exported by the entry point index.d.mts + // + // (undocumented) + readonly project: Project; + // Warning: (ae-forgotten-export) The symbol "ReporterStage" needs to be exported by the entry point index.d.mts + // + // (undocumented) + readonly reporterStage: ReporterStage; + // (undocumented) + readonly temporaryDirectoryPath: string; +} + +// @public (undocumented) +export type Primitive = boolean | number | string | null | undefined; + +// @public (undocumented) +export function readConfig(cliOptions: PartialStrykerOptions, basePath: string): Effect.Effect; + +// Warning: (ae-forgotten-export) The symbol "ReadConfigCommand_base" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export class ReadConfigCommand extends ReadConfigCommand_base {} + +// @public (undocumented) +export function readConfigFile(configFile: string): Effect.Effect; + +// @public (undocumented) +export const REMOVED_OPTIONS: Record; + +// @public (undocumented) +export interface ResolvedMode { + // (undocumented) + readonly mode: OutputMode; + // (undocumented) + readonly signal: ModeSignal; + // (undocumented) + readonly stdoutIsTTY: boolean; +} + +// @public (undocumented) +export function resolveExtends(configFile: string, document: PartialStrykerOptions): Effect.Effect; + +// Warning: (ae-forgotten-export) The symbol "RunEnvironment_base" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export class RunEnvironment extends RunEnvironment_base {} + +// @public (undocumented) +export interface RunEnvironmentShape { + // (undocumented) + readonly allowConsoleColors: boolean; + // (undocumented) + readonly basePath: string; + // (undocumented) + readonly reporterPluginModules: readonly string[]; + // (undocumented) + readonly resolvedMode: ResolvedMode; + // (undocumented) + readonly runId: string; + // (undocumented) + readonly runStartedAt: number; +} + +// Warning: (ae-forgotten-export) The symbol "StageServices" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export const runMutationTest: (cliOptions: PartialStrykerOptions, targetMutatePatterns?: string[]) => Effect.Effect; + +// @public (undocumented) +export interface RunOutcome { + // (undocumented) + readonly results: readonly RunMutantResult[]; + // (undocumented) + readonly verdict: ExitClass | null; +} + +// @public (undocumented) +export const shouldKeepTempDir: (exit: Exit.Exit, cleanTempDir: 'always' | boolean) => boolean; + +// @public +export interface SpawnedSocketWorker { + // (undocumented) + readonly clientLayer: Layer.Layer; + readonly exited: Effect.Effect; + // (undocumented) + readonly pid: number; +} + +// Warning: (ae-forgotten-export) The symbol "StageError_base" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export class StageError extends StageError_base { + // Warning: (ae-forgotten-export) The symbol "TypeId" needs to be exported by the entry point index.d.mts + // + // (undocumented) + readonly [TypeId]: "~stryker/mutation-run/StageError"; + // (undocumented) + get exitClass(): ExitClass; + // (undocumented) + get message(): string; +} + +// Warning: (ae-forgotten-export) The symbol "StrykerError_base" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export class StrykerError extends StrykerError_base {} + +// @public (undocumented) +export const strykerVersion: string; + +// @public (undocumented) +export const SUPPORTED_CONFIG_FILE_NAMES: readonly string[]; + +// @public (undocumented) +export const survivorsPriorReport: S.optionalKey; + +// @public (undocumented) +export const toRelativeNormalizedFileName: (fileName: string | undefined, basePath: string) => string; + +// @public (undocumented) +export interface UnserializableDescription { + // (undocumented) + path: string[]; + // (undocumented) + reason: string; +} + +// @public (undocumented) +export function validateOptions(options: Record, schema: ValidationSchemaDocument): Effect.Effect; + +// @public (undocumented) +export type ValidationSchemaDocument = { + readonly properties?: unknown; + readonly [key: string]: unknown; +}; + +// @public (undocumented) +export const VERDICT_ENVELOPE_SCHEMA_VERSION = "1.1"; + +// @public (undocumented) +export interface VerdictCounts { + // (undocumented) + readonly compileErrors: number; + // (undocumented) + readonly ignored: number; + // (undocumented) + readonly killed: number; + // (undocumented) + readonly noCoverage: number; + // (undocumented) + readonly pending: number; + // (undocumented) + readonly runtimeErrors: number; + // (undocumented) + readonly survived: number; + // (undocumented) + readonly timeout: number; +} + +// @public (undocumented) +export interface VerdictEnvelope { + // (undocumented) + readonly counts: VerdictCounts; + // (undocumented) + readonly mode: OutputMode; + // (undocumented) + readonly mutants: readonly VerdictMutant[]; + // (undocumented) + readonly reportFile: string | null; + // (undocumented) + readonly runId: string; + // (undocumented) + readonly schemaVersion: string; + // (undocumented) + readonly score: number | null; + // (undocumented) + readonly signal: ModeSignal; + // (undocumented) + readonly thresholds: VerdictThresholds; +} + +// @public (undocumented) +export interface VerdictMutant { + // (undocumented) + readonly file: string; + // (undocumented) + readonly id: string; + // (undocumented) + readonly location: schema.Location; + // (undocumented) + readonly mutator: string; + // (undocumented) + readonly replacement: string | null; + // (undocumented) + readonly status: MutantStatus; +} + +// @public (undocumented) +export interface VerdictThresholds { + // (undocumented) + readonly break: number | null; + // (undocumented) + readonly high: number; + // (undocumented) + readonly low: number; +} + +// @public (undocumented) +export type WarningOptions = Exclude; + +// Warning: (ae-forgotten-export) The symbol "WorkerEntries_base" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export class WorkerEntries extends WorkerEntries_base {} + +// @public +export interface WorkerEntriesShape { + // (undocumented) + readonly checkerWorkerUrl: URL; + // (undocumented) + readonly testRunnerWorkerUrl: URL; +} + +// Warning: (ae-forgotten-export) The symbol "WorkerLauncher_base" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export class WorkerLauncher extends WorkerLauncher_base {} + +// @public +export interface WorkerLauncherShape { + // (undocumented) + readonly spawn: (params: { + readonly entryUrl: URL; + readonly workingDirectory: string; + readonly execArgv: readonly string[]; + readonly optionsJson: string; + readonly tempDirPrefix: string; + }) => Effect.Effect; +} + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/stryker-js-engine/etc/worker.api.md b/packages/stryker-js-engine/etc/worker.api.md new file mode 100644 index 000000000..9fd3862b5 --- /dev/null +++ b/packages/stryker-js-engine/etc/worker.api.md @@ -0,0 +1,222 @@ +## API Report File for "@systemfsoftware/stryker-js-engine" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { AnyPluginContribution } from '@systemfsoftware/stryker-js-plugin-interface'; +import { CheckerFailed } from '@systemfsoftware/stryker-js-language'; +import { ContributionOf } from '@systemfsoftware/stryker-js-plugin-interface'; +import * as Effect from 'effect/Effect'; +import * as FileSystem from 'effect/FileSystem'; +import * as HashMap from 'effect/HashMap'; +import { Module } from '@systemfsoftware/stryker-js-language'; +import { Mutant } from '@systemfsoftware/stryker-js-language'; +import * as Path from 'effect/Path'; +import { PluginContribution } from '@systemfsoftware/stryker-js-plugin-interface'; +import { PluginKind } from '@systemfsoftware/stryker-js-plugin-interface'; +import * as Rpc from 'effect/unstable/rpc/Rpc'; +import * as RpcGroup from 'effect/unstable/rpc/RpcGroup'; +import * as S from 'effect/Schema'; +import { Schema } from 'effect'; +import { StrykerOptions } from '@systemfsoftware/stryker-js-language'; +import { TestRunnerFailed } from '@systemfsoftware/stryker-js-language'; +import { YieldableError } from 'effect/Cause'; + +// @public (undocumented) +export const CheckerRpcs: RpcGroup.RpcGroup; +}>, Schema.$Record; +}>, Schema.Struct<{ + readonly status: Schema.Literal<"compileError">; + readonly reason: Schema.String; +}>]>>, typeof CheckerFailed, never, never> | Rpc.Rpc<"group", Schema.Struct<{ + checkerName: Schema.String; + mutants: Schema.$Array; +}>, Schema.$Array>, typeof CheckerFailed, never, never>>; + +// Warning: (ae-forgotten-export) The symbol "PluginNotFoundError" needs to be exported by the entry point worker.d.mts +// +// @public (undocumented) +export function create(pluginsByKind: HashMap.HashMap, kind: K, name: string): Effect.Effect, PluginNotFoundError>; + +// @public (undocumented) +export const decodeWorkerOptions: (raw: string) => Effect.Effect<{ + readonly [x: string]: unknown; + readonly allowConsoleColors: boolean; + readonly buildCommand?: string | undefined; + readonly checkers: readonly string[]; + readonly checkerNodeArgs: readonly string[]; + readonly concurrency?: string | number | undefined; + readonly commandRunner: { + readonly [x: string]: unknown; + readonly command: string; + }; + readonly coverageAnalysis: "all" | "off" | "perTest"; + readonly clearTextReporter: { + readonly [x: string]: unknown; + readonly allowColor: boolean; + readonly allowEmojis: boolean; + readonly logTests: boolean; + readonly maxTestsToLog: number; + readonly reportTests: boolean; + readonly reportMutants: boolean; + readonly reportScoreTable: boolean; + readonly skipFull: boolean; + }; + readonly dryRunOnly: boolean; + readonly ignorePatterns: readonly string[]; + readonly ignoreStatic: boolean; + readonly incremental: boolean; + readonly incrementalFile: string; + readonly progressStreamFile: string; + readonly force: boolean; + readonly fileLogLevel: "debug" | "error" | "fatal" | "info" | "off" | "trace" | "warn"; + readonly inPlace: boolean; + readonly logLevel: "debug" | "error" | "fatal" | "info" | "off" | "trace" | "warn"; + readonly maxConcurrentTestRunners: number; + readonly maxTestRunnerReuse: number; + readonly mutate: readonly string[]; + readonly mutator: { + readonly excludedMutations: readonly string[]; + }; + readonly packageManager?: "npm" | "pnpm" | "yarn" | undefined; + readonly plugins: readonly string[]; + readonly appendPlugins: readonly string[]; + readonly reporters: readonly string[]; + readonly htmlReporter: { + readonly fileName: string; + }; + readonly jsonReporter: { + readonly fileName: string; + }; + readonly disableTypeChecks: string | boolean; + readonly symlinkNodeModules: boolean; + readonly tempDirName: string; + readonly cleanTempDir: "always" | boolean; + readonly testRunner: string; + readonly testRunnerNodeArgs: readonly string[]; + readonly thresholds: { + readonly high: number; + readonly low: number; + readonly break: number | null; + }; + readonly timeoutFactor: number; + readonly timeoutMS: number; + readonly dryRunTimeoutMinutes: number; + readonly tsconfigFile: string; + readonly warnings: boolean | { + readonly [x: string]: unknown; + readonly unknownOptions: boolean; + readonly preprocessorErrors: boolean; + readonly unserializableOptions: boolean; + readonly slow: boolean; + }; + readonly disableBail: boolean; + readonly allowEmpty: boolean; + readonly ignorers: readonly string[]; + readonly testFiles: readonly string[]; +}, Schema.SchemaError, never>; + +// @public (undocumented) +export const encodeWorkerOptions: (options: StrykerOptions) => Effect.Effect; + +// Warning: (ae-forgotten-export) The symbol "LoadedPlugins" needs to be exported by the entry point worker.d.mts +// Warning: (ae-forgotten-export) The symbol "PluginLoadFailedError" needs to be exported by the entry point worker.d.mts +// +// @public (undocumented) +export function loadPlugins(pluginDescriptors: readonly string[], basePath: string): Effect.Effect; + +// @public +export const MutantCoverageSchema: S.Struct<{ + readonly static: S.$Record; + readonly perTest: S.$Record>; +}>; + +// @public (undocumented) +export const TestRunnerRpcs: RpcGroup.RpcGroup, typeof TestRunnerFailed, never, never> | Rpc.Rpc<"dryRun", Schema.Struct<{ + options: Schema.Struct<{ + readonly timeout: Schema.Finite; + readonly disableBail: Schema.Boolean; + readonly coverageAnalysis: Schema.Literals; + readonly files: Schema.optionalKey>; + readonly testFiles: Schema.optionalKey>; + }>; +}>, Schema.Union; + readonly tests: Schema.$Array; + readonly startPosition: Schema.optionalKey>; + readonly status: Schema.Literal<"failed">; + readonly failureMessage: Schema.String; + }>, Schema.Struct<{ + readonly id: Schema.String; + readonly name: Schema.String; + readonly timeSpentMs: Schema.Finite; + readonly fileName: Schema.optionalKey; + readonly startPosition: Schema.optionalKey>; + readonly status: Schema.Literal<"skipped">; + }>, Schema.Struct<{ + readonly id: Schema.String; + readonly name: Schema.String; + readonly timeSpentMs: Schema.Finite; + readonly fileName: Schema.optionalKey; + readonly startPosition: Schema.optionalKey>; + readonly status: Schema.Literal<"success">; + }>]>>; + readonly mutantCoverage: Schema.optionalKey>; + readonly static: Schema.$Record; + }>>; +}>, Schema.Struct<{ + readonly status: Schema.Literal<"timeout">; + readonly reason: Schema.optionalKey; +}>, Schema.Struct<{ + readonly status: Schema.Literal<"error">; + readonly errorMessage: Schema.String; +}>]>, typeof TestRunnerFailed, never, never> | Rpc.Rpc<"mutantRun", Schema.Struct<{ + options: Schema.Struct<{ + readonly timeout: Schema.Finite; + readonly disableBail: Schema.Boolean; + readonly activeMutant: typeof Mutant; + readonly sandboxFileName: Schema.String; + readonly mutantActivation: Schema.Literals; + readonly reloadEnvironment: Schema.Boolean; + readonly testFilter: Schema.optionalKey>; + readonly hitLimit: Schema.optionalKey; + }>; +}>, Schema.Union; + readonly killedBy: Schema.$Array; + readonly failureMessage: Schema.String; + readonly nrOfTests: Schema.Finite; +}>, Schema.Struct<{ + readonly status: Schema.Literal<"survived">; + readonly nrOfTests: Schema.Finite; +}>, Schema.Struct<{ + readonly status: Schema.Literal<"timeout">; + readonly reason: Schema.optionalKey; +}>, Schema.Struct<{ + readonly status: Schema.Literal<"error">; + readonly errorMessage: Schema.String; +}>]>, typeof TestRunnerFailed, never, never>>; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/stryker-js-engine/package.json b/packages/stryker-js-engine/package.json index 0671c7488..716f624bf 100644 --- a/packages/stryker-js-engine/package.json +++ b/packages/stryker-js-engine/package.json @@ -7,7 +7,9 @@ "directory": "packages/stryker-js-engine" }, "homepage": "https://github.com/systemfsoftware/stryker-js-effect", - "bugs": "https://github.com/systemfsoftware/stryker-js-effect/issues", + "bugs": { + "url": "https://github.com/systemfsoftware/stryker-js-effect/issues" + }, "description": "The host-neutral mutation engine — the run, its stages, and the ports a process entry binds to start it.", "type": "module", "files": [ @@ -16,9 +18,11 @@ "scripts": { "typecheck": "tsc --noEmit --incremental && tsc -p tsconfig.node.json --noEmit", "test": "vitest run", - "build": "tsdown", + "build": "tsdown && pnpm api:check", "lint": "f=${OXLINT_FORMAT:-${AGENT:+agent}}; oxlint --format=${f:-default}", - "attw": "attw --pack ." + "attw": "attw --pack .", + "api:check": "tsdown && concurrently --kill-others-on-fail \"api-extractor run\" \"api-extractor run --config api-extractor.builtin-reporters.json\" \"api-extractor run --config api-extractor.base.json\" \"api-extractor run --config api-extractor.plugin-loader.json\" \"api-extractor run --config api-extractor.worker.json\"", + "api:update": "concurrently --kill-others-on-fail \"api-extractor run --local\" \"api-extractor run --local --config api-extractor.builtin-reporters.json\" \"api-extractor run --local --config api-extractor.base.json\" \"api-extractor run --local --config api-extractor.plugin-loader.json\" \"api-extractor run --local --config api-extractor.worker.json\"" }, "dependencies": { "@noble/hashes": "catalog:", @@ -35,6 +39,7 @@ }, "devDependencies": { "@effect/vitest": "catalog:", + "@microsoft/api-extractor": "catalog:", "@std/jsonc": "catalog:", "@systemfsoftware/all": "catalog:", "@systemfsoftware/arethetypeswrong-cli": "catalog:", @@ -43,6 +48,7 @@ "@systemfsoftware/vitest-config": "workspace:^", "@types/diff-match-patch": "1.0.36", "@types/node": "catalog:", + "concurrently": "catalog:", "effect": "catalog:", "oxlint": "catalog:", "oxlint-tsgolint": "catalog:", @@ -57,6 +63,7 @@ "exports": { ".": "./dist/index.mjs", "./builtin-reporters": "./dist/builtin-reporters.mjs", + "./plugin-loader": "./dist/plugin-loader.mjs", "./config/base": "./dist/config/base.mjs", "./worker": "./dist/worker.mjs", "./package.json": "./package.json" @@ -66,6 +73,7 @@ ".": "./dist/index.mjs", "./builtin-reporters": "./dist/builtin-reporters.mjs", "./config/base": "./dist/config/base.mjs", + "./plugin-loader": "./dist/plugin-loader.mjs", "./worker": "./dist/worker.mjs", "./package.json": "./package.json" }, diff --git a/packages/stryker-js-engine/src/Plugins.schema.ts b/packages/stryker-js-engine/src/Plugins.schema.ts index a3148f901..5b2a0fd9a 100644 --- a/packages/stryker-js-engine/src/Plugins.schema.ts +++ b/packages/stryker-js-engine/src/Plugins.schema.ts @@ -2,31 +2,25 @@ * Plugins capability — declarations for plugin module shapes and load failures. */ -import { PluginKind } from '@systemfsoftware/stryker-js-plugin-interface' -import type { PluginContribution } from '@systemfsoftware/stryker-js-plugin-interface' import { Schema as S } from 'effect' -import * as SSchema from 'effect/Schema' - -const isPluginContribution = (_value: unknown): _value is PluginContribution => true -const PluginContributionSchema = SSchema.Unknown.pipe(SSchema.refine(isPluginContribution)) - -export class PluginLoaderEntry extends SSchema.Class('PluginLoaderEntry')({ - moduleName: SSchema.String, - plugins: SSchema.optional(SSchema.Array(PluginContributionSchema)), - schemaContribution: SSchema.optional(SSchema.Record(SSchema.String, SSchema.Unknown)), -}) {} - -export class LoadPluginsCommand extends SSchema.Class('LoadPluginsCommand')({ - entries: SSchema.Array(PluginLoaderEntry), -}) {} export const PluginModuleSchema = S.Struct({ strykerPlugins: S.Array(S.Unknown), }) +const isShouldIgnore = (value: unknown): value is (path: unknown) => string | undefined => typeof value === 'function' + +export const PlainIgnorerSchema = S.Struct({ + name: S.String, + shouldIgnore: S.declare(isShouldIgnore), +}) + export const SchemaValidationContributionSchema = S.Struct({ strykerValidationSchema: S.Record(S.String, S.Unknown), }) +export const PlainIgnorerModuleSchema = S.Struct({ + strykerIgnorers: S.Array(PlainIgnorerSchema), +}) export class PluginNotFoundError extends S.TaggedError()( 'PluginNotFoundError', diff --git a/packages/stryker-js-engine/src/Plugins.ts b/packages/stryker-js-engine/src/Plugins.ts index b8f30acf1..ae99b4901 100644 --- a/packages/stryker-js-engine/src/Plugins.ts +++ b/packages/stryker-js-engine/src/Plugins.ts @@ -3,22 +3,27 @@ import * as Effect from 'effect/Effect' import * as FileSystem from 'effect/FileSystem' import * as HashMap from 'effect/HashMap' import * as HashSet from 'effect/HashSet' +import * as Layer from 'effect/Layer' import * as Match from 'effect/Match' import * as Option from 'effect/Option' import * as Path from 'effect/Path' import * as Predicate from 'effect/Predicate' +import * as Result from 'effect/Result' import { Module } from '@systemfsoftware/stryker-js-language' +import { Ignorer, type NodePath } from '@systemfsoftware/stryker-js-language' import type { AnyPluginContribution, ContributionOf, PluginContribution, } from '@systemfsoftware/stryker-js-plugin-interface' +import { declarePlugin } from '@systemfsoftware/stryker-js-plugin-interface' import type { PluginKind } from '@systemfsoftware/stryker-js-plugin-interface' import { defaultOptions, importModule } from './Config.js' import { StrykerError } from './stryker-error.schema.js' import { + PlainIgnorerModuleSchema, PluginLoadFailedError, PluginModuleSchema, PluginNotFoundError, @@ -173,6 +178,15 @@ interface PluginModule { strykerPlugins: readonly PluginContribution[] } +interface PlainIgnorerEntry { + readonly name: string + shouldIgnore(path: NodePath): string | undefined +} + +interface PlainIgnorerModule { + strykerIgnorers: readonly PlainIgnorerEntry[] +} + interface SchemaValidationContribution { strykerValidationSchema: Record } @@ -412,12 +426,32 @@ const recoverPluginImportFailure = ( Match.orElse(() => failPluginLoad(descriptor, error)), ) +const plainIgnorerContribution = (ignorer: PlainIgnorerEntry): PluginContribution<'Ignore'> => + declarePlugin( + 'Ignore', + ignorer.name, + Layer.succeed(Ignorer, { + shouldIgnore: (path) => Option.fromUndefinedOr(ignorer.shouldIgnore(path)), + }), + ) + const modulePluginContributions = (module: unknown): readonly PluginContribution[] | undefined => Match.value(module).pipe( Match.when(isPluginModule, (pluginModule: PluginModule) => pluginModule.strykerPlugins), Match.orElse((): undefined => undefined), ) +const modulePlainIgnorers = ( + module: unknown, +): Result.Result[] | undefined, S.SchemaError> => + Match.value(Predicate.hasProperty(module, 'strykerIgnorers')).pipe( + Match.when(true, () => + S.decodeUnknownResult(PlainIgnorerModuleSchema)(module).pipe( + Result.map((plain: PlainIgnorerModule) => plain.strykerIgnorers.map(plainIgnorerContribution)), + )), + Match.orElse(() => Result.succeed(undefined)), + ) + const moduleSchemaContribution = (module: unknown): Record | undefined => Match.value(module).pipe( Match.when( @@ -427,10 +461,28 @@ const moduleSchemaContribution = (module: unknown): Record | un Match.orElse((): undefined => undefined), ) -const pluginContributionsOf = (module: unknown): PluginContributions => ({ - plugins: modulePluginContributions(module), - schemaContribution: moduleSchemaContribution(module), -}) +const mergeContributions = ( + native: readonly PluginContribution[] | undefined, + plain: readonly PluginContribution<'Ignore'>[] | undefined, +): readonly PluginContribution[] | undefined => + Option.match(Option.fromUndefinedOr(native), { + onNone: () => plain, + onSome: (plugins) => + Option.match(Option.fromUndefinedOr(plain), { + onNone: () => plugins, + onSome: (wrapped) => [...plugins, ...wrapped], + }), + }) + +const pluginContributionsOf = ( + module: unknown, +): Result.Result => + modulePlainIgnorers(module).pipe( + Result.map((plain) => ({ + plugins: mergeContributions(modulePluginContributions(module), plain), + schemaContribution: moduleSchemaContribution(module), + })), + ) const hasContribution = (contributions: PluginContributions): boolean => Match.value(contributions.plugins !== undefined).pipe( @@ -440,17 +492,21 @@ const hasContribution = (contributions: PluginContributions): boolean => const warnUndescribedPluginModule = (descriptor: string): Effect.Effect => Effect.logWarning( - `Module "${descriptor}" did not contribute a StrykerJS plugin. It didn't export a "strykerPlugins" or "strykerValidationSchema".`, + `Module "${descriptor}" did not contribute a StrykerJS plugin. It didn't export a "strykerPlugins", "strykerIgnorers", or "strykerValidationSchema".`, ).pipe(Effect.as(undefined)) const describeLoadedPlugin = ( descriptor: string, module: unknown, -): Effect.Effect => - Match.value(pluginContributionsOf(module)).pipe( - Match.when(hasContribution, (contributions: PluginContributions) => Effect.succeed(contributions)), - Match.orElse(() => warnUndescribedPluginModule(descriptor)), - ) +): Effect.Effect => + Result.match(pluginContributionsOf(module), { + onFailure: (cause) => failPluginLoad(descriptor, cause), + onSuccess: (contributions) => + Match.value(hasContribution(contributions)).pipe( + Match.when(true, () => Effect.succeed(contributions)), + Match.orElse(() => warnUndescribedPluginModule(descriptor)), + ), + }) function loadPlugin( descriptor: string, diff --git a/packages/stryker-js-engine/src/config/base.ts b/packages/stryker-js-engine/src/config/base.ts index 0b23c6be6..667784c61 100644 --- a/packages/stryker-js-engine/src/config/base.ts +++ b/packages/stryker-js-engine/src/config/base.ts @@ -17,8 +17,7 @@ const basePreset: BasePreset = { plugins: [ '@systemfsoftware/stryker-js-vitest-runner', '@systemfsoftware/stryker-js-typescript-checker', - '@systemfsoftware/stryker-plugins/effect-schema-ignorer', - '@systemfsoftware/stryker-plugins/workflow-make-ignorer', + '@systemfsoftware/stryker-ignorer-effect-schema-declarations', '@systemfsoftware/stryker-test-contribution', ], reporters: ['progress', 'html', 'json', 'progress-stream'], diff --git a/packages/stryker-js-engine/src/mutation-reporting.ts b/packages/stryker-js-engine/src/mutation-reporting.ts index e7d7467e4..b1672ece5 100644 --- a/packages/stryker-js-engine/src/mutation-reporting.ts +++ b/packages/stryker-js-engine/src/mutation-reporting.ts @@ -200,7 +200,7 @@ export const makeMutationReportingService = (input: MakeMutationReportingInput): const MANIFEST_SPECIFIERS = [ '@systemfsoftware/stryker-js-vitest-runner', '@systemfsoftware/stryker-js-typescript-checker', - '@systemfsoftware/stryker-plugins', + '@systemfsoftware/stryker-ignorer-effect-schema-declarations', 'vitest', 'karma', 'karma-chai', diff --git a/packages/stryker-js-engine/src/plugin-loader.ts b/packages/stryker-js-engine/src/plugin-loader.ts new file mode 100644 index 000000000..fa04a09f5 --- /dev/null +++ b/packages/stryker-js-engine/src/plugin-loader.ts @@ -0,0 +1,3 @@ +export { create, createAll, loadPlugins } from './Plugins.js' +export type { LoadedPlugins, PluginLoaderEntryLike, PluginLoadPlan } from './Plugins.js' +export { PluginLoadFailedError, PluginNotFoundError } from './Plugins.schema.js' diff --git a/packages/stryker-js-engine/tests/__fixtures__/both-protocols.fixture.mjs b/packages/stryker-js-engine/tests/__fixtures__/both-protocols.fixture.mjs new file mode 100644 index 000000000..dcfc98a80 --- /dev/null +++ b/packages/stryker-js-engine/tests/__fixtures__/both-protocols.fixture.mjs @@ -0,0 +1,7 @@ +import { declarePlugin } from '@systemfsoftware/stryker-js-plugin-interface' + +export const strykerPlugins = [declarePlugin('Reporter', 'native-fixture-reporter', () => ({}))] + +export const strykerIgnorers = [ + { name: 'plain-fixture-rule', shouldIgnore: () => 'plain reason' }, +] diff --git a/packages/stryker-js-engine/tests/__fixtures__/invalid-plain-entry.fixture.mjs b/packages/stryker-js-engine/tests/__fixtures__/invalid-plain-entry.fixture.mjs new file mode 100644 index 000000000..52728684c --- /dev/null +++ b/packages/stryker-js-engine/tests/__fixtures__/invalid-plain-entry.fixture.mjs @@ -0,0 +1 @@ +export const strykerIgnorers = [{ shouldIgnore: 'not a function' }] diff --git a/packages/stryker-js-engine/tests/__fixtures__/plain-ignorer-only.fixture.mjs b/packages/stryker-js-engine/tests/__fixtures__/plain-ignorer-only.fixture.mjs new file mode 100644 index 000000000..d2f932aae --- /dev/null +++ b/packages/stryker-js-engine/tests/__fixtures__/plain-ignorer-only.fixture.mjs @@ -0,0 +1,4 @@ +export const strykerIgnorers = [ + { name: 'plain-fixture-rule', shouldIgnore: () => 'fixture reason' }, + { name: 'plain-fixture-never', shouldIgnore: () => undefined }, +] diff --git a/packages/stryker-js-engine/tests/__fixtures__/plain-ignorer-shadowed.fixture.mjs b/packages/stryker-js-engine/tests/__fixtures__/plain-ignorer-shadowed.fixture.mjs new file mode 100644 index 000000000..4430726d6 --- /dev/null +++ b/packages/stryker-js-engine/tests/__fixtures__/plain-ignorer-shadowed.fixture.mjs @@ -0,0 +1,4 @@ +export const strykerIgnorers = [ + { name: 'duplicated-rule', shouldIgnore: () => 'first' }, + { name: 'duplicated-rule', shouldIgnore: () => 'last' }, +] diff --git a/packages/stryker-js-engine/tests/plain-ignorer-loader.integration.test.ts b/packages/stryker-js-engine/tests/plain-ignorer-loader.integration.test.ts new file mode 100644 index 000000000..2b4b5ed86 --- /dev/null +++ b/packages/stryker-js-engine/tests/plain-ignorer-loader.integration.test.ts @@ -0,0 +1,152 @@ +import { Gherkin, Given, it, layer, makeFeature, Then, When } from '@systemfsoftware/effect-gherkin-spec' +import { createDefaultOptions } from '@systemfsoftware/stryker-js-engine' +import { create, createAll, loadPlugins, PluginLoadFailedError } from '@systemfsoftware/stryker-js-engine/plugin-loader' +import { Ignorer, Module, type NodePath } from '@systemfsoftware/stryker-js-language' +import type { ModuleRequire } from '@systemfsoftware/stryker-js-language' +import { RunConfiguration, SandboxDirectory } from '@systemfsoftware/stryker-js-plugin-interface' +import * as Context from 'effect/Context' +import * as Effect from 'effect/Effect' +import * as FileSystem from 'effect/FileSystem' +import * as Layer from 'effect/Layer' +import * as Option from 'effect/Option' +import * as Path from 'effect/Path' +import { expect } from 'vitest' + +const Feature = makeFeature({ it, layer }) + +interface NodeModuleShape { + createRequire(filename: string | URL): NodeRequire + isBuiltin(moduleName: string): boolean +} + +const EMPTY_PATHS: readonly string[] = [] + +const makeModuleRequire = (nodeModule: NodeModuleShape, filename: string | URL): ModuleRequire => { + const requireFrom: NodeRequire = nodeModule.createRequire(filename) + const requireFn: ModuleRequire = (request: string): unknown => requireFrom(request) + requireFn.resolve = (request, options) => + Option.match(Option.fromUndefinedOr(options), { + onNone: () => requireFrom.resolve(request), + onSome: (present) => + requireFrom.resolve(request, { + paths: [...Option.getOrElse(Option.fromNullishOr(present.paths), () => EMPTY_PATHS)], + }), + }) + return requireFn +} + +const moduleLayer = Layer.effect( + Module, + Effect.sync(() => { + const nodeModule: NodeModuleShape = process.getBuiltinModule('node:module') + return { + createRequire: (filename: string | URL) => makeModuleRequire(nodeModule, filename), + isBuiltin: (moduleName: string) => nodeModule.isBuiltin(moduleName), + } + }), +) + +const pluginEnvironmentLayer = Layer.mergeAll( + FileSystem.layerNoop({}), + Path.layer, + moduleLayer, + Layer.succeed(RunConfiguration, Effect.runSync(createDefaultOptions())), + Layer.succeed(SandboxDirectory, '/tmp'), +) + +const fixturePath = (name: string): string => `${process.cwd()}/tests/__fixtures__/${name}` + +const loadFixture = (name: string) => + loadPlugins([fixturePath(name)], process.cwd()).pipe( + Effect.provide(Layer.mergeAll(FileSystem.layerNoop({}), Path.layer, moduleLayer)), + ) + +const pathOf = (node: unknown): NodePath => ({ node, ancestors: [] }) + +Feature('Loading plain ignorer plugins') + .body(({ scenario }) => { + scenario( + 'A module exporting only plain ignorers loads every entry as an ignore contribution', + Gherkin.Do.pipe( + Given('a module exporting one plain ignorer and nothing else')( + 'loaded', + () => loadFixture('plain-ignorer-only.fixture.mjs'), + ), + When('each entry is selected by kind and name and its layer is built')( + 'services', + (s) => + Effect.gen(function*() { + const rule = yield* create(s.loaded.pluginsByKind, 'Ignore', 'plain-fixture-rule') + const never = yield* create(s.loaded.pluginsByKind, 'Ignore', 'plain-fixture-never') + const ruleContext = yield* Layer.build(rule.layer).pipe(Effect.provide(pluginEnvironmentLayer)) + const neverContext = yield* Layer.build(never.layer).pipe(Effect.provide(pluginEnvironmentLayer)) + return { + rule: Context.get(ruleContext, Ignorer), + never: Context.get(neverContext, Ignorer), + } + }), + ), + Then('every entry registers under its name and its decisions flow through')((s) => + Effect.sync(() => { + expect(Option.getOrThrow(s.services.rule.shouldIgnore(pathOf('any-node')))).toBe('fixture reason') + expect(Option.isNone(s.services.never.shouldIgnore(pathOf('any-node')))).toBe(true) + }) + ), + ), + ) + + scenario( + 'A module exporting both protocols keeps both kinds', + Gherkin.Do.pipe( + Given('a module exporting a native reporter and a plain ignorer')( + 'loaded', + () => loadFixture('both-protocols.fixture.mjs'), + ), + When('the loaded contributions are read back by kind')('names', (s) => + Effect.sync(() => ({ + reporters: createAll(s.loaded.pluginsByKind, 'Reporter').pipe(Effect.runSync).map((c) => c.name), + ignorers: createAll(s.loaded.pluginsByKind, 'Ignore').pipe(Effect.runSync).map((c) => c.name), + }))), + Then('both kinds are present')((s) => + Effect.sync(() => { + expect(s.names.reporters).toStrictEqual(['native-fixture-reporter']) + expect(s.names.ignorers).toStrictEqual(['plain-fixture-rule']) + }) + ), + ), + ) + + scenario( + 'A malformed plain entry fails the load with a named error', + Gherkin.Do.pipe( + Given('a module whose plain entry has no name and no callable decision')( + 'outcome', + () => loadFixture('invalid-plain-entry.fixture.mjs').pipe(Effect.flip), + ), + Then('the load is rejected naming the module')((s) => + Effect.sync(() => { + expect(s.outcome).toBeInstanceOf(PluginLoadFailedError) + expect(s.outcome.descriptor).toContain('invalid-plain-entry.fixture.mjs') + }) + ), + ), + ) + + scenario( + 'Two plain entries sharing a name both load under that name', + Gherkin.Do.pipe( + Given('a module exporting two plain entries with the same name')( + 'names', + () => + loadFixture('plain-ignorer-shadowed.fixture.mjs').pipe( + Effect.map((loaded) => createAll(loaded.pluginsByKind, 'Ignore').pipe(Effect.runSync).map((c) => c.name)), + ), + ), + Then('both entries appear under the shared name')((s) => + Effect.sync(() => { + expect(s.names).toStrictEqual(['duplicated-rule', 'duplicated-rule']) + }) + ), + ), + ) + }) diff --git a/packages/stryker-js-engine/tsconfig.api-base.json b/packages/stryker-js-engine/tsconfig.api-base.json new file mode 100644 index 000000000..947ecd4d8 --- /dev/null +++ b/packages/stryker-js-engine/tsconfig.api-base.json @@ -0,0 +1,6 @@ +{ + "extends": "@systemfsoftware/tsconfig/tsc/no-dom/library", + "include": [ + "dist/config/base.d.mts" + ] +} diff --git a/packages/stryker-js-engine/tsconfig.api-builtin-reporters.json b/packages/stryker-js-engine/tsconfig.api-builtin-reporters.json new file mode 100644 index 000000000..f1ec2ad7e --- /dev/null +++ b/packages/stryker-js-engine/tsconfig.api-builtin-reporters.json @@ -0,0 +1,6 @@ +{ + "extends": "@systemfsoftware/tsconfig/tsc/no-dom/library", + "include": [ + "dist/builtin-reporters.d.mts" + ] +} diff --git a/packages/stryker-js-engine/tsconfig.api-plugin-loader.json b/packages/stryker-js-engine/tsconfig.api-plugin-loader.json new file mode 100644 index 000000000..4c8e1dd98 --- /dev/null +++ b/packages/stryker-js-engine/tsconfig.api-plugin-loader.json @@ -0,0 +1,6 @@ +{ + "extends": "@systemfsoftware/tsconfig/tsc/no-dom/library", + "include": [ + "dist/plugin-loader.d.mts" + ] +} diff --git a/packages/stryker-js-engine/tsconfig.api-worker.json b/packages/stryker-js-engine/tsconfig.api-worker.json new file mode 100644 index 000000000..53b041b7d --- /dev/null +++ b/packages/stryker-js-engine/tsconfig.api-worker.json @@ -0,0 +1,6 @@ +{ + "extends": "@systemfsoftware/tsconfig/tsc/no-dom/library", + "include": [ + "dist/worker.d.mts" + ] +} diff --git a/packages/stryker-js-engine/tsdown.config.ts b/packages/stryker-js-engine/tsdown.config.ts index 3d75ef6b9..1e229cdb1 100644 --- a/packages/stryker-js-engine/tsdown.config.ts +++ b/packages/stryker-js-engine/tsdown.config.ts @@ -5,6 +5,7 @@ export default defineConfig({ index: './src/index.ts', 'config/base': './src/config/base.ts', 'builtin-reporters': './src/builtin-reporters.ts', + 'plugin-loader': './src/plugin-loader.ts', worker: './src/worker-wiring.ts', }, format: 'esm', diff --git a/packages/stryker-js-html-reporter/api-extractor.json b/packages/stryker-js-html-reporter/api-extractor.json new file mode 100644 index 000000000..409ab944c --- /dev/null +++ b/packages/stryker-js-html-reporter/api-extractor.json @@ -0,0 +1,55 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "projectFolder": ".", + "mainEntryPointFilePath": "/dist/index.d.mts", + "bundledPackages": [], + "compiler": { + "tsconfigFilePath": "/tsconfig.json" + }, + "apiReport": { + "enabled": true, + "reportFolder": "/etc/", + "reportTempFolder": "/temp/main/", + "reportFileName": "stryker-js-html-reporter.api.md" + }, + "docModel": { + "enabled": false + }, + "dtsRollup": { + "enabled": true, + "untrimmedFilePath": "/dist/index.d.mts" + }, + "tsdocMetadata": { + "enabled": false + }, + "messages": { + "extractorMessageReporting": { + "default": { + "logLevel": "warning" + }, + "ae-forgotten-export": { + "logLevel": "error" + }, + "ae-missing-release-tag": { + "logLevel": "none" + }, + "ae-internal-missing-underscore": { + "logLevel": "none" + } + }, + "tsdocMessageReporting": { + "default": { + "logLevel": "warning" + }, + "tsdoc-undefined-tag": { + "logLevel": "none" + }, + "tsdoc-unsupported-tag": { + "logLevel": "none" + }, + "tsdoc-param-tag-missing-hyphen": { + "logLevel": "none" + } + } + } +} diff --git a/packages/stryker-js-html-reporter/etc/stryker-js-html-reporter.api.md b/packages/stryker-js-html-reporter/etc/stryker-js-html-reporter.api.md new file mode 100644 index 000000000..20071b071 --- /dev/null +++ b/packages/stryker-js-html-reporter/etc/stryker-js-html-reporter.api.md @@ -0,0 +1,21 @@ +## API Report File for "@systemfsoftware/stryker-js-html-reporter" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { PluginReporterContribution } from '@systemfsoftware/stryker-js-plugin-interface'; +import { ReporterFactory } from '@systemfsoftware/stryker-js-language'; + +// @public (undocumented) +export const htmlReporterFactory: ReporterFactory; + +// @public (undocumented) +export const makeHtmlReporter: ReporterFactory; + +// @public (undocumented) +export const strykerPlugins: PluginReporterContribution[]; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/stryker-js-html-reporter/package.json b/packages/stryker-js-html-reporter/package.json index f5a3f4f99..5a918a43e 100644 --- a/packages/stryker-js-html-reporter/package.json +++ b/packages/stryker-js-html-reporter/package.json @@ -15,11 +15,13 @@ ], "scripts": { "clean": "rimraf dist", - "build": "tsdown", + "build": "tsdown && pnpm api:check", "typecheck": "tsc --noEmit --incremental && tsc -p tsconfig.node.json --noEmit", "attw": "attw --pack .", "test": "vitest run", - "lint": "f=${OXLINT_FORMAT:-${AGENT:+agent}}; oxlint --format=${f:-default}" + "lint": "f=${OXLINT_FORMAT:-${AGENT:+agent}}; oxlint --format=${f:-default}", + "api:check": "tsdown && api-extractor run", + "api:update": "api-extractor run --local" }, "dependencies": { "@effect/platform-node-shared": "catalog:", @@ -29,6 +31,7 @@ "mutation-testing-elements": "catalog:stryker" }, "devDependencies": { + "@microsoft/api-extractor": "catalog:", "@systemfsoftware/all": "catalog:", "@systemfsoftware/arethetypeswrong-cli": "catalog:", "@systemfsoftware/effect-gherkin-spec": "catalog:", diff --git a/packages/stryker-js-instrumenter/api-extractor.json b/packages/stryker-js-instrumenter/api-extractor.json new file mode 100644 index 000000000..a7336aeca --- /dev/null +++ b/packages/stryker-js-instrumenter/api-extractor.json @@ -0,0 +1,55 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "projectFolder": ".", + "mainEntryPointFilePath": "/dist/index.d.mts", + "bundledPackages": [], + "compiler": { + "tsconfigFilePath": "/tsconfig.json" + }, + "apiReport": { + "enabled": true, + "reportFolder": "/etc/", + "reportTempFolder": "/temp/main/", + "reportFileName": "stryker-js-instrumenter.api.md" + }, + "docModel": { + "enabled": false + }, + "dtsRollup": { + "enabled": true, + "untrimmedFilePath": "/dist/index.d.mts" + }, + "tsdocMetadata": { + "enabled": false + }, + "messages": { + "extractorMessageReporting": { + "default": { + "logLevel": "warning" + }, + "ae-forgotten-export": { + "logLevel": "error" + }, + "ae-missing-release-tag": { + "logLevel": "none" + }, + "ae-internal-missing-underscore": { + "logLevel": "none" + } + }, + "tsdocMessageReporting": { + "default": { + "logLevel": "warning" + }, + "tsdoc-undefined-tag": { + "logLevel": "none" + }, + "tsdoc-unsupported-tag": { + "logLevel": "none" + }, + "tsdoc-param-tag-missing-hyphen": { + "logLevel": "none" + } + } + } +} diff --git a/packages/stryker-js-instrumenter/etc/stryker-js-instrumenter.api.md b/packages/stryker-js-instrumenter/etc/stryker-js-instrumenter.api.md new file mode 100644 index 000000000..10125ad81 --- /dev/null +++ b/packages/stryker-js-instrumenter/etc/stryker-js-instrumenter.api.md @@ -0,0 +1,59 @@ +## API Report File for "@systemfsoftware/stryker-js-instrumenter" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import * as Effect from 'effect/Effect'; +import { FileDescription } from '@systemfsoftware/stryker-js-language'; +import { IgnorerService } from '@systemfsoftware/stryker-js-language'; +import { Mutant } from '@systemfsoftware/stryker-js-language'; +import * as S from 'effect/Schema'; +import { YieldableError } from 'effect/Cause'; + +// @public (undocumented) +export const angularIgnorer: IgnorerService; + +// @public (undocumented) +export function disableTypeChecks(file: File_2): Promise; + +// @public (undocumented) +interface File_2 extends FileDescription { + // (undocumented) + content: string; + // (undocumented) + name: string; +} +export { File_2 as File } + +// @public (undocumented) +export const frameworkPluginsFileUrl: string; + +// Warning: (ae-forgotten-export) The symbol "InstrumentResult$1" needs to be exported by the entry point index.d.mts +// Warning: (ae-forgotten-export) The symbol "InstrumentError" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export const instrument: (files: readonly File_2[], options: InstrumenterOptions) => Effect.Effect; + +// Warning: (ae-forgotten-export) The symbol "InstrumenterOptionsSchema" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export type InstrumenterOptions = typeof InstrumenterOptionsSchema.Type; + +// @public (undocumented) +export interface InstrumentResult { + // (undocumented) + files: readonly File_2[]; + // (undocumented) + mutants: readonly Mutant[]; +} + +// @public (undocumented) +export interface ParserOptions {} + +// @public (undocumented) +export const strykerPlugins: readonly unknown[]; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/stryker-js-instrumenter/package.json b/packages/stryker-js-instrumenter/package.json index e504005f0..2896d38c2 100644 --- a/packages/stryker-js-instrumenter/package.json +++ b/packages/stryker-js-instrumenter/package.json @@ -19,11 +19,13 @@ }, "scripts": { "clean": "rimraf dist", - "build": "tsdown", + "build": "tsdown && pnpm api:check", "typecheck": "tsc --noEmit --incremental && tsc -p tsconfig.node.json --noEmit", "attw": "attw --pack .", "test": "vitest run", - "lint": "f=${OXLINT_FORMAT:-${AGENT:+agent}}; oxlint . --format=${f:-default}" + "lint": "f=${OXLINT_FORMAT:-${AGENT:+agent}}; oxlint . --format=${f:-default}", + "api:check": "tsdown && api-extractor run", + "api:update": "api-extractor run --local" }, "license": "Apache-2.0", "dependencies": { @@ -33,21 +35,22 @@ "@systemfsoftware/stryker-js-language": "workspace:^", "angular-html-parser": "~10.4.0", "effect": "catalog:", - "oxc-parser": "catalog:" + "oxc-parser": "catalog:", + "oxc-walker": "catalog:" }, "devDependencies": { + "@microsoft/api-extractor": "catalog:", "@systemfsoftware/all": "catalog:", "@systemfsoftware/arethetypeswrong-cli": "catalog:", "@systemfsoftware/effect-gherkin-spec": "catalog:", "@systemfsoftware/tsconfig": "catalog:", "@systemfsoftware/vitest-config": "workspace:^", - "@types/estree": "1.0.8", "@types/node": "catalog:", - "estree-walker": "3.0.3", "oxlint": "catalog:", "rimraf": "catalog:", "svelte": "5.55.1", "tsdown": "catalog:", + "type-fest": "catalog:", "typescript": "catalog:", "vitest": "catalog:" }, @@ -68,5 +71,8 @@ "svelte": { "optional": true } + }, + "inlinedDependencies": { + "type-fest": "4.41.0" } } diff --git a/packages/stryker-js-instrumenter/src/Ast.ts b/packages/stryker-js-instrumenter/src/Ast.ts new file mode 100644 index 000000000..2a681eba5 --- /dev/null +++ b/packages/stryker-js-instrumenter/src/Ast.ts @@ -0,0 +1,708 @@ +// oxlint-disable typescript/no-unsafe-type-assertion typescript/no-unnecessary-type-assertion +import type * as Oxc from '@oxc-project/types' +import * as Arr from 'effect/Array' +import * as Match from 'effect/Match' +import * as Option from 'effect/Option' +import * as Predicate from 'effect/Predicate' +import { walk, type WalkerCallbackContext, type WalkerThisContextEnter } from 'oxc-walker' +import type { Simplify } from 'type-fest' + +import { computeLineStarts, positionFromOffset } from './Syntax.js' +import { TraversalStopped } from './Traversal.schema.js' + +type Built = T extends null | undefined ? T + : T extends readonly unknown[] ? (number extends T['length'] ? Array> : T) + : T extends Oxc.Span ? + & { + [K in keyof T as K extends keyof Oxc.Span ? never : K]: Child + } + & Partial> + : T + +type Child = T extends null | undefined ? T + : T extends readonly unknown[] ? (number extends T['length'] ? Array> : T) + : T extends Oxc.Span ? Built | T + : T + +export type Program = Simplify> +export type Expression = Simplify> | Oxc.Expression +export type Statement = Simplify> | Oxc.Statement +export type Node = Simplify> | Oxc.Node + +export type AccessorProperty = Simplify> +export type Argument = Simplify> +export type ArrayExpression = Simplify> +export type ArrayPattern = Simplify> +export type ArrowFunctionExpression = Simplify> +export type AssignmentExpression = Simplify> +export type AssignmentPattern = Simplify> +export type BigIntLiteral = Simplify> +export type BinaryExpression = Simplify> +export type BindingIdentifier = Simplify> +export type BindingPattern = Simplify> +export type BindingProperty = Simplify> +export type BindingRestElement = Simplify> +export type BlockStatement = Simplify> +export type BooleanLiteral = Simplify> +export type BreakStatement = Simplify> +export type CallExpression = Simplify> +export type CatchClause = Simplify> +export type Class = Simplify> +export type ClassBody = Simplify> +export type ConditionalExpression = Simplify> +export type ContinueStatement = Simplify> +export type Decorator = Simplify> +export type DoWhileStatement = Simplify> +export type ExportAllDeclaration = Simplify> +export type ExportDefaultDeclaration = Simplify> +export type ExportNamedDeclaration = Simplify> +export type ExpressionStatement = Simplify> +export type ForInStatement = Simplify> +export type ForOfStatement = Simplify> +export type ForStatement = Simplify> +export type Function = Simplify> +export type IdentifierName = Simplify> +export type IdentifierReference = Simplify> +export type IfStatement = Simplify> +export type ImportAttribute = Simplify> +export type ImportDeclaration = Simplify> +export type JSDocNonNullableType = Simplify> +export type JSDocNullableType = Simplify> +export type JSXAttribute = Simplify> +export type JSXElement = Simplify> +export type JSXExpressionContainer = Simplify> +export type JSXFragment = Simplify> +export type JSXIdentifier = Simplify> +export type JSXMemberExpression = Simplify> +export type JSXNamespacedName = Simplify> +export type JSXOpeningElement = Simplify> +export type JSXSpreadAttribute = Simplify> +export type JSXSpreadChild = Simplify> +export type JSXText = Simplify> +export type LabelIdentifier = Simplify> +export type LabeledStatement = Simplify> +export type LogicalExpression = Simplify> +export type MemberExpression = Simplify> +export type MetaProperty = Simplify> +export type MethodDefinition = Simplify> +export type NewExpression = Simplify> +export type NullLiteral = Simplify> +export type NumericLiteral = Simplify> +export type ObjectExpression = Simplify> +export type ObjectProperty = Simplify> +export type ParamPattern = Simplify> +export type PrivateIdentifier = Simplify> +export type PrivateInExpression = Simplify> +export type PropertyDefinition = Simplify> +export type RegExpLiteral = Simplify> +export type ReturnStatement = Simplify> +export type SequenceExpression = Simplify> +export type SimpleAssignmentTarget = Simplify> | Oxc.SimpleAssignmentTarget +export type SpreadElement = Simplify> +export type StaticBlock = Simplify> +export type StaticMemberExpression = Simplify> +export type StringLiteral = Simplify> +export type SwitchCase = Simplify> +export type SwitchStatement = Simplify> +export type TaggedTemplateExpression = Simplify> +export type TemplateElement = Simplify> +export type TemplateLiteral = Simplify> +export type ThrowStatement = Simplify> +export type TryStatement = Simplify> +export type TSArrayType = Simplify> +export type TSAsExpression = Simplify> +export type TSCallSignatureDeclaration = Simplify> +export type TSConditionalType = Simplify> +export type TSConstructorType = Simplify> +export type TSConstructSignatureDeclaration = Simplify> +export type TSEnumDeclaration = Simplify> +export type TSExportAssignment = Simplify> +export type TSFunctionType = Simplify> +export type TSImportEqualsDeclaration = Simplify> +export type TSImportType = Simplify> +export type TSIndexedAccessType = Simplify> +export type TSIndexSignature = Simplify> +export type TSInferType = Simplify> +export type TSInstantiationExpression = Simplify> +export type TSInterfaceBody = Simplify> +export type TSInterfaceDeclaration = Simplify> +export type TSIntersectionType = Simplify> +export type TSLiteralType = Simplify> +export type TSMappedType = Simplify> +export type TSMethodSignature = Simplify> +export type TSModuleBlock = Simplify> +export type TSModuleDeclaration = Simplify> +export type TSNamedTupleMember = Simplify> +export type TSNamespaceExportDeclaration = Simplify> +export type TSNonNullExpression = Simplify> +export type TSOptionalType = Simplify> +export type TSParenthesizedType = Simplify> +export type TSPropertySignature = Simplify> +export type TSQualifiedName = Simplify> +export type TSRestType = Simplify> +export type TSSatisfiesExpression = Simplify> +export type TSTemplateLiteralType = Simplify> +export type TSTupleType = Simplify> +export type TSType = Simplify> +export type TSTypeAliasDeclaration = Simplify> +export type TSTypeAnnotation = Simplify> +export type TSTypeAssertion = Simplify> +export type TSTypeOperator = Simplify> +export type TSTypeParameterDeclaration = Simplify> +export type TSTypeParameterInstantiation = Simplify> +export type TSTypePredicate = Simplify> +export type TSTypeQuery = Simplify> +export type TSTypeReference = Simplify> +export type TSUnionType = Simplify> +export type UnaryExpression = Simplify> +export type UpdateExpression = Simplify> +export type VariableDeclaration = Simplify> +export type VariableDeclarator = Simplify> +export type WhileStatement = Simplify> +export type WithStatement = Simplify> +export type YieldExpression = Simplify> + +export type ClassExpression = Omit>, 'type'> & { type: 'ClassExpression' } +export type FunctionExpression = Omit>, 'type'> & { type: 'FunctionExpression' } +export type Literal = StringLiteral | NumericLiteral | BooleanLiteral | BigIntLiteral | RegExpLiteral | NullLiteral + +const EXPRESSION_KINDS: ReadonlySet = new Set([ + 'ArrayExpression', + 'ArrowFunctionExpression', + 'AwaitExpression', + 'BinaryExpression', + 'CallExpression', + 'ChainExpression', + 'ClassExpression', + 'ConditionalExpression', + 'FunctionExpression', + 'Identifier', + 'Import', + 'ImportExpression', + 'JSXElement', + 'JSXFragment', + 'Literal', + 'LogicalExpression', + 'MetaProperty', + 'NewExpression', + 'ObjectExpression', + 'PrivateIdentifier', + 'SequenceExpression', + 'Super', + 'TaggedTemplateExpression', + 'TemplateLiteral', + 'ThisExpression', + 'TSAsExpression', + 'TSInstantiationExpression', + 'TSNonNullExpression', + 'TSSatisfiesExpression', + 'TSTypeAssertion', + 'UnaryExpression', + 'UpdateExpression', + 'YieldExpression', +]) + +const STATEMENT_KINDS: ReadonlySet = new Set([ + 'BlockStatement', + 'BreakStatement', + 'ClassDeclaration', + 'ContinueStatement', + 'DebuggerStatement', + 'DoWhileStatement', + 'ExportAllDeclaration', + 'ExportDefaultDeclaration', + 'ExportNamedDeclaration', + 'ExpressionStatement', + 'ForInStatement', + 'ForOfStatement', + 'ForStatement', + 'FunctionDeclaration', + 'IfStatement', + 'ImportDeclaration', + 'LabeledStatement', + 'ReturnStatement', + 'SwitchStatement', + 'ThrowStatement', + 'TSExportAssignment', + 'TSImportEqualsDeclaration', + 'TSInterfaceDeclaration', + 'TSModuleDeclaration', + 'TSTypeAliasDeclaration', + 'VariableDeclaration', + 'WhileStatement', + 'WithStatement', +]) + +export function spanOf(node: Node): { start: number; end: number } | undefined { + const range = node.range + if (range === undefined) return undefined + return { start: range[0], end: range[1] } +} + +export function nodeType(node: unknown): string | undefined { + if (!isAstNode(node)) return undefined + return node.type +} + +export function isExpressionKind(node: Node | undefined | null): boolean { + const type = nodeType(node) + return type !== undefined && EXPRESSION_KINDS.has(type) +} + +export function isStatementKind(node: Node | undefined | null): boolean { + const type = nodeType(node) + return type !== undefined && STATEMENT_KINDS.has(type) +} + +type Loc = { start: number; end: number } | undefined + +function mark(node: T, loc: Loc): T { + if (loc !== undefined) { + return Object.assign(node, { start: loc.start, end: loc.end }) + } + return node +} + +export function identifier(name: string, loc?: Loc): IdentifierReference { + return mark({ type: 'Identifier', name }, loc) +} + +export function stringLiteral(value: string, loc?: Loc): Expression { + return mark({ type: 'Literal', value, raw: null }, loc) +} + +export function booleanLiteral(value: boolean, loc?: Loc): Expression { + return mark({ type: 'Literal', value, raw: null }, loc) +} + +export function regExpLiteral(pattern: string, flags: string, loc?: Loc): Expression { + return mark({ type: 'Literal', value: null, raw: null, regex: { pattern, flags } }, loc) +} + +export function arrayExpression(elements: ReadonlyArray = [], loc?: Loc): Expression { + return mark({ type: 'ArrayExpression', elements: elements.filter(Predicate.isNotNullish) }, loc) +} + +export function callExpression( + callee: Expression, + args: ReadonlyArray = [], + optional?: boolean, + loc?: Loc, +): Expression { + return mark({ type: 'CallExpression', callee, arguments: [...args], optional: optional === true }, loc) +} + +export function newExpression(callee: Expression, args: ReadonlyArray = [], loc?: Loc): Expression { + return mark({ type: 'NewExpression', callee, arguments: [...args] }, loc) +} + +export function memberExpression( + object: Expression, + property: IdentifierName, + optional: boolean, + loc?: Loc, +): Expression { + return mark( + { type: 'MemberExpression', object, property, computed: false, optional }, + loc, + ) +} + +export function optionalCallExpression( + callee: Expression, + args: ReadonlyArray, + optional: boolean, + loc?: Loc, +): Expression { + return mark({ type: 'CallExpression', callee, arguments: [...args], optional }, loc) +} + +export function arrowFunctionExpression( + params: ReadonlyArray, + body: Expression | Statement, + loc?: Loc, +): Expression { + const fnBody = body as BlockStatement | Expression + return mark( + { + type: 'ArrowFunctionExpression', + id: null, + generator: false, + params: [...params], + body: fnBody, + async: false, + expression: fnBody.type !== 'BlockStatement', + }, + loc, + ) +} + +export function blockStatement(body: ReadonlyArray, loc?: Loc): Statement { + return mark({ type: 'BlockStatement', body: [...body] }, loc) +} + +export function expressionStatement(expression: Expression, loc?: Loc): Statement { + return mark({ type: 'ExpressionStatement', expression }, loc) +} + +export function ifStatement( + test: Expression, + consequent: Statement, + alternate?: Statement | null, + loc?: Loc, +): Statement { + return mark({ type: 'IfStatement', test, consequent, alternate: alternate ?? null }, loc) +} + +export function variableDeclarator( + id: BindingPattern | IdentifierReference, + init: Expression | null, + loc?: Loc, +): VariableDeclarator { + return mark({ type: 'VariableDeclarator', id, init }, loc) +} + +export function variableDeclaration( + kind: 'const' | 'let' | 'var', + declarations: ReadonlyArray, + loc?: Loc, +): Statement { + return mark({ type: 'VariableDeclaration', kind, declarations: [...declarations] }, loc) +} + +export function returnStatement(argument: Expression | null, loc?: Loc): Statement { + return mark({ type: 'ReturnStatement', argument }, loc) +} + +export function sequenceExpression(expressions: ReadonlyArray, loc?: Loc): Expression { + return mark({ type: 'SequenceExpression', expressions: [...expressions] }, loc) +} + +export function conditionalExpression( + test: Expression, + consequent: Expression, + alternate: Expression, + loc?: Loc, +): Expression { + return mark({ type: 'ConditionalExpression', test, consequent, alternate }, loc) +} + +export function unaryExpression( + operator: Extract, + argument: Expression, + loc?: Loc, +): Expression { + return mark({ type: 'UnaryExpression', operator, argument, prefix: true }, loc) +} + +export function updateExpression( + operator: '++' | '--', + argument: SimpleAssignmentTarget, + prefix: boolean, + loc?: Loc, +): Expression { + return mark({ type: 'UpdateExpression', operator, argument, prefix }, loc) +} + +export function templateElement(raw: string, loc?: Loc): TemplateElement { + return mark({ type: 'TemplateElement', value: { raw, cooked: raw }, tail: true }, loc) +} + +export function templateLiteral( + quasis: ReadonlyArray, + expressions: ReadonlyArray, + loc?: Loc, +): Expression { + return mark({ type: 'TemplateLiteral', quasis: [...quasis], expressions: [...expressions] }, loc) +} + +export function switchCase(test: Expression | null, consequent: ReadonlyArray, loc?: Loc): SwitchCase { + return mark({ type: 'SwitchCase', test, consequent: [...consequent] }, loc) +} + +export function cloneNode(node: T): T { + return structuredClone(node) +} + +export interface Comment { + readonly type: 'Line' | 'Block' + readonly value: string + readonly start: number + readonly end: number +} + +export interface AttachedComment extends Comment { + readonly loc?: { start: { line: number; column: number }; end: { line: number; column: number } } +} + +export function attachComments( + root: Node, + comments: ReadonlyArray, + lineTable: readonly number[], +): void { + if (comments.length === 0) return + const nodes = collectNodes(root).filter((entry) => entry.node !== root) + nodes.sort((a, b) => a.start - b.start) + const groups = groupComments(nodes, comments) + assignComments(groups.leading, lineTable, 'leadingComments') + assignComments(groups.trailing, lineTable, 'trailingComments') +} + +interface CommentGroups { + readonly leading: Map + readonly trailing: Map +} + +interface CommentHost { + readonly field: keyof CommentGroups + readonly node: Node +} + +function groupComments(nodes: ReadonlyArray, comments: ReadonlyArray): CommentGroups { + const groups: CommentGroups = { leading: new Map(), trailing: new Map() } + for (const comment of comments) hostComment(nodes, comment, groups) + return groups +} + +function hostComment(nodes: ReadonlyArray, comment: Comment, groups: CommentGroups): void { + const hosts: ReadonlyArray<{ readonly field: keyof CommentGroups; readonly node: Node | undefined }> = [ + { field: 'leading', node: followingNode(nodes, comment) }, + { field: 'trailing', node: precedingStatement(nodes, comment) }, + ] + const host = hosts.find((candidate): candidate is CommentHost => candidate.node !== undefined) + if (host !== undefined) pushComment(groups[host.field], host.node, comment) +} + +function followingNode(nodes: ReadonlyArray, comment: Comment): Node | undefined { + const entry = nodes.find((candidate) => candidate.start >= comment.end) + if (entry === undefined) return undefined + return entry.node +} + +function precedingStatement(nodes: ReadonlyArray, comment: Comment): Node | undefined { + const entry = nodes.findLast((candidate) => candidate.end <= comment.start && isStatementKind(candidate.node)) + if (entry === undefined) return undefined + return entry.node +} + +function assignComments( + map: Map, + lineTable: readonly number[], + field: 'leadingComments' | 'trailingComments', +): void { + for (const [node, list] of map) { + const located = list.map((comment) => ({ + ...comment, + loc: { + start: positionFromLineTable(comment.start, lineTable), + end: positionFromLineTable(comment.end, lineTable), + }, + })) + Object.assign(node, { [field]: located }) + } +} + +function pushComment(map: Map, node: Node, comment: Comment): void { + const list = map.get(node) + if (list === undefined) map.set(node, [comment]) + else list.push(comment) +} + +interface NodeEntry { + readonly node: Node + readonly start: number + readonly end: number +} + +const isNodeList = (value: unknown): value is Array => Array.isArray(value) + +function collectNodes(root: Node): NodeEntry[] { + const out: NodeEntry[] = [] + walk(root as Oxc.Node, { + enter(node) { + appendEntry(node, out) + }, + }) + return out +} + +function appendEntry(node: Oxc.Node, out: NodeEntry[]): void { + const span = spanOf(node) + if (span === undefined) return + out.push({ node, start: span.start, end: span.end }) +} + +export function isAstNode(value: unknown): value is Node & Record { + return Predicate.isObject(value) && typeof value['type'] === 'string' +} + +export function buildLineTable(content: string): readonly number[] { + return computeLineStarts(content) +} + +export function positionFromLineTable(offset: number, lineTable: readonly number[]): { line: number; column: number } { + const zeroBased = positionFromOffset(lineTable, offset) + return { line: zeroBased.line + 1, column: zeroBased.column + 1 } +} + +export interface TraversePath { + readonly node: Node + readonly parentPath: TraversePath | null + skip(): void + stop(): void + find(predicate: (path: TraversePath) => boolean): TraversePath | undefined + getStatementParent(): TraversePath | undefined + replaceWith(node: Node): void + isExpression(): boolean + isStatement(): boolean +} + +export interface TraverseVisitors { + enter?: TraverseVisitor + exit?: TraverseVisitor +} + +type TraverseVisitor = (path: TraversePath) => void + +const COMMENT_KEYS: ReadonlySet = new Set(['leadingComments', 'trailingComments']) + +function isCommentKey(key: unknown): boolean { + return typeof key === 'string' && COMMENT_KEYS.has(key) +} + +export function traverse(root: Program | Node, visitors: TraverseVisitors): void { + const stack: TraversePath[] = [] + try { + walk(root as Oxc.Node, { + enter(node, _parent, context) { + readPath(stack, node, this, context, visitors) + }, + leave(_node, _parent, context) { + closePath(stack, context, visitors) + }, + }) + } catch (error) { + rethrowUnlessStopped(error) + } +} + +const rethrowUnlessStopped = (error: unknown): void => { + if (!(error instanceof TraversalStopped)) throw error +} + +const readPath = ( + stack: TraversePath[], + node: Oxc.Node, + controls: WalkerThisContextEnter, + context: WalkerCallbackContext, + visitors: TraverseVisitors, +): void => { + if (isCommentKey(context.key)) return + const path = createPath(node, parentOf(stack), controls, context) + stack.push(path) + notify(visitors.enter, path) +} + +const closePath = (stack: TraversePath[], context: WalkerCallbackContext, visitors: TraverseVisitors): void => { + if (isCommentKey(context.key)) return + notify(visitors.exit, stack.pop()) +} + +const parentOf = (stack: ReadonlyArray): TraversePath | null => Option.getOrNull(Arr.last(stack)) + +function notify(visitor: TraverseVisitor | undefined, path: TraversePath | undefined): void { + if (visitor === undefined) return + relay(visitor, path) +} + +const relay = (visitor: TraverseVisitor, path: TraversePath | undefined): void => { + if (path !== undefined) visitor(path) +} + +function createPath( + node: Oxc.Node, + parentPath: TraversePath | null, + controls: WalkerThisContextEnter, + context: WalkerCallbackContext, +): TraversePath { + const key = typeof context.key === 'string' ? context.key : undefined + const path: TraversePath = { + node, + parentPath, + skip() { + controls.skip() + }, + stop() { + throw new TraversalStopped() + }, + find(predicate) { + return nearest(path, predicate) + }, + getStatementParent() { + return nearest(parentPath, (ancestor) => isStatementKind(ancestor.node)) + }, + replaceWith(replacement) { + replaceInSlot(parentPath, key, context.index, replacement) + }, + isExpression() { + return isExpressionKind(node) + }, + isStatement() { + return isStatementKind(node) + }, + } + return path +} + +function nearest( + from: TraversePath | null, + predicate: (path: TraversePath) => boolean, +): TraversePath | undefined { + if (from === null) return undefined + return takeOrAscend(from, predicate) +} + +function takeOrAscend( + path: TraversePath, + predicate: (path: TraversePath) => boolean, +): TraversePath | undefined { + if (predicate(path)) return path + return nearest(path.parentPath, predicate) +} + +function replaceInSlot( + parentPath: TraversePath | null, + key: string | undefined, + index: number | null, + replacement: Node, +): void { + if (parentPath === null) return + writeInto(parentPath.node, key, index, replacement) +} + +function writeInto(parent: unknown, key: unknown, index: number | null, replacement: Node): void { + if (!isAstNode(parent)) return + writeAtKey(parent, key, index, replacement) +} + +function writeAtKey(parent: Record, key: unknown, index: number | null, replacement: Node): void { + Match.value(key).pipe( + Match.when(Predicate.isString, (slot) => writeAtSlot(parent, slot, index, replacement)), + Match.orElse(() => undefined), + ) +} + +function writeAtSlot(parent: Record, key: string, index: number | null, replacement: Node): void { + Match.value(index).pipe( + Match.when(Match.null, () => overwrite(parent, key, replacement)), + Match.orElse((position) => writeElement(parent[key], position, replacement)), + ) +} + +const overwrite = (parent: Record, key: string, replacement: Node): void => { + parent[key] = replacement +} + +const writeElement = (container: unknown, index: number, replacement: Node): void => { + if (isNodeList(container)) container[index] = replacement +} diff --git a/packages/stryker-js-instrumenter/src/Instrument.ts b/packages/stryker-js-instrumenter/src/Instrument.ts index ff6512871..242511158 100644 --- a/packages/stryker-js-instrumenter/src/Instrument.ts +++ b/packages/stryker-js-instrumenter/src/Instrument.ts @@ -30,7 +30,7 @@ export interface InstrumentResult { export type { InstrumenterOptions } -import { spanOf } from './estree.js' +import { spanOf } from './Ast.js' import { toApiMutant } from './Mutator.js' import { type SpannedComment } from './Syntax.js' diff --git a/packages/stryker-js-instrumenter/src/Mutator.ts b/packages/stryker-js-instrumenter/src/Mutator.ts index 60bfc9dd9..605032419 100644 --- a/packages/stryker-js-instrumenter/src/Mutator.ts +++ b/packages/stryker-js-instrumenter/src/Mutator.ts @@ -6,35 +6,39 @@ import { type Location, Mutant as ApiMutant, type Position } from '@systemfsoftw import * as Match from 'effect/Match' import * as Predicate from 'effect/Predicate' import type { - ArrayExpression as EstreeArrayExpression, - ArrowFunctionExpression as EstreeArrowFunctionExpression, - AssignmentExpression as EstreeAssignmentExpression, - BinaryExpression as EstreeBinaryExpression, - BlockStatement as EstreeBlockStatement, - ClassBody as EstreeClassBody, - DoWhileStatement as EstreeDoWhileStatement, - Expression as EstreeExpression, - ForStatement as EstreeForStatement, - Identifier as EstreeIdentifier, - IfStatement as EstreeIfStatement, - Literal as EstreeLiteral, - LogicalExpression as EstreeLogicalExpression, - MemberExpression as EstreeMemberExpression, - MethodDefinition as EstreeMethodDefinition, - NewExpression as EstreeNewExpression, - Node as EstreeNode, - ObjectExpression as EstreeObjectExpression, - Property as EstreeProperty, - PropertyDefinition as EstreePropertyDefinition, - SimpleCallExpression as EstreeCallExpression, - SpreadElement as EstreeSpreadElement, - SwitchCase as EstreeSwitchCase, - TemplateElement as EstreeTemplateElement, - TemplateLiteral as EstreeTemplateLiteral, - UnaryExpression as EstreeUnaryExpression, - UpdateExpression as EstreeUpdateExpression, - WhileStatement as EstreeWhileStatement, -} from 'estree' + ArrayExpression, + ArrowFunctionExpression, + AssignmentExpression, + BinaryExpression, + BlockStatement, + BooleanLiteral, + CallExpression, + ClassBody, + DoWhileStatement, + Expression, + ForStatement, + IdentifierReference, + IfStatement, + Literal, + LogicalExpression, + MemberExpression, + MethodDefinition, + NewExpression, + Node, + ObjectExpression, + ObjectProperty, + PrivateInExpression, + PropertyDefinition, + SpreadElement, + StaticMemberExpression, + StringLiteral, + SwitchCase, + TemplateElement, + TemplateLiteral, + UnaryExpression, + UpdateExpression, + WhileStatement, +} from './Ast.js' import { arrayExpression, @@ -58,14 +62,14 @@ import { type TraversePath, unaryExpression, updateExpression, -} from './estree.js' +} from './Ast.js' import { printNode } from './print/index.js' -export type Node = EstreeNode +export type { Node } /** * Node identity: same kind, same span. oxc nodes always carry a range * (parsed with `range: true`), which is a stronger identity than the old - * estree line/column loc. + * line/column loc. */ export function eqNode(a: Node, b: Node): boolean { const identity = nodeIdentity(a) @@ -464,11 +468,11 @@ function propertyOf(node: unknown, key: string): unknown { ) } -function isIdentifier(node: unknown): node is EstreeIdentifier { +function isIdentifier(node: unknown): node is IdentifierReference { return nodeType(node) === 'Identifier' } -function isCallExpression(node: Node): node is EstreeCallExpression { +function isCallExpression(node: Node): node is CallExpression { return node.type === 'CallExpression' } @@ -484,7 +488,7 @@ const arithmeticOperatorReplacements = Object.freeze( const ARITHMETIC_OPERATOR_KEYS: readonly string[] = Object.keys(arithmeticOperatorReplacements) -type ArithmeticBinary = EstreeBinaryExpression & { operator: keyof typeof arithmeticOperatorReplacements } +type ArithmeticBinary = BinaryExpression & { operator: keyof typeof arithmeticOperatorReplacements } export const arithmeticOperatorMutator: Mutator = (node) => Match.value(node).pipe( @@ -493,28 +497,36 @@ export const arithmeticOperatorMutator: Mutator = (node) => ) function isArithmeticBinary(node: Node): node is ArithmeticBinary { - return node.type === 'BinaryExpression' && isSupportedArithmeticOperator(node.operator, node) + return isBinaryExpression(node) && isSupportedArithmeticOperator(node.operator, node) } -function isSupportedArithmeticOperator(operator: string, node: EstreeBinaryExpression): boolean { +function isBinaryExpression(node: Node): node is BinaryExpression { + return node.type === 'BinaryExpression' && !isPrivateInExpression(node) +} + +function isPrivateInExpression(node: BinaryExpression | PrivateInExpression): node is PrivateInExpression { + return node.left.type === 'PrivateIdentifier' +} + +function isSupportedArithmeticOperator(operator: string, node: BinaryExpression): boolean { return ARITHMETIC_OPERATOR_KEYS.includes(operator) && !isStringConcatenation(node) } /** `1 + x` is arithmetic; `"a" + x` concatenates, and there is nothing to mutate. */ -function isStringConcatenation(node: EstreeBinaryExpression): boolean { +function isStringConcatenation(node: BinaryExpression): boolean { return isStringLike(node.right) || isStringLike(outerLeftOperand(node)) } /** A chained `a + b + c` carries its value on the innermost left operand's right side. */ -function outerLeftOperand(node: EstreeBinaryExpression): unknown { +function outerLeftOperand(node: BinaryExpression): unknown { if (node.left.type === 'BinaryExpression') { return node.left.right } return node.left } -type ArrayConstructorCall = (EstreeCallExpression | EstreeNewExpression) & { - callee: EstreeIdentifier & { name: 'Array' } +type ArrayConstructorCall = (CallExpression | NewExpression) & { + callee: IdentifierReference & { name: 'Array' } } export const arrayDeclarationMutator: Mutator = (node) => @@ -524,11 +536,11 @@ export const arrayDeclarationMutator: Mutator = (node) => Match.orElse(() => NO_MUTANTS), ) -function isArrayExpression(node: Node): node is EstreeArrayExpression { +function isArrayExpression(node: Node): node is ArrayExpression { return node.type === 'ArrayExpression' } -function arrayDeclarationReplacement(array: EstreeArrayExpression): EstreeExpression { +function arrayDeclarationReplacement(array: ArrayExpression): Expression { if (array.elements.length > 0) { return arrayExpression() } @@ -539,15 +551,15 @@ function isArrayConstructorCall(node: Node): node is ArrayConstructorCall { return isCallOrNewExpression(node) && isArrayIdentifier(node.callee) } -function isCallOrNewExpression(node: Node): node is EstreeCallExpression | EstreeNewExpression { +function isCallOrNewExpression(node: Node): node is CallExpression | NewExpression { return node.type === 'CallExpression' || node.type === 'NewExpression' } -function isArrayIdentifier(node: Node): node is EstreeIdentifier & { name: 'Array' } { +function isArrayIdentifier(node: Node): node is IdentifierReference & { name: 'Array' } { return node.type === 'Identifier' && node.name === 'Array' } -function arrayConstructorReplacement(construct: ArrayConstructorCall): EstreeExpression { +function arrayConstructorReplacement(construct: ArrayConstructorCall): Expression { const mutatedCallArgs = constructorArguments(construct.arguments) if (construct.type === 'NewExpression') { return newExpression(cloneNode(construct.callee), mutatedCallArgs) @@ -555,7 +567,7 @@ function arrayConstructorReplacement(construct: ArrayConstructorCall): EstreeExp return callExpression(cloneNode(construct.callee), mutatedCallArgs) } -function constructorArguments(args: ReadonlyArray): EstreeExpression[] { +function constructorArguments(args: ReadonlyArray): Expression[] { if (args.length > 0) { return [] } @@ -568,15 +580,15 @@ export const arrowFunctionMutator: Mutator = (node) => Match.orElse(() => NO_MUTANTS), ) -function isExpressionBodiedArrow(node: Node): node is EstreeArrowFunctionExpression { +function isExpressionBodiedArrow(node: Node): node is ArrowFunctionExpression { return node.type === 'ArrowFunctionExpression' && hasMutableArrowBody(node.body) } -function hasMutableArrowBody(body: EstreeBlockStatement | EstreeExpression): boolean { +function hasMutableArrowBody(body: BlockStatement | Expression): boolean { return body.type !== 'BlockStatement' && !isUndefinedExpression(body) } -function isUndefinedExpression(node: EstreeBlockStatement | EstreeExpression): node is EstreeIdentifier { +function isUndefinedExpression(node: BlockStatement | Expression): node is IdentifierReference { return node.type === 'Identifier' && node.name === 'undefined' } @@ -597,18 +609,14 @@ const assignmentOperatorReplacements = Object.freeze( } as const, ) -// estree merges string literals into `Literal` (numbers, booleans and regex -// share the tag), so the string check inspects the value, not the tag. -function isStringLike(value: unknown): value is EstreeTemplateLiteral | StringLiteral { +function isStringLike(value: unknown): value is TemplateLiteral | StringLiteral { return isTemplateLiteral(value) || isStringLiteral(value) } -function isTemplateLiteral(value: unknown): value is EstreeTemplateLiteral { +function isTemplateLiteral(value: unknown): value is TemplateLiteral { return nodeType(value) === 'TemplateLiteral' } -type StringLiteral = EstreeLiteral & { value: string } - function isStringLiteral(value: unknown): value is StringLiteral { return nodeType(value) === 'Literal' && hasStringValue(value) } @@ -624,7 +632,7 @@ const stringAssignmentTypes = Object.freeze(['&&=', '||=', '??=']) const ASSIGNMENT_OPERATOR_KEYS: readonly string[] = Object.keys(assignmentOperatorReplacements) -type AssignmentBinary = EstreeAssignmentExpression & { operator: keyof typeof assignmentOperatorReplacements } +type AssignmentBinary = AssignmentExpression & { operator: keyof typeof assignmentOperatorReplacements } export const assignmentOperatorMutator: Mutator = (node) => Match.value(node).pipe( @@ -638,11 +646,11 @@ function isMutatableAssignment(node: Node): node is AssignmentBinary { return node.type === 'AssignmentExpression' && isSupportedAssignment(node) } -function isSupportedAssignment(node: EstreeAssignmentExpression): boolean { +function isSupportedAssignment(node: AssignmentExpression): boolean { return ASSIGNMENT_OPERATOR_KEYS.includes(node.operator) && isSupportedAssignmentExpression(node) } -function isSupportedAssignmentExpression(node: EstreeAssignmentExpression): boolean { +function isSupportedAssignmentExpression(node: AssignmentExpression): boolean { return !isStringLike(node.right) || stringAssignmentTypes.includes(node.operator) } @@ -653,38 +661,38 @@ function isMutableBlock(node: Node, context: MutatorContext): boolean { return node.type === 'BlockStatement' && isValid(node, context) } -function isValid(node: EstreeBlockStatement, context: MutatorContext): boolean { +function isValid(node: BlockStatement, context: MutatorContext): boolean { return !isEmpty(node) && !isInvalidConstructorBody(node, context) } -function isEmpty(node: EstreeBlockStatement): boolean { +function isEmpty(node: BlockStatement): boolean { return node.body.length === 0 } -function isInvalidConstructorBody(block: EstreeBlockStatement, context: MutatorContext): boolean { +function isInvalidConstructorBody(block: BlockStatement, context: MutatorContext): boolean { const parent = context.parent - // estree: the constructor is a MethodDefinition whose `value` is the function + // oxc: the constructor is a MethodDefinition whose `value` is the function return isConstructorMethod(parent) && constructorBodyMatters(block, parent, context) } -function isConstructorMethod(node: Node | undefined): node is EstreeMethodDefinition { +function isConstructorMethod(node: Node | undefined): node is MethodDefinition { return isMethodDefinition(node) && node.kind === 'constructor' } -function isMethodDefinition(node: Node | undefined): node is EstreeMethodDefinition { +function isMethodDefinition(node: Node | undefined): node is MethodDefinition { return node?.type === 'MethodDefinition' } function constructorBodyMatters( - block: EstreeBlockStatement, - constructor: EstreeMethodDefinition, + block: BlockStatement, + constructor: MethodDefinition, context: MutatorContext, ): boolean { return containsSuperCall(block) && hasConstructorInitialization(constructor, context) } /** A derived constructor's body is load-bearing: it runs `super()` and seeds parameter properties. */ -function hasConstructorInitialization(constructor: EstreeMethodDefinition, context: MutatorContext): boolean { +function hasConstructorInitialization(constructor: MethodDefinition, context: MutatorContext): boolean { return [constructor.value.params.some(isParameterProperty), hasInitializedProperties(context)].some(Boolean) } @@ -699,7 +707,7 @@ function hasInitializedProperties(context: MutatorContext): boolean { return isClassBody(classBody) && classBody.body.some(isInitializedField) } -function isClassBody(node: Node | undefined): node is EstreeClassBody { +function isClassBody(node: Node | undefined): node is ClassBody { return node?.type === 'ClassBody' } @@ -707,7 +715,7 @@ function isInitializedField(member: Node): boolean { return isPropertyDefinition(member) && isPresent(member.value) } -function isPropertyDefinition(node: Node): node is EstreePropertyDefinition { +function isPropertyDefinition(node: Node): node is PropertyDefinition { return node.type === 'PropertyDefinition' } @@ -746,8 +754,6 @@ function containsSuperInValue(value: unknown): boolean { return containsSuperCall(value) } -type BooleanLiteral = EstreeLiteral & { value: boolean } - export const booleanLiteralMutator: Mutator = (node) => Match.value(node).pipe( Match.when(isBooleanLiteral, (literal) => [booleanLiteral(!literal.value)]), @@ -759,17 +765,17 @@ function isBooleanLiteral(node: Node): node is BooleanLiteral { return node.type === 'Literal' && typeof node.value === 'boolean' } -function isNegatedPrefix(node: Node): node is EstreeUnaryExpression { +function isNegatedPrefix(node: Node): node is UnaryExpression { return isUnaryExpression(node) && isNegation(node) } -function isUnaryExpression(node: Node): node is EstreeUnaryExpression { +function isUnaryExpression(node: Node): node is UnaryExpression { return node.type === 'UnaryExpression' } -type NegatedPrefix = EstreeUnaryExpression & { operator: '!' } +type NegatedPrefix = UnaryExpression & { operator: '!' } -function isNegation(unary: EstreeUnaryExpression): unary is NegatedPrefix { +function isNegation(unary: UnaryExpression): unary is NegatedPrefix { return unary.operator === '!' && unary.prefix } @@ -803,23 +809,23 @@ function statementMutants(node: Node): readonly Node[] { ) } -function withEmptyTest(loop: EstreeForStatement): EstreeForStatement { +function withEmptyTest(loop: ForStatement): ForStatement { const replacement = cloneNode(loop) replacement.test = booleanLiteral(false) return replacement } -function withEmptyConsequent(switchCase: EstreeSwitchCase): EstreeSwitchCase { +function withEmptyConsequent(switchCase: SwitchCase): SwitchCase { const replacement = cloneNode(switchCase) replacement.consequent = [] return replacement } -function isEmptyTestForStatement(node: Node): node is EstreeForStatement { +function isEmptyTestForStatement(node: Node): node is ForStatement { return node.type === 'ForStatement' && node.test === null } -function isNonEmptySwitchCase(node: Node): node is EstreeSwitchCase { +function isNonEmptySwitchCase(node: Node): node is SwitchCase { return node.type === 'SwitchCase' && node.consequent.length > 0 } @@ -839,7 +845,7 @@ function logicalParentOperator(parent: Node | undefined): string | undefined { ) } -function isLogicalExpression(node: Node | undefined): node is EstreeLogicalExpression { +function isLogicalExpression(node: Node | undefined): node is LogicalExpression { return node?.type === 'LogicalExpression' } @@ -855,7 +861,7 @@ function isLoopStatement(node: Node | undefined): boolean { return isTestBearingStatement(node) && LOOP_STATEMENT_KINDS[node.type] === true } -function isIfStatement(node: Node | undefined): node is EstreeIfStatement { +function isIfStatement(node: Node | undefined): node is IfStatement { return node?.type === 'IfStatement' } @@ -872,7 +878,7 @@ const TEST_BEARING_KINDS: Readonly> = { ForStatement: true, } -type TestBearingStatement = EstreeIfStatement | EstreeWhileStatement | EstreeDoWhileStatement | EstreeForStatement +type TestBearingStatement = IfStatement | WhileStatement | DoWhileStatement | ForStatement function isTestBearingStatement(node: Node | undefined): node is TestBearingStatement { return node !== undefined && TEST_BEARING_KINDS[node.type] === true @@ -885,11 +891,11 @@ function testOfStatement(node: Node | undefined): Node | undefined { ) } -function isBooleanExpression(node: Node): node is EstreeBinaryExpression | EstreeLogicalExpression { +function isBooleanExpression(node: Node): node is BinaryExpression | LogicalExpression { return isOperatorExpression(node) && booleanOperators.includes(node.operator) } -function isOperatorExpression(node: Node): node is EstreeBinaryExpression | EstreeLogicalExpression { +function isOperatorExpression(node: Node): node is BinaryExpression | LogicalExpression { return node.type === 'BinaryExpression' || node.type === 'LogicalExpression' } @@ -906,7 +912,7 @@ const operators = { const EQUALITY_OPERATOR_KEYS: readonly string[] = Object.keys(operators) -type EqualityBinary = EstreeBinaryExpression & { operator: keyof typeof operators } +type EqualityBinary = BinaryExpression & { operator: keyof typeof operators } export const equalityOperatorMutator: Mutator = (node) => Match.value(node).pipe( @@ -932,7 +938,7 @@ const logicalOperatorReplacements = Object.freeze( const LOGICAL_OPERATOR_KEYS: readonly string[] = Object.keys(logicalOperatorReplacements) -type LogicalBinary = EstreeLogicalExpression & { operator: keyof typeof logicalOperatorReplacements } +type LogicalBinary = LogicalExpression & { operator: keyof typeof logicalOperatorReplacements } export const logicalOperatorMutator: Mutator = (node) => Match.value(node).pipe( @@ -980,13 +986,13 @@ for (const [key, value] of Object.entries(baseReplacements)) { } } -interface NamedMember extends EstreeMemberExpression { - readonly property: EstreeIdentifier - readonly object: EstreeExpression +interface NamedMember extends StaticMemberExpression { + readonly property: IdentifierReference + readonly object: Expression } interface MethodMutation { - readonly call: EstreeCallExpression + readonly call: CallExpression readonly callee: NamedMember readonly newName: string | null } @@ -997,7 +1003,7 @@ export const methodExpressionMutator: Mutator = (node) => Match.orElse(() => NO_MUTANTS), ) -function methodCallMutants(call: EstreeCallExpression): readonly Node[] { +function methodCallMutants(call: CallExpression): readonly Node[] { return Match.value(methodMutation(call)).pipe( Match.when(isMethodMutation, (mutation) => [methodExpressionReplacement(mutation)]), Match.orElse(() => NO_MUTANTS), @@ -1009,7 +1015,7 @@ function isMethodMutation(mutation: MethodMutation | undefined): mutation is Met } /** The method this call replaces, or `undefined` when the call is not one this operator knows. */ -function methodMutation(call: EstreeCallExpression): MethodMutation | undefined { +function methodMutation(call: CallExpression): MethodMutation | undefined { const callee = namedMethodCallee(call) return Match.value(callee).pipe( Match.when(undefined, () => undefined), @@ -1017,14 +1023,14 @@ function methodMutation(call: EstreeCallExpression): MethodMutation | undefined ) } -function mutationFor(call: EstreeCallExpression, callee: NamedMember): MethodMutation | undefined { +function mutationFor(call: CallExpression, callee: NamedMember): MethodMutation | undefined { return Match.value(replacements.get(callee.property.name)).pipe( Match.when(undefined, () => undefined), Match.orElse((newName) => ({ call, callee, newName })), ) } -function namedMethodCallee(call: EstreeCallExpression): NamedMember | undefined { +function namedMethodCallee(call: CallExpression): NamedMember | undefined { return Match.value(call.callee).pipe( Match.when(isNamedMember, (member) => member), Match.orElse(() => undefined), @@ -1043,28 +1049,27 @@ function isNotSuperMember(member: NamedMember): boolean { return !isSuperType(member.object) } -function methodExpressionReplacement(mutation: MethodMutation): EstreeExpression { +function methodExpressionReplacement(mutation: MethodMutation): Expression { return Match.value(mutation.newName).pipe( Match.when(null, () => callExpression(cloneNode(mutation.callee.object), [], mutation.callee.optional === true)), Match.orElse((newName) => renamedMethodCall(mutation, newName)), ) } -function renamedMethodCall(mutation: MethodMutation, newName: string): EstreeExpression { +function renamedMethodCall(mutation: MethodMutation, newName: string): Expression { const mutatedCallee = memberExpression( cloneNode(mutation.callee.object), identifier(newName), - false, mutation.callee.optional === true, ) return callExpression(mutatedCallee, spreadFreeArguments(mutation.call.arguments), mutation.call.optional === true) } -function spreadFreeArguments(args: ReadonlyArray): EstreeExpression[] { +function spreadFreeArguments(args: ReadonlyArray): Expression[] { return args.filter(isNotSpreadElement).map((argument) => cloneNode(argument)) } -function isNotSpreadElement(node: EstreeExpression | EstreeSpreadElement): node is EstreeExpression { +function isNotSpreadElement(node: Expression | SpreadElement): node is Expression { return node.type !== 'SpreadElement' } @@ -1074,7 +1079,7 @@ export const objectLiteralMutator: Mutator = (node) => Match.orElse(() => NO_MUTANTS), ) -function isNonEmptyObjectLiteral(node: Node): node is EstreeObjectExpression { +function isNonEmptyObjectLiteral(node: Node): node is ObjectExpression { return node.type === 'ObjectExpression' && node.properties.length > 0 } @@ -1085,11 +1090,11 @@ export const optionalChainingMutator: Mutator = (node) => Match.orElse(() => NO_MUTANTS), ) -function isOptionalMember(node: Node): node is EstreeMemberExpression { +function isOptionalMember(node: Node): node is MemberExpression { return node.type === 'MemberExpression' && node.optional === true } -function isOptionalCall(node: Node): node is EstreeCallExpression { +function isOptionalCall(node: Node): node is CallExpression { return node.type === 'CallExpression' && node.optional === true } @@ -1099,7 +1104,7 @@ function withoutOptional(node: T): T { return replacement } -type RegexLiteral = EstreeLiteral & { regex: { pattern: string; flags: string } } +type RegexLiteral = Literal & { regex: { pattern: string; flags: string } } export const regexMutator: Mutator = (node, context) => Match.value(node).pipe( @@ -1134,7 +1139,7 @@ function isRegExpConstructor(parent: Node | undefined): boolean { return isNewExpression(parent) && isRegExpIdentifier(parent.callee) } -function isNewExpression(node: Node | undefined): node is EstreeNewExpression { +function isNewExpression(node: Node | undefined): node is NewExpression { return node?.type === 'NewExpression' } @@ -1168,14 +1173,14 @@ export const stringLiteralMutator: Mutator = (node, context) => Match.orElse(() => NO_MUTANTS), ) -function templateMutants(template: EstreeTemplateLiteral): readonly Node[] { +function templateMutants(template: TemplateLiteral): readonly Node[] { return Match.value(template.quasis[0]).pipe( Match.when(undefined, () => NO_MUTANTS), Match.orElse((first) => [emptyOrPlaceholderTemplate(template, first)]), ) } -function emptyOrPlaceholderTemplate(template: EstreeTemplateLiteral, first: EstreeTemplateElement): Node { +function emptyOrPlaceholderTemplate(template: TemplateLiteral, first: TemplateElement): Node { const isEmptyTemplate = [template.quasis.length === 1, first.value.raw.length === 0].every(Boolean) return templateLiteral([templateElement(replacementText(isEmptyTemplate))], []) } @@ -1223,7 +1228,7 @@ function isJsxOrExpressionRelated(parent: Node): boolean { return JSX_KINDS[parent.type] === true || isObjectMethod(parent) } -function isObjectMethod(node: Node): node is EstreeProperty { +function isObjectMethod(node: Node): node is ObjectProperty { return node.type === 'Property' && node.method === true } @@ -1231,11 +1236,11 @@ function isObjectOrClassPropertyKey(parent: Node, child: Node): boolean { return isPropertyHost(parent) && isKeyOf(parent, child) } -function isPropertyHost(node: Node): node is EstreeProperty | EstreePropertyDefinition { +function isPropertyHost(node: Node): node is ObjectProperty | PropertyDefinition { return node.type === 'Property' || node.type === 'PropertyDefinition' } -function isKeyOf(host: EstreeProperty | EstreePropertyDefinition, child: Node): boolean { +function isKeyOf(host: ObjectProperty | PropertyDefinition, child: Node): boolean { return nodeType(host.key) !== undefined && host.key === child } @@ -1245,7 +1250,7 @@ function isDisallowedCallExpression(parent: Node): boolean { return isCallExpression(parent) && DISALLOWED_CALLEES[calleeName(parent)] === true } -function calleeName(parent: EstreeCallExpression): string { +function calleeName(parent: CallExpression): string { return Match.value(parent.callee).pipe( Match.when(isIdentifier, (identifier) => identifier.name), Match.when(isImportCallee, () => 'import'), @@ -1267,7 +1272,7 @@ const UnaryOperator = { const UNARY_OPERATOR_KEYS: readonly string[] = Object.keys(UnaryOperator) -type SupportedUnaryExpression = EstreeUnaryExpression & { operator: keyof typeof UnaryOperator } +type SupportedUnaryExpression = UnaryExpression & { operator: keyof typeof UnaryOperator } export const unaryOperatorMutator: Mutator = (node) => Match.value(node).pipe( @@ -1279,12 +1284,12 @@ function isSupportedUnaryExpression(node: Node): node is SupportedUnaryExpressio return isPrefixUnaryExpression(node) && isSupportedUnaryOperator(node.operator) } -function isPrefixUnaryExpression(node: Node): node is EstreeUnaryExpression { +function isPrefixUnaryExpression(node: Node): node is UnaryExpression { return node.type === 'UnaryExpression' && node.prefix } /** The sign-flipping unary becomes a flipped unary; `~x` loses its operator entirely. */ -function unaryOperatorReplacement(unary: SupportedUnaryExpression): EstreeExpression { +function unaryOperatorReplacement(unary: SupportedUnaryExpression): Expression { const mutatedOperator = UnaryOperator[unary.operator] return Match.value(mutatedOperator).pipe( Match.when(isPlusOrMinus, (operator) => unaryExpression(operator, cloneNode(unary.argument))), @@ -1313,7 +1318,7 @@ export const updateOperatorMutator: Mutator = (node) => Match.orElse(() => NO_MUTANTS), ) -function isUpdateExpression(node: Node): node is EstreeUpdateExpression { +function isUpdateExpression(node: Node): node is UpdateExpression { return node.type === 'UpdateExpression' } diff --git a/packages/stryker-js-instrumenter/src/Parser.ts b/packages/stryker-js-instrumenter/src/Parser.ts index 891664ec7..cd75bb3fd 100644 --- a/packages/stryker-js-instrumenter/src/Parser.ts +++ b/packages/stryker-js-instrumenter/src/Parser.ts @@ -6,10 +6,9 @@ import * as Arr from 'effect/Array' import * as Match from 'effect/Match' import * as Option from 'effect/Option' import * as Predicate from 'effect/Predicate' -import type { BaseNode, Program } from 'estree' import type { OxcError } from 'oxc-parser' import path from 'path' -import { buildLineTable, positionFromLineTable } from './estree.js' +import { buildLineTable, type Node, positionFromLineTable, type Program } from './Ast.js' import { loadOxc } from './Oxc.js' import { ParseFailed, @@ -78,7 +77,7 @@ function isTypedRecord(value: unknown): value is Record & { typ return isPlainRecord(value) && Predicate.isString(value['type']) } -function isRangedBaseNode(value: unknown): value is BaseNode & Range { +function isRangedBaseNode(value: unknown): value is Node & Range { return isTypedRecord(value) && hasNumericRange(value) } @@ -125,9 +124,6 @@ export async function parseWithOxc( if (failure !== undefined) { throw failure } - // oxc's Program and comments are structurally the estree Program / - // SpannedComment shapes the rest of this package works with; the boundary - // casts name that contract once, and the scoped disables stay on those lines. // oxlint-disable-next-line typescript/no-unsafe-type-assertion const program = result.program as unknown as Program // oxlint-disable-next-line typescript/no-unnecessary-type-assertion typescript/no-unsafe-type-assertion @@ -466,7 +462,7 @@ interface Version { const MINIMUM_SVELTE_VERSION: Version = { major: 3, minor: 30 } const SVELTE_5: Version = { major: 5, minor: 0 } -const ESTREE_WALKER_MISSING = 'estree-walker module without walk export' +const WALKER_MODULE_MISSING = 'walker module without walk export' const COMPILER_WALK_MISSING = 'svelte/compiler module without walk export' const INSTANCE_RANGE_MISSING = 'Svelte instance script without a source range' @@ -585,7 +581,7 @@ function rangedExpressionOf(payload: unknown): TemplateRange | undefined { */ function loadWalker(version: string, fileName: string): Promise { return Match.value(isAtLeast(version, SVELTE_5)).pipe( - Match.when(true, () => loadWalkerModule(import.meta.resolve('estree-walker'), fileName, ESTREE_WALKER_MISSING)), + Match.when(true, () => loadWalkerModule(import.meta.resolve('oxc-walker'), fileName, WALKER_MODULE_MISSING)), Match.orElse(() => loadWalkerModule('svelte/compiler', fileName, COMPILER_WALK_MISSING)), ) } diff --git a/packages/stryker-js-instrumenter/src/Printer.ts b/packages/stryker-js-instrumenter/src/Printer.ts index 86e62971b..31d3dc7b2 100644 --- a/packages/stryker-js-instrumenter/src/Printer.ts +++ b/packages/stryker-js-instrumenter/src/Printer.ts @@ -4,7 +4,7 @@ * html/svelte slicing needs come from the parsed `range`. */ import * as Predicate from 'effect/Predicate' -import { spanOf } from './estree.js' +import { spanOf } from './Ast.js' import { type Hashbang, printProgram } from './print/index.js' import { type Ast, @@ -36,7 +36,6 @@ export function print(file: Ast): string { } } -// oxc carries the hashbang on the Program; estree's type does not declare it. const HASHBANG_FIELDS: Readonly boolean>> = { type: (field) => field === 'Hashbang', value: (field) => typeof field === 'string', diff --git a/packages/stryker-js-instrumenter/src/Syntax.ts b/packages/stryker-js-instrumenter/src/Syntax.ts index 66aab7404..f4985cdb7 100644 --- a/packages/stryker-js-instrumenter/src/Syntax.ts +++ b/packages/stryker-js-instrumenter/src/Syntax.ts @@ -3,7 +3,7 @@ */ import type { Position } from '@systemfsoftware/stryker-js-language' import * as Match from 'effect/Match' -import type { Program } from 'estree' +import type { Program } from './Ast.js' import { AstFormat as SchemaAstFormat } from './Syntax.schema.js' export const AstFormat = SchemaAstFormat diff --git a/packages/stryker-js-instrumenter/src/Transformer.ts b/packages/stryker-js-instrumenter/src/Transformer.ts index 47667706f..213e204a0 100644 --- a/packages/stryker-js-instrumenter/src/Transformer.ts +++ b/packages/stryker-js-instrumenter/src/Transformer.ts @@ -7,46 +7,44 @@ import { propertyPath, type StrykerOptions, strykerReportBugUrl } from '@systemf import * as Match from 'effect/Match' import * as Option from 'effect/Option' import * as Predicate from 'effect/Predicate' -import type { - ArrowFunctionExpression, - ClassExpression, - Comment, - Expression, - FunctionExpression, - Identifier, - MemberExpression, - Node, - Program, - Statement, -} from 'estree' import path from 'node:path' import { + type ArrowFunctionExpression, arrowFunctionExpression, attachComments, - type AttachedComment, blockStatement, buildLineTable, callExpression, + type ClassExpression, cloneNode, + type Comment, conditionalExpression, + type Expression, expressionStatement, + type FunctionExpression, identifier, + type IdentifierReference, ifStatement, isExpressionKind, isStatementKind, + type MemberExpression, + type Node, nodeType, positionFromLineTable, + type Program, returnStatement, sequenceExpression, spanOf, + type Statement, stringLiteral, switchCase, traverse, type TraversePath, variableDeclaration, + type VariableDeclarator, variableDeclarator, -} from './estree.js' +} from './Ast.js' import { applyMutant, createMutant, type Mutable, type Mutant } from './Mutator.js' import { type MutatorContext, type MutatorOptions } from './Mutator.js' import { allMutators } from './Mutator.js' @@ -305,38 +303,11 @@ function mutatorWarning(directive: StrykerDirective, mutatorName: string, origin } function toIgnorerPath(path: TraversePath): IgnorerNodePath { - let parentResult: IgnorerNodePath | null = null - if (path.parentPath !== null) { - parentResult = toIgnorerPath(path.parentPath) - } - const node: unknown = path.node - const result: IgnorerNodePath = { - node: path.node, - parentPath: parentResult, - isObjectExpression(): boolean { - return nodeType(node) === 'ObjectExpression' - }, - isCallExpression(): boolean { - return nodeType(node) === 'CallExpression' - }, - isClassProperty(): boolean { - return nodeType(node) === 'PropertyDefinition' - }, - isClassPrivateProperty(): boolean { - return nodeType(node) === 'PropertyDefinition' && - nodeType(propertyKeyOf(node)) === 'PrivateIdentifier' - }, - isClassAccessorProperty(): boolean { - return nodeType(node) === 'AccessorProperty' - }, + const ancestors: unknown[] = [] + for (let current = path.parentPath; current !== null; current = current.parentPath) { + ancestors.push(current.node) } - return result -} - -function propertyKeyOf(node: unknown): unknown { - return Option.getOrUndefined( - Option.map(Option.filter(Option.some(node), Predicate.hasProperty('key')), (holder) => holder['key']), - ) + return { node: path.node, ancestors } } export function isTypeNode(path: TraversePath): boolean { @@ -476,10 +447,7 @@ function classOrFunctionExpressionNamedIfNeeded(path: TraversePath): Expression function nameFromParent(path: TraversePath, node: AnonymousFunctionOrClass): Expression | undefined { return Match.value(path.parentPath?.node).pipe( - Match.when( - { type: 'VariableDeclarator', id: { type: 'Identifier' } }, - (declarator) => adoptIdentifier(node, declarator.id), - ), + Match.when(isVariableDeclarator, (declarator) => adoptDeclaredName(node, declarator)), Match.when({ type: 'Property', key: { type: 'Identifier' } }, () => namedPropertyValue(path, node)), Match.orElse(() => undefined), ) @@ -493,7 +461,7 @@ function namedPropertyValue(path: TraversePath, node: AnonymousFunctionOrClass): ) } -function adoptIdentifier(node: AnonymousFunctionOrClass, identifier: Identifier): Expression { +function adoptIdentifier(node: AnonymousFunctionOrClass, identifier: IdentifierReference): Expression { node.id = identifier return node } @@ -524,7 +492,7 @@ function arrowNamedByDeclarator( }) } -function namedArrowExpression(node: ArrowFunctionExpression, identifier: Identifier): Expression { +function namedArrowExpression(node: ArrowFunctionExpression, identifier: IdentifierReference): Expression { const declaration = variableDeclaration('const', [variableDeclarator(identifier, node)]) return callExpression( arrowFunctionExpression([], blockStatement([declaration, returnStatement(identifier)])), @@ -532,17 +500,36 @@ function namedArrowExpression(node: ArrowFunctionExpression, identifier: Identif ) } -function declaratorIdentifier(parentPath: TraversePath | null): Option.Option { +function declaratorIdentifier(parentPath: TraversePath | null): Option.Option { return Option.flatMap(Option.fromNullishOr(parentPath), (parent) => Match.value(parent.node).pipe( - Match.when( - { type: 'VariableDeclarator', id: { type: 'Identifier' } }, - (declarator) => Option.some(declarator.id), - ), - Match.orElse(() => Option.none()), + Match.when(isVariableDeclarator, (declarator) => declaredName(declarator)), + Match.orElse(() => Option.none()), )) } +function isVariableDeclarator(node: unknown): node is VariableDeclarator { + return nodeType(node) === 'VariableDeclarator' +} + +function adoptDeclaredName(node: AnonymousFunctionOrClass, declarator: VariableDeclarator): Expression | undefined { + return Option.match(declaredName(declarator), { + onNone: () => undefined, + onSome: (identifier) => adoptIdentifier(node, identifier), + }) +} + +function declaredName(declarator: VariableDeclarator): Option.Option { + return Match.value(declarator.id).pipe( + Match.when(isIdentifierReference, (identifier) => Option.some(identifier)), + Match.orElse(() => Option.none()), + ) +} + +function isIdentifierReference(node: unknown): node is IdentifierReference { + return nodeType(node) === 'Identifier' +} + function nameIfAnonymous(path: TraversePath): Expression { return classOrFunctionExpressionNamedIfNeeded(path) ?? arrowNameOrNode(path) } @@ -772,15 +759,21 @@ function reasonAt(path: IgnorerNodePath): Option.Option { } function ancestorReason(path: IgnorerNodePath): Option.Option { - return Option.flatMap(Option.fromNullishOr(path.parentPath), shouldIgnore) + return path.ancestors + .map((_ancestor, index) => reasonAt(ancestorPath(path.ancestors, index))) + .find(Option.isSome) ?? Option.none() +} + +function ancestorPath(ancestors: readonly unknown[], index: number): IgnorerNodePath { + return { node: ancestors[index], ancestors: ancestors.slice(index + 1) } } export const angularIgnorer: IgnorerService = { shouldIgnore, } -function isClassFieldLike(path: IgnorerNodePath): boolean { - return CLASS_FIELD_KINDS.includes(nodeType(path.node) ?? '') +function isClassFieldLike(node: unknown): boolean { + return CLASS_FIELD_KINDS.includes(nodeType(node) ?? '') } const CLASS_FIELD_KINDS: readonly string[] = Object.freeze(['PropertyDefinition', 'AccessorProperty']) @@ -788,9 +781,11 @@ const CLASS_FIELD_KINDS: readonly string[] = Object.freeze(['PropertyDefinition' interface SignalCallSite { readonly callee: unknown readonly args: readonly unknown[] - readonly objectExpression: IgnorerNodePath + readonly objectExpression: unknown } +type OwnsCallSite = (owner: unknown) => boolean + function isInputModelOrOutputConfigurationObject(path: IgnorerNodePath): boolean { return Option.match(signalCallSiteOf(path, isPropertyDefinitionField), { onNone: () => false, @@ -805,30 +800,27 @@ function isSignalQueryOptionsObject(path: IgnorerNodePath): boolean { }) } -function isPropertyDefinitionField(path: IgnorerNodePath): boolean { - return nodeType(path.node) === 'PropertyDefinition' +function isPropertyDefinitionField(node: unknown): boolean { + return nodeType(node) === 'PropertyDefinition' } /** The call that takes the object expression as an argument, when a field-like member owns it. */ function signalCallSiteOf( path: IgnorerNodePath, - ownsCallSite: (owner: IgnorerNodePath) => boolean, + ownsCallSite: OwnsCallSite, ): Option.Option { return Option.flatMap( ownedCallPath(path, ownsCallSite), - (callPath) => Option.map(callArgumentsOf(callPath), (call) => ({ ...call, objectExpression: path })), + (callNode) => Option.map(callArgumentsOf(callNode), (call) => ({ ...call, objectExpression: path.node })), ) } -function ownedCallPath( - path: IgnorerNodePath, - ownsCallSite: (owner: IgnorerNodePath) => boolean, -): Option.Option { +function ownedCallPath(path: IgnorerNodePath, ownsCallSite: OwnsCallSite): Option.Option { const argument = Option.filter(Option.some(path), (candidate) => isOwnedCallArgument(candidate, ownsCallSite)) - return Option.flatMap(argument, (candidate) => Option.fromNullishOr(candidate.parentPath)) + return Option.flatMap(argument, () => Option.fromUndefinedOr(path.ancestors[0])) } -function isOwnedCallArgument(path: IgnorerNodePath, ownsCallSite: (owner: IgnorerNodePath) => boolean): boolean { +function isOwnedCallArgument(path: IgnorerNodePath, ownsCallSite: OwnsCallSite): boolean { return isObjectArgumentOfCall(path) && ownsCallSiteOf(path, ownsCallSite) } @@ -837,11 +829,11 @@ function isObjectArgumentOfCall(path: IgnorerNodePath): boolean { } function parentIsCallExpression(path: IgnorerNodePath): boolean { - return nodeType(path.parentPath?.node) === 'CallExpression' + return nodeType(path.ancestors[0]) === 'CallExpression' } -function ownsCallSiteOf(path: IgnorerNodePath, ownsCallSite: (owner: IgnorerNodePath) => boolean): boolean { - return Option.exists(Option.fromNullishOr(path.parentPath?.parentPath), ownsCallSite) +function ownsCallSiteOf(path: IgnorerNodePath, ownsCallSite: OwnsCallSite): boolean { + return Option.exists(Option.fromUndefinedOr(path.ancestors[1]), ownsCallSite) } interface CallArguments { @@ -849,9 +841,9 @@ interface CallArguments { readonly args: readonly unknown[] } -function callArgumentsOf(callPath: IgnorerNodePath): Option.Option { +function callArgumentsOf(callNode: unknown): Option.Option { return Option.flatMap( - Option.filter(Option.some(callPath.node), hasCallShape), + Option.filter(Option.some(callNode), hasCallShape), (call) => Option.map(argumentArrayOf(call['arguments']), (args) => ({ callee: call['callee'], args })), ) } @@ -897,7 +889,7 @@ function isSignalQueryCall(callee: unknown): boolean { function isArgumentAt(site: SignalCallSite, index: Option.Option): boolean { return Option.exists( index, - (position) => site.args.length > position && site.args[position] === site.objectExpression.node, + (position) => site.args.length > position && site.args[position] === site.objectExpression, ) } @@ -1219,7 +1211,7 @@ async function placeModuleHeader(svelte: AstByFormat['svelte']): Promise { } function emptyProgram(): Program { - return { type: 'Program', sourceType: 'module', body: [] } + return { type: 'Program', sourceType: 'module', body: [], hashbang: null } } interface MutantsPlacement { @@ -1242,7 +1234,7 @@ export const transformScript: AstTransformer = async ( ) => { const lineTable = buildLineTable(rawContent) - attachComments(root, comments as readonly AttachedComment[], lineTable) + attachComments(root, comments, lineTable) const placementMap: PlacementMap = new Map() diff --git a/packages/stryker-js-instrumenter/src/Traversal.schema.ts b/packages/stryker-js-instrumenter/src/Traversal.schema.ts new file mode 100644 index 000000000..dd7c427f3 --- /dev/null +++ b/packages/stryker-js-instrumenter/src/Traversal.schema.ts @@ -0,0 +1,3 @@ +import { Schema as S } from 'effect' + +export class TraversalStopped extends S.TaggedError()('TraversalStopped', {}) {} diff --git a/packages/stryker-js-instrumenter/src/estree.ts b/packages/stryker-js-instrumenter/src/estree.ts deleted file mode 100644 index 708eaf7e9..000000000 --- a/packages/stryker-js-instrumenter/src/estree.ts +++ /dev/null @@ -1,735 +0,0 @@ -// oxlint-disable typescript/no-unsafe-type-assertion typescript/no-unnecessary-type-assertion -// ^ An AST toolkit: builders and a walker over plain oxc nodes whose shapes the -// type system cannot express (estree unions vs oxc serializer output). -/** - * ESTree toolkit — node builders, kind classification and a walker with - * parent-chain paths. The walker replaces the previous traversal stack: oxc hands - * back standard ESTree, so the instrumenter owns the traversal instead of - * without pulling scope machinery along. - */ -import * as Predicate from 'effect/Predicate' -import type { - BlockStatement, - Expression, - Identifier, - Node, - Program, - Statement, - TemplateElement, - UnaryOperator, -} from 'estree' - -import { computeLineStarts, positionFromOffset } from './Syntax.js' - -export type { Expression, Program, Statement } - -// --------------------------------------------------------------------------- -// Kind classification -// --------------------------------------------------------------------------- - -const EXPRESSION_KINDS: ReadonlySet = new Set([ - 'ArrayExpression', - 'ArrowFunctionExpression', - 'AwaitExpression', - 'BinaryExpression', - 'CallExpression', - 'ChainExpression', - 'ClassExpression', - 'ConditionalExpression', - 'FunctionExpression', - 'Identifier', - 'Import', - 'ImportExpression', - 'JSXElement', - 'JSXFragment', - 'Literal', - 'LogicalExpression', - 'MetaProperty', - 'NewExpression', - 'ObjectExpression', - 'PrivateIdentifier', - 'SequenceExpression', - 'Super', - 'TaggedTemplateExpression', - 'TemplateLiteral', - 'ThisExpression', - 'TSAsExpression', - 'TSInstantiationExpression', - 'TSNonNullExpression', - 'TSSatisfiesExpression', - 'TSTypeAssertion', - 'UnaryExpression', - 'UpdateExpression', - 'YieldExpression', -]) - -const STATEMENT_KINDS: ReadonlySet = new Set([ - 'BlockStatement', - 'BreakStatement', - 'ClassDeclaration', - 'ContinueStatement', - 'DebuggerStatement', - 'DoWhileStatement', - 'ExportAllDeclaration', - 'ExportDefaultDeclaration', - 'ExportNamedDeclaration', - 'ExpressionStatement', - 'ForInStatement', - 'ForOfStatement', - 'ForStatement', - 'FunctionDeclaration', - 'IfStatement', - 'ImportDeclaration', - 'LabeledStatement', - 'ReturnStatement', - 'SwitchStatement', - 'ThrowStatement', - 'TSExportAssignment', - 'TSImportEqualsDeclaration', - 'TSInterfaceDeclaration', - 'TSModuleDeclaration', - 'TSTypeAliasDeclaration', - 'VariableDeclaration', - 'WhileStatement', - 'WithStatement', -]) - -/** - * The node's [start, end) offsets. oxc is invoked with `range: true`, so every - * parsed node carries one; synthesized builders omit it. - */ -export function spanOf(node: Node): { start: number; end: number } | undefined { - const range = node.range - if (range === undefined) return undefined - return { start: range[0], end: range[1] } -} - -/** - * The node's `type` tag, for checks against kinds outside the @types/estree - * union (TS and JSX extensions oxc emits). - */ -export function nodeType(node: unknown): string | undefined { - if (!isAstNode(node)) return undefined - return node.type -} - -export function isExpressionKind(node: Node | undefined | null): boolean { - const type = nodeType(node) - return type !== undefined && EXPRESSION_KINDS.has(type) -} - -export function isStatementKind(node: Node | undefined | null): boolean { - const type = nodeType(node) - return type !== undefined && STATEMENT_KINDS.has(type) -} - -// --------------------------------------------------------------------------- -// Builders — the shapes oxc's ESTree serializer emits, so a synthesized node -// is indistinguishable from a parsed one when printed. -// --------------------------------------------------------------------------- - -type Loc = { start: number; end: number } | undefined - -function mark(node: T, loc: Loc): T { - if (loc !== undefined) { - return Object.assign(node, { start: loc.start, end: loc.end }) - } - return node -} - -export function identifier(name: string, loc?: Loc): Identifier { - return mark({ type: 'Identifier', name }, loc) -} - -export function stringLiteral(value: string, loc?: Loc): Expression { - return mark({ type: 'Literal', value }, loc) -} - -export function booleanLiteral(value: boolean, loc?: Loc): Expression { - return mark({ type: 'Literal', value }, loc) -} - -export function regExpLiteral(pattern: string, flags: string, loc?: Loc): Expression { - return mark({ type: 'Literal', value: null, regex: { pattern, flags } }, loc) -} - -export function arrayExpression(elements: ReadonlyArray = [], loc?: Loc): Expression { - return mark({ type: 'ArrayExpression', elements: elements.filter(Predicate.isNotNullish) }, loc) -} - -export function callExpression( - callee: Expression, - args: ReadonlyArray = [], - optional?: boolean, - loc?: Loc, -): Expression { - return mark({ type: 'CallExpression', callee, arguments: [...args], optional: optional === true }, loc) -} - -export function newExpression(callee: Expression, args: ReadonlyArray = [], loc?: Loc): Expression { - return mark({ type: 'NewExpression', callee, arguments: [...args] }, loc) -} - -export function memberExpression( - object: Expression, - property: Expression, - computed: boolean, - optional: boolean, - loc?: Loc, -): Expression { - return mark({ type: 'MemberExpression', object, property, computed, optional }, loc) -} - -export function optionalMemberExpression( - object: Expression, - property: Expression, - computed: boolean, - optional: boolean, - loc?: Loc, -): Expression { - return mark({ type: 'MemberExpression', object, property, computed, optional }, loc) -} - -export function optionalCallExpression( - callee: Expression, - args: ReadonlyArray, - optional: boolean, - loc?: Loc, -): Expression { - return mark({ type: 'CallExpression', callee, arguments: [...args], optional }, loc) -} - -export function arrowFunctionExpression( - params: ReadonlyArray, - body: Expression | Statement, - loc?: Loc, -): Expression { - // Callers pass `Expression` or `blockStatement()` output; estree narrows the - // field to `BlockStatement | Expression`, which this construction satisfies. - const fnBody = body as BlockStatement | Expression - return mark( - { - type: 'ArrowFunctionExpression', - params: [...params], - body: fnBody, - async: false, - expression: fnBody.type !== 'BlockStatement', - }, - loc, - ) -} - -export function blockStatement(body: ReadonlyArray, loc?: Loc): Statement { - return mark({ type: 'BlockStatement', body: [...body] }, loc) -} - -export function expressionStatement(expression: Expression, loc?: Loc): Statement { - return mark({ type: 'ExpressionStatement', expression }, loc) -} - -export function ifStatement( - test: Expression, - consequent: Statement, - alternate?: Statement | null, - loc?: Loc, -): Statement { - return mark({ type: 'IfStatement', test, consequent, alternate: alternate ?? null }, loc) -} - -export function variableDeclarator(id: Identifier, init: Expression | null, loc?: Loc): VariableDeclaratorNode { - return mark({ type: 'VariableDeclarator', id, init }, loc) -} - -export function variableDeclaration( - kind: 'const' | 'let' | 'var', - declarations: ReadonlyArray, - loc?: Loc, -): Statement { - return mark({ type: 'VariableDeclaration', kind, declarations: [...declarations] }, loc) -} - -export function returnStatement(argument: Expression | null, loc?: Loc): Statement { - return mark({ type: 'ReturnStatement', argument }, loc) -} - -export function sequenceExpression(expressions: ReadonlyArray, loc?: Loc): Expression { - return mark({ type: 'SequenceExpression', expressions: [...expressions] }, loc) -} - -export function conditionalExpression( - test: Expression, - consequent: Expression, - alternate: Expression, - loc?: Loc, -): Expression { - return mark({ type: 'ConditionalExpression', test, consequent, alternate }, loc) -} - -export function unaryExpression( - operator: Extract, - argument: Expression, - loc?: Loc, -): Expression { - // Synthesized unaries are always prefixed; the postfix case is UpdateExpression. - return mark({ type: 'UnaryExpression', operator, argument, prefix: true }, loc) -} - -export function updateExpression(operator: '++' | '--', argument: Expression, prefix: boolean, loc?: Loc): Expression { - return mark({ type: 'UpdateExpression', operator, argument, prefix }, loc) -} - -export function templateElement(raw: string, loc?: Loc): TemplateElement { - // The only synthesized quasi is a single fully-tail element. - return mark({ type: 'TemplateElement', value: { raw, cooked: raw }, tail: true }, loc) -} - -export function templateLiteral( - quasis: ReadonlyArray, - expressions: ReadonlyArray, - loc?: Loc, -): Expression { - return mark({ type: 'TemplateLiteral', quasis: [...quasis], expressions: [...expressions] }, loc) -} - -export function switchCase(test: Expression | null, consequent: ReadonlyArray, loc?: Loc): SwitchCaseNode { - return mark({ type: 'SwitchCase', test, consequent: [...consequent] }, loc) -} - -/** - * Deep clone of a plain ESTree node. oxc nodes are JSON-shaped, so - * Deep clone over plain nodes: no prototypes to preserve. - */ -export function cloneNode(node: T): T { - return structuredClone(node) -} - -interface VariableDeclaratorNode { - type: 'VariableDeclarator' - id: Identifier - init: Expression | null -} - -interface SwitchCaseNode { - type: 'SwitchCase' - test: Expression | null - consequent: Statement[] -} - -// --------------------------------------------------------------------------- -// Comments -// --------------------------------------------------------------------------- - -export interface AttachedComment { - readonly type: 'Line' | 'Block' - readonly value: string - readonly start: number - readonly end: number - readonly loc?: { start: { line: number; column: number }; end: { line: number; column: number } } -} - -/** - * Attaches every comment to the node it precedes (`leadingComments`) or, when - * no node follows it, to the node it trails (`trailingComments`). oxc ships - * comments flat, and the Stryker directive pass reads `node.leadingComments`, - * so the flat list is folded into the tree once per transform with the file's - * line table in hand. - */ -export function attachComments( - root: Node, - comments: ReadonlyArray, - lineTable: readonly number[], -): void { - if (comments.length === 0) return - // The Program root is not a comment host: its span starts at the first - // statement, so a leading file comment would otherwise attach to it and - // never print. Candidates are the statements and expressions under it. - const nodes = collectNodes(root).filter((entry) => entry.node !== root) - nodes.sort((a, b) => a.start - b.start) - const groups = groupComments(nodes, comments) - assignComments(groups.leading, lineTable, 'leadingComments') - assignComments(groups.trailing, lineTable, 'trailingComments') -} - -interface CommentGroups { - readonly leading: Map - readonly trailing: Map -} - -interface CommentHost { - readonly field: keyof CommentGroups - readonly node: Node -} - -function groupComments(nodes: ReadonlyArray, comments: ReadonlyArray): CommentGroups { - const groups: CommentGroups = { leading: new Map(), trailing: new Map() } - for (const comment of comments) hostComment(nodes, comment, groups) - return groups -} - -/** - * The map a comment is hosted in. A comment attaches to the node it precedes; - * one that precedes nothing — a same-line trailing comment — attaches to the - * nearest statement before it, because the statement printer is the only - * emitter of trailing comments and an expression host would never reach the - * output. - */ -function hostComment(nodes: ReadonlyArray, comment: AttachedComment, groups: CommentGroups): void { - const hosts: ReadonlyArray<{ readonly field: keyof CommentGroups; readonly node: Node | undefined }> = [ - { field: 'leading', node: followingNode(nodes, comment) }, - { field: 'trailing', node: precedingStatement(nodes, comment) }, - ] - const host = hosts.find((candidate): candidate is CommentHost => candidate.node !== undefined) - if (host !== undefined) pushComment(groups[host.field], host.node, comment) -} - -function followingNode(nodes: ReadonlyArray, comment: AttachedComment): Node | undefined { - const entry = nodes.find((candidate) => candidate.start >= comment.end) - if (entry === undefined) return undefined - return entry.node -} - -function precedingStatement(nodes: ReadonlyArray, comment: AttachedComment): Node | undefined { - const entry = nodes.findLast((candidate) => candidate.end <= comment.start && isStatementKind(candidate.node)) - if (entry === undefined) return undefined - return entry.node -} - -function assignComments( - map: Map, - lineTable: readonly number[], - field: 'leadingComments' | 'trailingComments', -): void { - for (const [node, list] of map) { - const located = list.map((comment) => ({ - ...comment, - loc: { - start: positionFromLineTable(comment.start, lineTable), - end: positionFromLineTable(comment.end, lineTable), - }, - })) - Object.assign(node, { [field]: located }) - } -} - -function pushComment(map: Map, node: Node, comment: AttachedComment): void { - const list = map.get(node) - if (list === undefined) map.set(node, [comment]) - else list.push(comment) -} - -interface NodeEntry { - readonly node: Node - readonly start: number - readonly end: number -} - -/** oxc emits a node's children as a plain array the walker reads and replaces into. */ -const isNodeList = (value: unknown): value is Array => Array.isArray(value) - -function collectNodes(node: unknown): NodeEntry[] { - const out: NodeEntry[] = [] - collect(node, out) - return out -} - -function collect(node: unknown, out: NodeEntry[]): void { - if (isNodeList(node)) return collectList(node, out) - collectAstNode(node, out) -} - -function collectList(items: ReadonlyArray, out: NodeEntry[]): void { - for (const item of items) collect(item, out) -} - -function collectAstNode(value: unknown, out: NodeEntry[]): void { - if (!isAstNode(value)) return - appendEntry(value, out) - collectChildren(value, out) -} - -function collectChildren(node: Node & Record, out: NodeEntry[]): void { - for (const key of Object.keys(node)) collectChild(node, key, out) -} - -function collectChild(node: Node & Record, key: string, out: NodeEntry[]): void { - if (SKIP_KEYS.has(key)) return - collect(node[key], out) -} - -function appendEntry(node: Node & Record, out: NodeEntry[]): void { - const span = spanOf(node) - if (span === undefined) return - out.push({ node, start: span.start, end: span.end }) -} - -export function isAstNode(value: unknown): value is Node & Record { - return Predicate.isObject(value) && typeof value['type'] === 'string' -} - -// --------------------------------------------------------------------------- -// Line table — delegates to Syntax's line starts; positions here are 1-based -// (The `loc` shape the directive and API-location code compares). -// --------------------------------------------------------------------------- - -export function buildLineTable(content: string): readonly number[] { - return computeLineStarts(content) -} - -export function positionFromLineTable(offset: number, lineTable: readonly number[]): { line: number; column: number } { - const zeroBased = positionFromOffset(lineTable, offset) - return { line: zeroBased.line + 1, column: zeroBased.column } -} - -// --------------------------------------------------------------------------- -// Walker -// --------------------------------------------------------------------------- - -export interface TraversePath { - readonly node: Node - readonly parentPath: TraversePath | null - readonly parent: Node | undefined - readonly key: string - readonly listKey: string | undefined - skip(): void - stop(): void - find(predicate: (path: TraversePath) => boolean): TraversePath | undefined - getStatementParent(): TraversePath | undefined - replaceWith(node: Node): void - is(kind: string): boolean - isExpression(): boolean - isStatement(): boolean -} - -export interface TraverseVisitors { - enter?: TraverseVisitor - exit?: TraverseVisitor -} - -type TraverseVisitor = (path: TraversePath) => void - -interface WalkContext { - readonly skipped: Set - stopped: boolean -} - -const SKIP_KEYS: ReadonlySet = new Set([ - 'type', - 'start', - 'end', - 'range', - 'loc', - 'leadingComments', - 'trailingComments', -]) - -export function traverse(root: Program | Node, visitors: TraverseVisitors): void { - const context: WalkContext = { skipped: new Set(), stopped: false } - visit(root, null, 'root', undefined, visitors, context) -} - -function visit( - node: unknown, - parentPath: TraversePath | null, - key: string, - listKey: string | undefined, - visitors: TraverseVisitors, - context: WalkContext, -): void { - if (context.stopped) return - visitAstNode(node, parentPath, key, listKey, visitors, context) -} - -function visitAstNode( - node: unknown, - parentPath: TraversePath | null, - key: string, - listKey: string | undefined, - visitors: TraverseVisitors, - context: WalkContext, -): void { - if (!isVisitable(node, context)) return - const path = createPath(node, parentPath, key, listKey, context) - notify(visitors.enter, path) - visitEnteredNode(node, path, visitors, context) -} - -function isVisitable(node: unknown, context: WalkContext): node is Node & Record { - return isAstNode(node) && !context.skipped.has(node) -} - -function visitEnteredNode( - node: Node & Record, - path: TraversePath, - visitors: TraverseVisitors, - context: WalkContext, -): void { - if (context.skipped.has(node)) return - visitChildren(node, path, visitors, context) - notifyExit(visitors.exit, path, context) -} - -function notifyExit(visitor: TraverseVisitor | undefined, path: TraversePath, context: WalkContext): void { - if (context.stopped) return - notify(visitor, path) -} - -function notify(visitor: TraverseVisitor | undefined, path: TraversePath): void { - if (visitor === undefined) return - visitor(path) -} - -function visitChildren( - node: Node & Record, - path: TraversePath, - visitors: TraverseVisitors, - context: WalkContext, -): void { - Object.keys(node).every((key) => keepVisitingKey(node, path, key, visitors, context)) -} - -function keepVisitingKey( - node: Node & Record, - path: TraversePath, - key: string, - visitors: TraverseVisitors, - context: WalkContext, -): boolean { - if (SKIP_KEYS.has(key)) return true - return keepVisitingChild(node[key], path, key, visitors, context) -} - -function keepVisitingChild( - child: unknown, - path: TraversePath, - key: string, - visitors: TraverseVisitors, - context: WalkContext, -): boolean { - if (isNodeList(child)) return keepVisitingList(child, path, key, visitors, context) - return keepVisitingValue(child, path, key, undefined, visitors, context) -} - -function keepVisitingList( - items: ReadonlyArray, - path: TraversePath, - key: string, - visitors: TraverseVisitors, - context: WalkContext, -): boolean { - return items.every((item, index) => keepVisitingValue(item, path, key, String(index), visitors, context)) -} - -function keepVisitingValue( - value: unknown, - path: TraversePath, - key: string, - listKey: string | undefined, - visitors: TraverseVisitors, - context: WalkContext, -): boolean { - visit(value, path, key, listKey, visitors, context) - return !context.stopped -} - -function createPath( - node: Node, - parentPath: TraversePath | null, - key: string, - listKey: string | undefined, - context: WalkContext, -): TraversePath { - const path: TraversePath = { - node, - parentPath, - parent: parentPath?.node, - key, - listKey, - skip() { - context.skipped.add(node) - }, - stop() { - context.stopped = true - }, - find(predicate) { - // `path.find` includes the path itself: a node that registered as its - // own placement anchor wins over any ancestor. - return nearest(path, predicate) - }, - getStatementParent() { - return nearest(parentPath, (ancestor) => isStatementKind(ancestor.node)) - }, - replaceWith(replacement) { - replaceInParent(parentPath, key, listKey, replacement) - }, - is(kind) { - return node.type === kind - }, - isExpression() { - return isExpressionKind(node) - }, - isStatement() { - return isStatementKind(node) - }, - } - return path -} - -function nearest( - from: TraversePath | null, - predicate: (path: TraversePath) => boolean, -): TraversePath | undefined { - if (from === null) return undefined - return takeOrAscend(from, predicate) -} - -function takeOrAscend( - path: TraversePath, - predicate: (path: TraversePath) => boolean, -): TraversePath | undefined { - if (predicate(path)) return path - return nearest(path.parentPath, predicate) -} - -function replaceInParent( - parentPath: TraversePath | null, - key: string, - listKey: string | undefined, - replacement: Node, -): void { - if (parentPath === null) return - replaceChild(parentPath.node, key, listKey, replacement) -} - -function replaceChild(parent: Node, key: string, listKey: string | undefined, replacement: Node): void { - if (!isAstNode(parent)) return - assignChild(parent, key, listKey, replacement) -} - -function assignChild( - parent: Node & Record, - key: string, - listKey: string | undefined, - replacement: Node, -): void { - if (listKey === undefined) { - parent[key] = replacement - return - } - assignIndexed(parent, key, listKey, replacement) -} - -function assignIndexed( - parent: Record, - key: string, - listKey: string, - replacement: Node, -): void { - // Array descent is inlined in `visitChild`, so a list key is always a single index. - const index = Number(listKey) - const container = parent[key] - if (isIndexedList(container, index)) container[index] = replacement -} - -function isIndexedList(container: unknown, index: number): container is Array { - return isNodeList(container) && Number.isInteger(index) -} diff --git a/packages/stryker-js-instrumenter/src/print/index.ts b/packages/stryker-js-instrumenter/src/print/index.ts index 45642d15c..01c3b7761 100644 --- a/packages/stryker-js-instrumenter/src/print/index.ts +++ b/packages/stryker-js-instrumenter/src/print/index.ts @@ -131,8 +131,7 @@ import type { WhileStatement, WithStatement, YieldExpression, -} from '@oxc-project/types' -import type { Program as EstreeProgram } from 'estree' +} from '../Ast.js' // --------------------------------------------------------------------------- // Public API @@ -158,14 +157,9 @@ export interface PrintOptions { export interface PrintProgramOptions extends PrintOptions {} -// Primary entry — the Printer.ts replacement and the print property suite's -// entry. Accepts the estree `Program` shape the instrumenter's ASTs carry -// (oxc's serializer output satisfies it); the internal renderer keeps its -// @oxc-project/types view of the same plain objects. -export function printProgram(program: Program | EstreeProgram, opts: PrintProgramOptions = {}): string { +export function printProgram(program: Program, opts: PrintProgramOptions = {}): string { const state = new PrintState(opts) - // One boundary: the renderer's oxc-typed view of the same plain node tree. - return state.printProgram(program as Program) + return state.printProgram(program) } // Convenience: print any single node (used for synthesized replacement snippets) diff --git a/packages/stryker-js-instrumenter/tests/angular-ignorer.integration.test.ts b/packages/stryker-js-instrumenter/tests/angular-ignorer.integration.test.ts new file mode 100644 index 000000000..30f81a3a2 --- /dev/null +++ b/packages/stryker-js-instrumenter/tests/angular-ignorer.integration.test.ts @@ -0,0 +1,67 @@ +import { Gherkin, Given, it, layer, makeFeature, Then, When } from '@systemfsoftware/effect-gherkin-spec' +import { angularIgnorer } from '@systemfsoftware/stryker-js-instrumenter' +import { Effect } from 'effect' +import { expect } from 'vitest' + +import { instrument } from './__fixtures__/instrument.js' + +const ANGULAR_SOURCE = `class C { + foo = input.required({ required: true }) + bar = viewChild(BarToken, { read: BarToken }) + baz = compute({ keep: 1 }) +} +` + +const INPUT_MODEL_OUTPUT_CONFIG_MSG = + 'Angular signal based input, model and output functions configuration object cannot be mutated as that causes issues with the Angular compiler.' +const SIGNAL_QUERY_OPTIONS_MSG = + 'Angular signal query options object cannot be mutated as that causes issues with the Angular compiler.' + +interface Mutant { + readonly mutatorName: string + readonly status?: string + readonly statusReason?: string +} + +const Feature = makeFeature({ it, layer }) + +Feature('Angular signal configuration objects') + .body(({ scenario }) => { + scenario( + 'Signal configuration objects are ignored while a plain object keeps its mutants', + Gherkin.Do.pipe( + Given('a class holding a signal input config, a signal query config and a plain object')( + 'source', + () => Effect.succeed(ANGULAR_SOURCE), + ), + When('it is instrumented with the Angular ignorer')( + 'result', + ({ source }: { source: string }) => + instrument([{ name: '/tmp/probe/angular.ts', content: source, mutate: true }], { + ignorers: [angularIgnorer], + excludedMutations: [], + }), + ), + Then('the two signal configs are ignored and the plain object is not')(( + { result }: { result: { mutants: readonly Mutant[] } }, + ) => + Effect.sync(() => { + const reasons = result.mutants + .filter((mutant) => mutant.status === 'Ignored') + .map((mutant) => mutant.statusReason) + expect( + reasons.every( + (reason) => reason === INPUT_MODEL_OUTPUT_CONFIG_MSG || reason === SIGNAL_QUERY_OPTIONS_MSG, + ), + ).toBe(true) + expect(reasons).toContain(INPUT_MODEL_OUTPUT_CONFIG_MSG) + expect(reasons).toContain(SIGNAL_QUERY_OPTIONS_MSG) + + const objectLiterals = result.mutants.filter((mutant) => mutant.mutatorName === 'ObjectLiteral') + expect(objectLiterals.filter((mutant) => mutant.status === 'Ignored')).toHaveLength(2) + expect(objectLiterals.filter((mutant) => mutant.status !== 'Ignored')).toHaveLength(1) + }) + ), + ), + ) + }) diff --git a/packages/stryker-js-instrumenter/tests/instrumenter.integration.test.ts b/packages/stryker-js-instrumenter/tests/instrumenter.integration.test.ts index 68953a14a..798512dbd 100644 --- a/packages/stryker-js-instrumenter/tests/instrumenter.integration.test.ts +++ b/packages/stryker-js-instrumenter/tests/instrumenter.integration.test.ts @@ -1,4 +1,5 @@ import { Gherkin, Given, it, layer, makeFeature, Then, When } from '@systemfsoftware/effect-gherkin-spec' +import type { IgnorerService } from '@systemfsoftware/stryker-js-language' import { Effect } from 'effect' import * as Option from 'effect/Option' import { expect } from 'vitest' @@ -35,49 +36,45 @@ type Mutant = { replacement?: string } -type IgnorerPath = { node: unknown; parentPath?: IgnorerPath | null } - -const isRecord = (value: unknown): value is Record => typeof value === 'object' && value !== null - const isKeepCall = (node: unknown): node is { arguments: unknown[] } => { - if (!isRecord(node) || node['type'] !== 'CallExpression') { + if (typeof node !== 'object' || node === null || !('type' in node) || node.type !== 'CallExpression') { return false } - const callee = node['callee'] - return isRecord(callee) && callee['type'] === 'Identifier' && callee['name'] === 'keep' && - Array.isArray(node['arguments']) + if (!('callee' in node) || !('arguments' in node)) { + return false + } + const callee = node.callee + return typeof callee === 'object' && callee !== null && 'type' in callee && callee.type === 'Identifier' && + 'name' in callee && callee.name === 'keep' && Array.isArray(node.arguments) } -const invertedKeepIgnorer = { - shouldIgnore: (path: IgnorerPath) => { +const invertedKeepIgnorer: IgnorerService = { + shouldIgnore: (path) => { let child: unknown = path.node - for (let current = path.parentPath; current; current = current.parentPath) { - if (isKeepCall(current.node) && current.node.arguments.includes(child)) { + for (const ancestor of path.ancestors) { + if (isKeepCall(ancestor) && ancestor.arguments.includes(child)) { return Option.none() } - child = current.node + child = ancestor } return Option.some(OUTSIDE_KEEP) }, } const isFlagIf = (node: unknown): boolean => { - if (!isRecord(node) || node['type'] !== 'IfStatement') { + if (typeof node !== 'object' || node === null || !('type' in node) || node.type !== 'IfStatement') { return false } - const test = node['test'] - return isRecord(test) && test['type'] === 'Identifier' && test['name'] === 'flag' + if (!('test' in node)) { + return false + } + const test = node.test + return typeof test === 'object' && test !== null && 'type' in test && test.type === 'Identifier' && + 'name' in test && test.name === 'flag' } -const regionFlagIgnorer = { - shouldIgnore: (path: IgnorerPath) => { - for (let current = path.parentPath; current; current = current.parentPath) { - if (isFlagIf(current.node)) { - return Option.some(INSIDE_FLAG) - } - } - return Option.none() - }, +const regionFlagIgnorer: IgnorerService = { + shouldIgnore: (path) => path.ancestors.some(isFlagIf) ? Option.some(INSIDE_FLAG) : Option.none(), } const countByMutator = (mutants: readonly Mutant[]): Record => { const counts: Record = {} diff --git a/packages/stryker-js-instrumenter/tests/svelte-parsing.integration.test.ts b/packages/stryker-js-instrumenter/tests/svelte-parsing.integration.test.ts index b4f336942..7c86bec29 100644 --- a/packages/stryker-js-instrumenter/tests/svelte-parsing.integration.test.ts +++ b/packages/stryker-js-instrumenter/tests/svelte-parsing.integration.test.ts @@ -22,7 +22,7 @@ Feature('Svelte component instrumentation') scenario( // Svelte is an OPTIONAL PEER, not a bundled dependency: the parser reads the // installed compiler's `VERSION` and branches on v4 versus v5, and resolves - // `estree-walker` relative to that same install. A build that inlines a copy + // `oxc-walker` relative to that same install. A build that inlines a copy // of the compiler pins whichever version happened to be present when the // package was built, and the version branch then reads the wrong answer. // This scenario fails if the compiler stops resolving from the consumer's diff --git a/packages/stryker-js-language/api-extractor.json b/packages/stryker-js-language/api-extractor.json new file mode 100644 index 000000000..8296d8414 --- /dev/null +++ b/packages/stryker-js-language/api-extractor.json @@ -0,0 +1,55 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "projectFolder": ".", + "mainEntryPointFilePath": "/dist/index.d.mts", + "bundledPackages": [], + "compiler": { + "tsconfigFilePath": "/tsconfig.json" + }, + "apiReport": { + "enabled": true, + "reportFolder": "/etc/", + "reportTempFolder": "/temp/main/", + "reportFileName": "stryker-js-language.api.md" + }, + "docModel": { + "enabled": false + }, + "dtsRollup": { + "enabled": true, + "untrimmedFilePath": "/dist/index.d.mts" + }, + "tsdocMetadata": { + "enabled": false + }, + "messages": { + "extractorMessageReporting": { + "default": { + "logLevel": "warning" + }, + "ae-forgotten-export": { + "logLevel": "error" + }, + "ae-missing-release-tag": { + "logLevel": "none" + }, + "ae-internal-missing-underscore": { + "logLevel": "none" + } + }, + "tsdocMessageReporting": { + "default": { + "logLevel": "warning" + }, + "tsdoc-undefined-tag": { + "logLevel": "none" + }, + "tsdoc-unsupported-tag": { + "logLevel": "none" + }, + "tsdoc-param-tag-missing-hyphen": { + "logLevel": "none" + } + } + } +} diff --git a/packages/stryker-js-language/etc/stryker-js-language.api.md b/packages/stryker-js-language/etc/stryker-js-language.api.md new file mode 100644 index 000000000..67c12fce9 --- /dev/null +++ b/packages/stryker-js-language/etc/stryker-js-language.api.md @@ -0,0 +1,1368 @@ +## API Report File for "@systemfsoftware/stryker-js-language" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import * as Cause from 'effect/Cause'; +import * as Context from 'effect/Context'; +import * as Effect from 'effect/Effect'; +import * as Exit from 'effect/Exit'; +import * as HashMap from 'effect/HashMap'; +import * as Option from 'effect/Option'; +import * as Queue from 'effect/Queue'; +import * as S from 'effect/Schema'; +import { StandardSchemaV1 } from 'effect/StandardSchema'; +import { YieldableError } from 'effect/Cause'; + +// @public (undocumented) +export interface BaseTestResult { + // (undocumented) + readonly fileName?: string; + // (undocumented) + readonly id: string; + // (undocumented) + readonly name: string; + // (undocumented) + readonly startPosition?: Position; + // (undocumented) + readonly timeSpentMs: number; +} + +// @public (undocumented) +export type BrandingInformation = typeof BrandingInformationSchema.Type; + +// @public (undocumented) +export const BrandingInformationSchema: S.Struct<{ + readonly homepageUrl: S.String; + readonly imageUrl: S.optional; +}>; + +// @public (undocumented) +export const calculateMetrics: (files: Readonly>) => MetricsResult; + +// @public (undocumented) +export const causeText: (cause: unknown, depth: number) => string | undefined; + +// Warning: (ae-forgotten-export) The symbol "Checker_base" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export class Checker extends Checker_base {} + +// Warning: (ae-forgotten-export) The symbol "CheckerFailed_base" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export class CheckerFailed extends CheckerFailed_base {} + +// @public (undocumented) +export interface CheckerService { + // (undocumented) + readonly check: (mutants: readonly Mutant[]) => Effect.Effect, CheckerFailed>; + // (undocumented) + readonly group: (mutants: readonly Mutant[]) => Effect.Effect; + // (undocumented) + readonly init: Effect.Effect; +} + +// @public (undocumented) +export type CheckResult = FailedCheckResult | PassedCheckResult; + +// @public (undocumented) +export const CheckResultSchema: S.Union; +}>, S.Struct<{ + readonly status: S.Literal<"compileError">; + readonly reason: S.String; +}>]>; + +// @public (undocumented) +export const CheckStatus: S.Literals; + +// @public (undocumented) +export type CheckStatus = typeof CheckStatus.Type; + +// @public (undocumented) +export const classifyExit: (command: ClassifyExitCommand) => ClassifyExitDecision; + +// Warning: (ae-forgotten-export) The symbol "ClassifyExitCommand_base" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export class ClassifyExitCommand extends ClassifyExitCommand_base {} + +// Warning: (ae-forgotten-export) The symbol "ClassifyExitDecision_base" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export class ClassifyExitDecision extends ClassifyExitDecision_base {} + +// @public (undocumented) +export type CommandRunnerOptions = S.Schema.Type; + +// @public (undocumented) +export const CommandRunnerOptionsSchema: S.StructWithRest; +}>, readonly [S.$Record]>; + +// @public (undocumented) +export interface CompleteDryRunResult { + // (undocumented) + readonly mutantCoverage?: MutantCoverage; + // (undocumented) + readonly status: 'complete'; + // (undocumented) + readonly tests: readonly TestResult[]; +} + +// @public (undocumented) +export const countMutants: (mutants: readonly MutantResult[]) => Metrics; + +// @public (undocumented) +export interface Coverage { + // (undocumented) + readonly perTest: CoveragePerTestId; + // (undocumented) + readonly static: CoverageData; +} + +// @public (undocumented) +export type CoverageAnalysis = 'off' | 'all' | 'perTest'; + +// @public (undocumented) +const CoverageAnalysisMode: S.Literals; + +// @public (undocumented) +type CoverageAnalysisMode = typeof CoverageAnalysisMode.Type; +export { CoverageAnalysisMode } +export { CoverageAnalysisMode as CoverageAnalysisModeType } + +// @public (undocumented) +export const CoverageAnalysisSchema: S.Literals; + +// @public (undocumented) +export type CoverageData = Record; + +// @public (undocumented) +export type CoveragePerTestId = Record; + +// @public +export type DeepOptional = { -readonly [P in keyof T]?: T[P] extends Record ? DeepOptional | undefined : T[P]; }; + +// @public (undocumented) +export type Dependencies = typeof DependenciesSchema.Type; + +// @public (undocumented) +export const DependenciesSchema: S.$Record; + +// Warning: (ae-forgotten-export) The symbol "DryRunCompleted_base" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export class DryRunCompleted extends DryRunCompleted_base {} + +// @public (undocumented) +export interface DryRunOptions extends RunOptions { + // (undocumented) + readonly coverageAnalysis: CoverageAnalysis; + // (undocumented) + readonly files?: readonly string[]; + // (undocumented) + readonly testFiles?: readonly string[]; +} + +// @public (undocumented) +export const DryRunOptionsSchema: S.Struct<{ + readonly timeout: S.Finite; + readonly disableBail: S.Boolean; + readonly coverageAnalysis: S.Literals; + readonly files: S.optionalKey>; + readonly testFiles: S.optionalKey>; +}>; + +// @public (undocumented) +export type DryRunResult = CompleteDryRunResult | ErrorDryRunResult | TimeoutDryRunResult; + +// @public (undocumented) +export const DryRunResultSchema: S.Union; + readonly tests: S.$Array; + readonly startPosition: S.optionalKey>; + readonly status: S.Literal<"failed">; + readonly failureMessage: S.String; + }>, S.Struct<{ + readonly id: S.String; + readonly name: S.String; + readonly timeSpentMs: S.Finite; + readonly fileName: S.optionalKey; + readonly startPosition: S.optionalKey>; + readonly status: S.Literal<"skipped">; + }>, S.Struct<{ + readonly id: S.String; + readonly name: S.String; + readonly timeSpentMs: S.Finite; + readonly fileName: S.optionalKey; + readonly startPosition: S.optionalKey>; + readonly status: S.Literal<"success">; + }>]>>; + readonly mutantCoverage: S.optionalKey>; + readonly static: S.$Record; + }>>; +}>, S.Struct<{ + readonly status: S.Literal<"timeout">; + readonly reason: S.optionalKey; +}>, S.Struct<{ + readonly status: S.Literal<"error">; + readonly errorMessage: S.String; +}>]>; + +// @public (undocumented) +export const DryRunStatus: S.Literals; + +// @public (undocumented) +export type DryRunStatus = typeof DryRunStatus.Type; + +// @public (undocumented) +export interface EarlyResultPlan { + // (undocumented) + readonly mutant: Mutant; + // (undocumented) + readonly plan: 'EarlyResult'; +} + +// @public (undocumented) +export interface ErrnoException extends Error { + // (undocumented) + code?: string; + // (undocumented) + errno?: number; + // (undocumented) + path?: string; + // (undocumented) + syscall?: string; +} + +// @public (undocumented) +export const ERROR_CODES: Readonly<{ + NoSuchFileOrDirectory: 'ENOENT'; +}>; + +// @public (undocumented) +export interface ErrorDryRunResult { + // (undocumented) + readonly errorMessage: string; + // (undocumented) + readonly status: 'error'; +} + +// @public (undocumented) +export interface ErrorMutantRunResult { + // (undocumented) + readonly errorMessage: string; + // (undocumented) + readonly status: 'error'; +} + +// @public (undocumented) +export function errorToString(error: unknown): string; + +// Warning: (ae-forgotten-export) The symbol "Evaluator_base" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export class Evaluator extends Evaluator_base {} + +// Warning: (ae-forgotten-export) The symbol "EvaluatorFailed_base" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export class EvaluatorFailed extends EvaluatorFailed_base {} + +// @public (undocumented) +export interface EvaluatorService { + // (undocumented) + readonly evaluate: (report: MutationTestResult) => Effect.Effect; +} + +// @public (undocumented) +export const EXIT_CODE: Record; + +// @public (undocumented) +export const ExitClass: S.Literals; + +// @public (undocumented) +export type ExitClass = typeof ExitClass.Type; + +// @public (undocumented) +export interface FailedCheckResult { + // (undocumented) + readonly reason: string; + // (undocumented) + readonly status: 'compileError'; +} + +// @public (undocumented) +export interface FailedTestResult extends BaseTestResult { + // (undocumented) + readonly failureMessage: string; + // (undocumented) + readonly status: 'failed'; +} + +// @public (undocumented) +export interface FileDescription { + // (undocumented) + readonly mutate: MutateDescription; +} + +// @public (undocumented) +export type FileDescriptions = Record; + +// @public (undocumented) +export type FileResult = typeof FileResultSchema.Type; + +// @public (undocumented) +export type FileResultDictionary = typeof FileResultDictionarySchema.Type; + +// @public (undocumented) +export const FileResultDictionarySchema: S.$Record; + readonly location: S.Struct<{ + readonly start: S.Struct<{ + readonly line: S.Finite; + readonly column: S.Finite; + }>; + readonly end: S.Struct<{ + readonly line: S.Finite; + readonly column: S.Finite; + }>; + }>; + readonly replacement: S.optional; + readonly description: S.optional; + readonly statusReason: S.optional; + readonly static: S.optional; + readonly coveredBy: S.optional>; + readonly killedBy: S.optional>; + readonly testsCompleted: S.optional; + readonly duration: S.optional; + }>>; +}>>; + +// @public (undocumented) +export const FileResultSchema: S.Struct<{ + readonly language: S.String; + readonly source: S.String; + readonly mutants: S.$Array; + readonly location: S.Struct<{ + readonly start: S.Struct<{ + readonly line: S.Finite; + readonly column: S.Finite; + }>; + readonly end: S.Struct<{ + readonly line: S.Finite; + readonly column: S.Finite; + }>; + }>; + readonly replacement: S.optional; + readonly description: S.optional; + readonly statusReason: S.optional; + readonly static: S.optional; + readonly coveredBy: S.optional>; + readonly killedBy: S.optional>; + readonly testsCompleted: S.optional; + readonly duration: S.optional; + }>>; +}>; + +// @public (undocumented) +export type FrameworkInformation = typeof FrameworkInformationSchema.Type; + +// @public (undocumented) +export const FrameworkInformationSchema: S.Struct<{ + readonly name: S.String; + readonly version: S.optional; + readonly branding: S.optional; + }>>; + readonly dependencies: S.optional>; +}>; + +// Warning: (ae-forgotten-export) The symbol "Heartbeat_base" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export class Heartbeat extends Heartbeat_base {} + +// Warning: (ae-forgotten-export) The symbol "HelpRendered_base" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export class HelpRendered extends HelpRendered_base {} + +// @public (undocumented) +export function highestExitClass(pending: Iterable): ExitClass | null; + +// Warning: (ae-forgotten-export) The symbol "Ignorer_base" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export class Ignorer extends Ignorer_base {} + +// @public (undocumented) +export interface IgnorerService { + // (undocumented) + readonly shouldIgnore: (path: NodePath) => Option.Option; +} + +// @public (undocumented) +export const INSTRUMENTER_CONSTANTS: Readonly<{ + NAMESPACE: '__stryker__'; + MUTATION_COVERAGE_OBJECT: 'mutantCoverage'; + ACTIVE_MUTANT: 'activeMutant'; + CURRENT_TEST_ID: 'currentTestId'; + HIT_COUNT: 'hitCount'; + HIT_LIMIT: 'hitLimit'; + ACTIVE_MUTANT_ENV_VARIABLE: '__STRYKER_ACTIVE_MUTANT__'; +}>; + +// @public (undocumented) +export interface InstrumenterContext { + // (undocumented) + activeMutant?: string; + // (undocumented) + currentTestId?: string; + // (undocumented) + hitCount?: number; + // (undocumented) + hitLimit?: number; + // (undocumented) + mutantCoverage?: MutantCoverage; +} + +// @public (undocumented) +export function isErrnoException(error: unknown): error is ErrnoException; + +// @public (undocumented) +export const isMutant: (value: unknown) => value is Mutant; + +// @public (undocumented) +export interface KilledMutantRunResult { + // (undocumented) + readonly failureMessage: string; + // (undocumented) + readonly killedBy: readonly string[]; + // (undocumented) + readonly nrOfTests: number; + // (undocumented) + readonly status: 'killed'; +} + +// @public +export type KnownKeys = keyof { [P in keyof T as string extends P ? never : number extends P ? never : P]: T[P]; }; + +// @public (undocumented) +export type Location = typeof LocationSchema.Type; + +// @public (undocumented) +export const LocationSchema: S.Struct<{ + readonly start: S.Struct<{ + readonly line: S.Finite; + readonly column: S.Finite; + }>; + readonly end: S.Struct<{ + readonly line: S.Finite; + readonly column: S.Finite; + }>; +}>; + +// @public (undocumented) +const LogLevel: S.Literals; + +// @public +type LogLevel = typeof LogLevel.Type; +export { LogLevel } +export { LogLevel as LogLevelType } + +// @public (undocumented) +export type Metrics = typeof MetricsSchema.Type; + +// @public (undocumented) +export interface MetricsResult { + // (undocumented) + readonly childResults: readonly MetricsResult[]; + // (undocumented) + readonly metrics: Metrics; + // (undocumented) + readonly name: string; +} + +// @public (undocumented) +export const MetricsResultSchema: S.Struct<{ + readonly name: S.String; + readonly metrics: S.Struct<{ + readonly pending: S.Finite; + readonly killed: S.Finite; + readonly timeout: S.Finite; + readonly survived: S.Finite; + readonly noCoverage: S.Finite; + readonly runtimeErrors: S.Finite; + readonly compileErrors: S.Finite; + readonly ignored: S.Finite; + readonly totalDetected: S.Finite; + readonly totalUndetected: S.Finite; + readonly totalInvalid: S.Finite; + readonly totalValid: S.Finite; + readonly totalMutants: S.Finite; + readonly totalCovered: S.Finite; + readonly mutationScore: S.Finite; + readonly mutationScoreBasedOnCoveredCode: S.Finite; + }>; + readonly childResults: S.$Array>>; +}>; + +// @public (undocumented) +export const MetricsSchema: S.Struct<{ + readonly pending: S.Finite; + readonly killed: S.Finite; + readonly timeout: S.Finite; + readonly survived: S.Finite; + readonly noCoverage: S.Finite; + readonly runtimeErrors: S.Finite; + readonly compileErrors: S.Finite; + readonly ignored: S.Finite; + readonly totalDetected: S.Finite; + readonly totalUndetected: S.Finite; + readonly totalInvalid: S.Finite; + readonly totalValid: S.Finite; + readonly totalMutants: S.Finite; + readonly totalCovered: S.Finite; + readonly mutationScore: S.Finite; + readonly mutationScoreBasedOnCoveredCode: S.Finite; +}>; + +// @public (undocumented) +export const ModeSignal: S.Literals; + +// @public (undocumented) +export type ModeSignal = typeof ModeSignal.Type; + +// Warning: (ae-forgotten-export) The symbol "Module_base" needs to be exported by the entry point index.d.mts +// +// @public +export class Module extends Module_base {} + +// @public +export interface ModuleRequire { + // (undocumented) + (request: string): unknown; + // (undocumented) + resolve(request: string, options?: { + readonly paths?: readonly string[]; + }): string; +} + +// Warning: (ae-forgotten-export) The symbol "Mutant_base" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export class Mutant extends Mutant_base {} + +// @public (undocumented) +export type MutantActivation = 'runtime' | 'static'; + +// @public (undocumented) +export const MutantActivationSchema: S.Literals; + +// @public (undocumented) +export interface MutantCoverage { + // (undocumented) + readonly perTest: Record>; + // (undocumented) + readonly static: Record; +} + +// @public (undocumented) +export const MutantCoverageSchema: S.Struct<{ + readonly perTest: S.$Record>; + readonly static: S.$Record; +}>; + +// @public (undocumented) +export type MutantResult = typeof MutantResultSchema.Type; + +// @public (undocumented) +export const MutantResultSchema: S.Struct<{ + readonly id: S.String; + readonly mutatorName: S.String; + readonly status: S.Literals; + readonly location: S.Struct<{ + readonly start: S.Struct<{ + readonly line: S.Finite; + readonly column: S.Finite; + }>; + readonly end: S.Struct<{ + readonly line: S.Finite; + readonly column: S.Finite; + }>; + }>; + readonly replacement: S.optional; + readonly description: S.optional; + readonly statusReason: S.optional; + readonly static: S.optional; + readonly coveredBy: S.optional>; + readonly killedBy: S.optional>; + readonly testsCompleted: S.optional; + readonly duration: S.optional; +}>; + +// @public (undocumented) +export interface MutantRunOptions extends RunOptions { + // (undocumented) + readonly activeMutant: Mutant; + // (undocumented) + readonly hitLimit?: number; + // (undocumented) + readonly mutantActivation: MutantActivation; + // (undocumented) + readonly reloadEnvironment: boolean; + // (undocumented) + readonly sandboxFileName: string; + // (undocumented) + readonly testFilter?: readonly string[]; +} + +// @public (undocumented) +export const MutantRunOptionsSchema: S.Struct<{ + readonly timeout: S.Finite; + readonly disableBail: S.Boolean; + readonly activeMutant: typeof Mutant; + readonly sandboxFileName: S.String; + readonly mutantActivation: S.Literals; + readonly reloadEnvironment: S.Boolean; + readonly testFilter: S.optionalKey>; + readonly hitLimit: S.optionalKey; +}>; + +// @public (undocumented) +export type MutantRunPlan = RunPlan; + +// @public (undocumented) +export type MutantRunResult = ErrorMutantRunResult | KilledMutantRunResult | SurvivedMutantRunResult | TimeoutMutantRunResult; + +// @public (undocumented) +export const MutantRunResultSchema: S.Union; + readonly killedBy: S.$Array; + readonly failureMessage: S.String; + readonly nrOfTests: S.Finite; +}>, S.Struct<{ + readonly status: S.Literal<"survived">; + readonly nrOfTests: S.Finite; +}>, S.Struct<{ + readonly status: S.Literal<"timeout">; + readonly reason: S.optionalKey; +}>, S.Struct<{ + readonly status: S.Literal<"error">; + readonly errorMessage: S.String; +}>]>; + +// @public (undocumented) +export const MutantRunStatus: S.Literals; + +// @public (undocumented) +export type MutantRunStatus = typeof MutantRunStatus.Type; + +// @public (undocumented) +export type MutantStatus = typeof MutantStatusSchema.Type; + +// @public (undocumented) +export const MutantStatusSchema: S.Literals; + +// @public (undocumented) +export type MutantTestCoverage = Mutant & { + readonly coveredBy: ReadonlyArray | undefined; + readonly static: boolean | undefined; +}; + +// Warning: (ae-forgotten-export) The symbol "MutantTested_base" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export class MutantTested extends MutantTested_base {} + +// @public (undocumented) +export type MutateDescription = ReadonlyArray | boolean; + +// @public (undocumented) +export interface MutationRange { + // (undocumented) + readonly end: Position; + // (undocumented) + readonly start: Position; +} + +// @public (undocumented) +export interface MutationRunIo { + // (undocumented) + readonly read: (command: RunCommand) => Effect.Effect; + // (undocumented) + readonly write: (output: RunOutput) => Effect.Effect; +} + +// Warning: (ae-forgotten-export) The symbol "MutationRunPlan_base" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export class MutationRunPlan extends MutationRunPlan_base {} + +// @public (undocumented) +export type MutationScoreThresholds = typeof MutationScoreThresholdsSchema.Type; + +// @public (undocumented) +export const MutationScoreThresholdsSchema: S.Struct<{ + readonly high: S.withDecodingDefaultKey; + readonly low: S.withDecodingDefaultKey; + readonly break: S.withDecodingDefaultKey, never>; +}>; + +// Warning: (ae-forgotten-export) The symbol "MutationTestingPlanReady_base" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export class MutationTestingPlanReady extends MutationTestingPlanReady_base {} + +// Warning: (ae-forgotten-export) The symbol "MutationTestReportReady_base" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export class MutationTestReportReady extends MutationTestReportReady_base {} + +// @public (undocumented) +export type MutationTestResult = typeof MutationTestResultSchema.Type; + +// @public (undocumented) +export const MutationTestResultSchema: S.Struct<{ + readonly schemaVersion: S.String; + readonly files: S.$Record; + readonly location: S.Struct<{ + readonly start: S.Struct<{ + readonly line: S.Finite; + readonly column: S.Finite; + }>; + readonly end: S.Struct<{ + readonly line: S.Finite; + readonly column: S.Finite; + }>; + }>; + readonly replacement: S.optional; + readonly description: S.optional; + readonly statusReason: S.optional; + readonly static: S.optional; + readonly coveredBy: S.optional>; + readonly killedBy: S.optional>; + readonly testsCompleted: S.optional; + readonly duration: S.optional; + }>>; + }>>; + readonly thresholds: S.Struct<{ + readonly high: S.Finite; + readonly low: S.Finite; + }>; + readonly config: S.optional>; + readonly testFiles: S.optional; + readonly tests: S.$Array; + readonly end: S.optional>; + }>>; + }>>; + }>>>; + readonly projectRoot: S.optional; + readonly framework: S.optional; + readonly branding: S.optional; + }>>; + readonly dependencies: S.optional>; + }>>; +}>; + +// @public (undocumented) +export interface NodePath { + // (undocumented) + readonly ancestors: readonly unknown[]; + // (undocumented) + readonly node: unknown; +} + +// @public (undocumented) +export function normalizeFileName(fileName: string): string; + +// @public (undocumented) +export type OnlyObject = Exclude; + +// @public (undocumented) +export type OpenEndLocation = typeof OpenEndLocationSchema.Type; + +// @public (undocumented) +export const OpenEndLocationSchema: S.Struct<{ + readonly start: S.Struct<{ + readonly line: S.Finite; + readonly column: S.Finite; + }>; + readonly end: S.optional>; +}>; + +// @public (undocumented) +export const OutputMode: S.Literals; + +// @public (undocumented) +export type OutputMode = typeof OutputMode.Type; + +// @public (undocumented) +const PackageManager: S.Literals; + +// @public (undocumented) +type PackageManager = typeof PackageManager.Type; +export { PackageManager } +export { PackageManager as PackageManagerType } + +// @public +export type PartialStrykerOptions = DeepOptional; + +// @public (undocumented) +export interface PassedCheckResult { + // (undocumented) + readonly status: 'passed'; +} + +// Warning: (ae-forgotten-export) The symbol "PhaseEntered_base" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export class PhaseEntered extends PhaseEntered_base {} + +// Warning: (ae-forgotten-export) The symbol "PlanKnown_base" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export class PlanKnown extends PlanKnown_base {} + +// @public (undocumented) +export const planMutationRun: (command: PlanMutationRunCommand) => MutationRunPlan; + +// Warning: (ae-forgotten-export) The symbol "PlanMutationRunCommand_base" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export class PlanMutationRunCommand extends PlanMutationRunCommand_base {} + +// @public (undocumented) +export type Position = typeof PositionSchema.Type; + +// @public (undocumented) +export const PositionSchema: S.Struct<{ + readonly line: S.Finite; + readonly column: S.Finite; +}>; + +// @public (undocumented) +export type Primitive = boolean | number | string | null | undefined; + +// @public +export function propertyPath(): PropertyPathOverloads; + +// @public (undocumented) +export interface PropertyPathOverloads { + // (undocumented) + (key: KnownKeys): string; + // (undocumented) + >(key: TProp1, key2: KnownKeys>): string; + // (undocumented) + , TProp2 extends KnownKeys>>(key: TProp1, key2: TProp2, key3: KnownKeys[TProp2]>>): string; +} + +// @public +export const RENDERED_OPTION_DEFAULTS: { + readonly coverageAnalysis: 'perTest'; + readonly fileLogLevel: 'off'; + readonly logLevel: 'info'; + readonly tempDirName: '.stryker-tmp'; +}; + +// @public (undocumented) +export type ReporterEvent = DryRunCompleted | MutationTestingPlanReady | MutantTested | MutationTestReportReady; + +// @public (undocumented) +export const ReporterEventKind: S.Literals; + +// @public (undocumented) +export type ReporterEventKind = typeof ReporterEventKind.Type; + +// @public (undocumented) +export const ReporterEventSchema: StandardSchemaV1; + +// @public (undocumented) +export const ReporterEventUnion: S.Union; + +// @public (undocumented) +export type ReporterFactory = (options: StrykerOptions, init: ReporterInit) => (events: AsyncIterable) => Promise; + +// Warning: (ae-forgotten-export) The symbol "ReporterFailed_base" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export class ReporterFailed extends ReporterFailed_base {} + +// @public (undocumented) +export interface ReporterInit { + // (undocumented) + readonly traceparent?: string | undefined; + // (undocumented) + readonly tracestate?: string | undefined; +} + +// @public (undocumented) +export type ReporterPlanDescriptor = typeof ReporterPlanDescriptorSchema.Type; + +// @public (undocumented) +export const ReporterPlanDescriptorSchema: S.Struct<{ + readonly mutantId: S.String; + readonly plan: S.Literals; + readonly netTime: S.Finite; + readonly reloadEnvironment: S.Boolean; +}>; + +// @public (undocumented) +export const ReporterPlanKind: S.Literals; + +// @public (undocumented) +export const ReportTypeType: S.Literals; + +// @public (undocumented) +export type ReportTypeType = typeof ReportTypeType.Type; + +// @public (undocumented) +export function resolveExitCode(pending: Iterable, signal: number | null): number; + +// Warning: (ae-forgotten-export) The symbol "RunCommand_base" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export class RunCommand extends RunCommand_base {} + +// @public (undocumented) +const RunEvent: S.Union; + +// @public (undocumented) +type RunEvent = typeof RunEvent.Type; +export { RunEvent } +export { RunEvent as RunEventType } + +// Warning: (ae-forgotten-export) The symbol "RunEvents_base" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export class RunEvents extends RunEvents_base {} + +// Warning: (ae-forgotten-export) The symbol "RunFailed_base" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export class RunFailed extends RunFailed_base {} + +// Warning: (ae-forgotten-export) The symbol "RunIdentity_base" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export class RunIdentity extends RunIdentity_base {} + +// @public (undocumented) +export interface RunIdentityShape { + // (undocumented) + readonly basePath: string; + // (undocumented) + readonly runId: string; +} + +// @public (undocumented) +export type RunMutantResult = Mutant & { + readonly status: MutantStatus; + readonly statusReason?: string | undefined; + readonly testsCompleted?: number | undefined; + readonly killedBy?: readonly string[] | undefined; + readonly coveredBy?: readonly string[] | undefined; + readonly static?: boolean | undefined; +}; + +// Warning: (ae-forgotten-export) The symbol "RunMutantTested_base" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export class RunMutantTested extends RunMutantTested_base {} + +// @public (undocumented) +export const runMutationTest: (io: MutationRunIo, command: RunCommand) => Effect.Effect; + +// @public (undocumented) +export interface RunOptions { + // (undocumented) + readonly disableBail: boolean; + // (undocumented) + readonly timeout: number; +} + +// Warning: (ae-forgotten-export) The symbol "RunOutput_base" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export class RunOutput extends RunOutput_base {} + +// @public (undocumented) +export const RunPhase: S.Literals; + +// @public (undocumented) +export type RunPhase = typeof RunPhase.Type; + +// @public (undocumented) +export interface RunPlan { + // (undocumented) + readonly mutant: Mutant; + // (undocumented) + readonly netTime: number; + // (undocumented) + readonly plan: 'Run'; + // (undocumented) + readonly runOptions: MutantRunOptions; +} + +// Warning: (ae-forgotten-export) The symbol "RunStarted_base" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export class RunStarted extends RunStarted_base {} + +// @public (undocumented) +export type RunTerminalEvent = VerdictReached | RunFailed | HelpRendered; + +// @public (undocumented) +export type RunTiming = typeof RunTimingSchema.Type; + +// @public (undocumented) +export const RunTimingSchema: S.Struct<{ + readonly net: S.Finite; + readonly overhead: S.Finite; +}>; + +// @public (undocumented) +export const shouldKeepTempDir: (exit: Exit.Exit, cleanTempDir: 'always' | boolean) => boolean; + +// @public (undocumented) +export interface SkippedTestResult extends BaseTestResult { + // (undocumented) + readonly status: 'skipped'; +} + +// @public +export const strykerCoreSchema: Record; + +// @public +export type StrykerOptions = S.Schema.Type; + +// @public (undocumented) +export const StrykerOptionsSchema: S.StructWithRest; + readonly buildCommand: S.optional; + readonly checkers: S.withDecodingDefaultKey, never>; + readonly checkerNodeArgs: S.withDecodingDefaultKey, never>; + readonly concurrency: S.optional>; + readonly commandRunner: S.withDecodingDefaultKey; + }>, readonly [S.$Record]>, never>; + readonly coverageAnalysis: S.withDecodingDefaultKey, never>; + readonly clearTextReporter: S.withDecodingDefaultKey; + readonly allowEmojis: S.withDecodingDefaultKey; + readonly logTests: S.withDecodingDefaultKey; + readonly maxTestsToLog: S.withDecodingDefaultKey; + readonly reportTests: S.withDecodingDefaultKey; + readonly reportMutants: S.withDecodingDefaultKey; + readonly reportScoreTable: S.withDecodingDefaultKey; + readonly skipFull: S.withDecodingDefaultKey; + }>, readonly [S.$Record]>, never>; + readonly dryRunOnly: S.withDecodingDefaultKey; + readonly ignorePatterns: S.withDecodingDefaultKey, never>; + readonly ignoreStatic: S.withDecodingDefaultKey; + readonly incremental: S.withDecodingDefaultKey; + readonly incrementalFile: S.withDecodingDefaultKey; + readonly progressStreamFile: S.withDecodingDefaultKey; + readonly force: S.withDecodingDefaultKey; + readonly fileLogLevel: S.withDecodingDefaultKey, never>; + readonly inPlace: S.withDecodingDefaultKey; + readonly logLevel: S.withDecodingDefaultKey, never>; + readonly maxConcurrentTestRunners: S.withDecodingDefaultKey; + readonly maxTestRunnerReuse: S.withDecodingDefaultKey; + readonly mutate: S.withDecodingDefaultKey, never>; + readonly mutator: S.withDecodingDefaultKey, never>; + }>, never>; + readonly packageManager: S.optional>; + readonly plugins: S.withDecodingDefaultKey, never>; + readonly appendPlugins: S.withDecodingDefaultKey, never>; + readonly reporters: S.withDecodingDefaultKey, never>; + readonly htmlReporter: S.withDecodingDefaultKey; + }>, never>; + readonly jsonReporter: S.withDecodingDefaultKey; + }>, never>; + readonly disableTypeChecks: S.withDecodingDefaultKey, never>; + readonly symlinkNodeModules: S.withDecodingDefaultKey; + readonly tempDirName: S.withDecodingDefaultKey; + readonly cleanTempDir: S.withDecodingDefaultKey, never>; + readonly testRunner: S.withDecodingDefaultKey; + readonly testRunnerNodeArgs: S.withDecodingDefaultKey, never>; + readonly thresholds: S.withDecodingDefaultKey; + readonly low: S.withDecodingDefaultKey; + readonly break: S.withDecodingDefaultKey, never>; + }>, never>; + readonly timeoutFactor: S.withDecodingDefaultKey; + readonly timeoutMS: S.withDecodingDefaultKey; + readonly dryRunTimeoutMinutes: S.withDecodingDefaultKey; + readonly tsconfigFile: S.withDecodingDefaultKey; + readonly warnings: S.withDecodingDefaultKey; + readonly preprocessorErrors: S.withDecodingDefaultKey; + readonly unserializableOptions: S.withDecodingDefaultKey; + readonly slow: S.withDecodingDefaultKey; + }>, readonly [S.$Record]>]>, never>; + readonly disableBail: S.withDecodingDefaultKey; + readonly allowEmpty: S.withDecodingDefaultKey; + readonly ignorers: S.withDecodingDefaultKey, never>; + readonly testFiles: S.withDecodingDefaultKey, never>; +}>, readonly [S.$Record]>; + +// @public +export function strykerReportBugUrl(titleSuggestion: string): string; + +// @public (undocumented) +export interface SuccessTestResult extends BaseTestResult { + // (undocumented) + readonly status: 'success'; +} + +// @public (undocumented) +export interface SurvivedMutantRunResult { + // (undocumented) + readonly nrOfTests: number; + // (undocumented) + readonly status: 'survived'; +} + +// @public (undocumented) +export type TestDefinition = typeof TestDefinitionSchema.Type; + +// @public (undocumented) +export const TestDefinitionSchema: S.Struct<{ + readonly id: S.String; + readonly name: S.String; + readonly location: S.optional; + readonly end: S.optional>; + }>>; +}>; + +// @public (undocumented) +export type TestFile = typeof TestFileSchema.Type; + +// @public (undocumented) +export type TestFileDefinitionDictionary = typeof TestFileDefinitionDictionarySchema.Type; + +// @public (undocumented) +export const TestFileDefinitionDictionarySchema: S.$Record; + readonly tests: S.$Array; + readonly end: S.optional>; + }>>; + }>>; +}>>; + +// @public (undocumented) +export const TestFileSchema: S.Struct<{ + readonly source: S.optional; + readonly tests: S.$Array; + readonly end: S.optional>; + }>>; + }>>; +}>; + +// @public (undocumented) +export function testFilesProvided(options: { + readonly testFiles?: readonly string[]; +}): boolean; + +// @public (undocumented) +export type TestPlan = EarlyResultPlan | RunPlan; + +// @public (undocumented) +export type TestResult = FailedTestResult | SkippedTestResult | SuccessTestResult; + +// @public (undocumented) +export const TestResultSchema: S.Union; + readonly startPosition: S.optionalKey>; + readonly status: S.Literal<"failed">; + readonly failureMessage: S.String; +}>, S.Struct<{ + readonly id: S.String; + readonly name: S.String; + readonly timeSpentMs: S.Finite; + readonly fileName: S.optionalKey; + readonly startPosition: S.optionalKey>; + readonly status: S.Literal<"skipped">; +}>, S.Struct<{ + readonly id: S.String; + readonly name: S.String; + readonly timeSpentMs: S.Finite; + readonly fileName: S.optionalKey; + readonly startPosition: S.optionalKey>; + readonly status: S.Literal<"success">; +}>]>; + +// Warning: (ae-forgotten-export) The symbol "TestRunner_base" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export class TestRunner extends TestRunner_base {} + +// @public (undocumented) +export interface TestRunnerCapabilities { + // (undocumented) + readonly reloadEnvironment: boolean; +} + +// @public (undocumented) +export const TestRunnerCapabilitiesSchema: S.Struct<{ + readonly reloadEnvironment: S.Boolean; +}>; + +// Warning: (ae-forgotten-export) The symbol "TestRunnerFailed_base" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export class TestRunnerFailed extends TestRunnerFailed_base {} + +// @public (undocumented) +export interface TestRunnerService { + // (undocumented) + readonly capabilities: Effect.Effect; + // (undocumented) + readonly dispose: Effect.Effect; + // (undocumented) + readonly dryRun: (options: DryRunOptions) => Effect.Effect; + // (undocumented) + readonly init: Effect.Effect; + // (undocumented) + readonly mutantRun: (options: MutantRunOptions) => Effect.Effect; +} + +// @public (undocumented) +export const TestStatus: S.Literals; + +// @public (undocumented) +export type TestStatus = typeof TestStatus.Type; + +// @public (undocumented) +export type Thresholds = typeof ThresholdsSchema.Type; + +// @public (undocumented) +export const ThresholdsSchema: S.Struct<{ + readonly high: S.Finite; + readonly low: S.Finite; +}>; + +// @public (undocumented) +export interface TimeoutDryRunResult { + // (undocumented) + readonly reason?: string; + // (undocumented) + readonly status: 'timeout'; +} + +// @public (undocumented) +export interface TimeoutMutantRunResult { + // (undocumented) + readonly reason?: string; + // (undocumented) + readonly status: 'timeout'; +} + +// @public (undocumented) +export function toMutantRunResult(dryRunResult: DryRunResult, reportAllKillers: boolean): MutantRunResult; + +// @public (undocumented) +export function verdictExitClass(score: number | null, breakingThreshold: number | null): ExitClass | null; + +// Warning: (ae-forgotten-export) The symbol "VerdictReached_base" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export class VerdictReached extends VerdictReached_base {} + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/stryker-js-language/package.json b/packages/stryker-js-language/package.json index 5348fc3ba..02a5658c0 100644 --- a/packages/stryker-js-language/package.json +++ b/packages/stryker-js-language/package.json @@ -15,11 +15,13 @@ ], "scripts": { "clean": "rimraf dist", - "build": "tsdown", + "build": "tsdown && pnpm api:check", "typecheck": "tsc --noEmit --incremental && tsc -p tsconfig.node.json --noEmit", "test": "vitest run", "attw": "attw --pack .", - "lint": "f=${OXLINT_FORMAT:-${AGENT:+agent}}; oxlint . --format=${f:-default}" + "lint": "f=${OXLINT_FORMAT:-${AGENT:+agent}}; oxlint . --format=${f:-default}", + "api:check": "tsdown && api-extractor run", + "api:update": "api-extractor run --local" }, "license": "Apache-2.0", "engines": { @@ -31,10 +33,12 @@ }, "devDependencies": { "@effect/vitest": "catalog:", + "@microsoft/api-extractor": "catalog:", "@systemfsoftware/all": "catalog:", "@systemfsoftware/arethetypeswrong-cli": "catalog:", "@systemfsoftware/effect-schema-law": "catalog:", "@systemfsoftware/effect-schema-vite": "catalog:", + "@systemfsoftware/stryker-ignorer-interface": "workspace:^", "@systemfsoftware/tsconfig": "catalog:", "@systemfsoftware/vitest-config": "workspace:^", "@types/node": "catalog:", diff --git a/packages/stryker-js-language/src/Ignorer.ts b/packages/stryker-js-language/src/Ignorer.ts index 892c7a9d6..5fd1cd9a1 100644 --- a/packages/stryker-js-language/src/Ignorer.ts +++ b/packages/stryker-js-language/src/Ignorer.ts @@ -1,15 +1,8 @@ +import type { NodePath } from '@systemfsoftware/stryker-ignorer-interface' import * as Context from 'effect/Context' import type * as Option from 'effect/Option' -export interface NodePath { - readonly node: unknown - readonly parentPath?: NodePath | null - isObjectExpression(): boolean - isCallExpression(): boolean - isClassProperty(): boolean - isClassPrivateProperty(): boolean - isClassAccessorProperty(): boolean -} +export type { NodePath } export interface IgnorerService { readonly shouldIgnore: (path: NodePath) => Option.Option diff --git a/packages/stryker-js-plugin-interface/api-extractor.json b/packages/stryker-js-plugin-interface/api-extractor.json new file mode 100644 index 000000000..42ba743a2 --- /dev/null +++ b/packages/stryker-js-plugin-interface/api-extractor.json @@ -0,0 +1,55 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "projectFolder": ".", + "mainEntryPointFilePath": "/dist/index.d.mts", + "bundledPackages": [], + "compiler": { + "tsconfigFilePath": "/tsconfig.json" + }, + "apiReport": { + "enabled": true, + "reportFolder": "/etc/", + "reportTempFolder": "/temp/main/", + "reportFileName": "stryker-js-plugin-interface.api.md" + }, + "docModel": { + "enabled": false + }, + "dtsRollup": { + "enabled": true, + "untrimmedFilePath": "/dist/index.d.mts" + }, + "tsdocMetadata": { + "enabled": false + }, + "messages": { + "extractorMessageReporting": { + "default": { + "logLevel": "warning" + }, + "ae-forgotten-export": { + "logLevel": "error" + }, + "ae-missing-release-tag": { + "logLevel": "none" + }, + "ae-internal-missing-underscore": { + "logLevel": "none" + } + }, + "tsdocMessageReporting": { + "default": { + "logLevel": "warning" + }, + "tsdoc-undefined-tag": { + "logLevel": "none" + }, + "tsdoc-unsupported-tag": { + "logLevel": "none" + }, + "tsdoc-param-tag-missing-hyphen": { + "logLevel": "none" + } + } + } +} diff --git a/packages/stryker-js-plugin-interface/etc/stryker-js-plugin-interface.api.md b/packages/stryker-js-plugin-interface/etc/stryker-js-plugin-interface.api.md new file mode 100644 index 000000000..10757a1e1 --- /dev/null +++ b/packages/stryker-js-plugin-interface/etc/stryker-js-plugin-interface.api.md @@ -0,0 +1,129 @@ +## API Report File for "@systemfsoftware/stryker-js-plugin-interface" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { Checker } from '@systemfsoftware/stryker-js-language'; +import * as Context from 'effect/Context'; +import { Evaluator } from '@systemfsoftware/stryker-js-language'; +import * as FileSystem from 'effect/FileSystem'; +import { Ignorer } from '@systemfsoftware/stryker-js-language'; +import * as Layer from 'effect/Layer'; +import { Module } from '@systemfsoftware/stryker-js-language'; +import * as Option from 'effect/Option'; +import * as Path from 'effect/Path'; +import { ReporterFactory } from '@systemfsoftware/stryker-js-language'; +import * as S from 'effect/Schema'; +import { TestRunner } from '@systemfsoftware/stryker-js-language'; + +// @public (undocumented) +export type AnyPluginContribution = { [K in PluginKind]: PluginContribution; }[PluginKind]; + +// @public (undocumented) +export interface ComposedPlugins { + // (undocumented) + readonly layer: Option.Option>; + // (undocumented) + readonly reporterFactories: readonly SelectedReporterFactory[]; + // (undocumented) + readonly shadowings: readonly Shadowing[]; +} + +// @public (undocumented) +export function composePlugins(contributions: readonly AnyPluginContribution[]): ComposedPlugins; + +// @public (undocumented) +export type ContributionOf = Extract; + +// @public (undocumented) +export function declarePlugin(kind: 'Reporter', name: string, make: ReporterFactory): PluginContribution<'Reporter'>; + +// @public (undocumented) +export function declarePlugin(kind: K, name: string, layer: Layer.Layer): PluginContribution; + +// @public (undocumented) +export type MergedPluginServices = Checker & Ignorer & TestRunner; + +// @public (undocumented) +export type PluginContribution = K extends 'Reporter' ? PluginReporterContribution : PluginLayerContribution>; + +// @public (undocumented) +export type PluginEnvironment = RunConfiguration | SandboxDirectory | FileSystem.FileSystem | Module | Path.Path; + +// @public (undocumented) +export interface PluginInterfaces { + // (undocumented) + Checker: Checker; + // (undocumented) + Evaluator: Evaluator; + // (undocumented) + Ignore: Ignorer; + // (undocumented) + TestRunner: TestRunner; +} + +// @public (undocumented) +export const PluginKind: S.Literals; + +// @public (undocumented) +export type PluginKind = typeof PluginKind.Type; + +// Warning: (ae-forgotten-export) The symbol "PluginLayerContribution_base" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export class PluginLayerContribution extends PluginLayerContribution_base { + // (undocumented) + readonly kind: K; + // (undocumented) + readonly layer: Layer.Layer; + // (undocumented) + readonly name: string; +} + +// @public (undocumented) +export const PluginLayerKind: S.Literals; + +// @public (undocumented) +export type PluginLayerKind = typeof PluginLayerKind.Type; + +// Warning: (ae-forgotten-export) The symbol "PluginReporterContribution_base" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export class PluginReporterContribution extends PluginReporterContribution_base { + // (undocumented) + readonly kind: 'Reporter'; + // (undocumented) + readonly make: ReporterFactory; + // (undocumented) + readonly name: string; +} + +// Warning: (ae-forgotten-export) The symbol "RunConfiguration_base" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export class RunConfiguration extends RunConfiguration_base {} + +// Warning: (ae-forgotten-export) The symbol "SandboxDirectory_base" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export class SandboxDirectory extends SandboxDirectory_base {} + +// @public (undocumented) +export interface SelectedReporterFactory { + // (undocumented) + readonly make: ReporterFactory; + // (undocumented) + readonly name: string; +} + +// Warning: (ae-forgotten-export) The symbol "Shadowing_base" needs to be exported by the entry point index.d.mts +// +// @public (undocumented) +export class Shadowing extends Shadowing_base {} + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/stryker-js-plugin-interface/package.json b/packages/stryker-js-plugin-interface/package.json index 506bda9aa..e2c3a2bd6 100644 --- a/packages/stryker-js-plugin-interface/package.json +++ b/packages/stryker-js-plugin-interface/package.json @@ -15,11 +15,13 @@ ], "scripts": { "clean": "rimraf dist", - "build": "tsdown", + "build": "tsdown && pnpm api:check", "typecheck": "tsc --noEmit --incremental && tsc -p tsconfig.node.json --noEmit", "test": "vitest run", "attw": "attw --pack .", - "lint": "f=${OXLINT_FORMAT:-${AGENT:+agent}}; oxlint . --format=${f:-default}" + "lint": "f=${OXLINT_FORMAT:-${AGENT:+agent}}; oxlint . --format=${f:-default}", + "api:check": "tsdown && api-extractor run", + "api:update": "api-extractor run --local" }, "license": "Apache-2.0", "engines": { @@ -30,6 +32,7 @@ "effect": "catalog:" }, "devDependencies": { + "@microsoft/api-extractor": "catalog:", "@systemfsoftware/all": "catalog:", "@systemfsoftware/arethetypeswrong-cli": "catalog:", "@systemfsoftware/effect-schema-law": "catalog:", diff --git a/packages/stryker-js-typescript-checker/api-extractor.json b/packages/stryker-js-typescript-checker/api-extractor.json new file mode 100644 index 000000000..8bf0723ef --- /dev/null +++ b/packages/stryker-js-typescript-checker/api-extractor.json @@ -0,0 +1,55 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "projectFolder": ".", + "mainEntryPointFilePath": "/dist/index.d.mts", + "bundledPackages": [], + "compiler": { + "tsconfigFilePath": "/tsconfig.json" + }, + "apiReport": { + "enabled": true, + "reportFolder": "/etc/", + "reportTempFolder": "/temp/main/", + "reportFileName": "stryker-js-typescript-checker.api.md" + }, + "docModel": { + "enabled": false + }, + "dtsRollup": { + "enabled": true, + "untrimmedFilePath": "/dist/index.d.mts" + }, + "tsdocMetadata": { + "enabled": false + }, + "messages": { + "extractorMessageReporting": { + "default": { + "logLevel": "warning" + }, + "ae-forgotten-export": { + "logLevel": "error" + }, + "ae-missing-release-tag": { + "logLevel": "none" + }, + "ae-internal-missing-underscore": { + "logLevel": "none" + } + }, + "tsdocMessageReporting": { + "default": { + "logLevel": "warning" + }, + "tsdoc-undefined-tag": { + "logLevel": "none" + }, + "tsdoc-unsupported-tag": { + "logLevel": "none" + }, + "tsdoc-param-tag-missing-hyphen": { + "logLevel": "none" + } + } + } +} diff --git a/packages/stryker-js-typescript-checker/etc/stryker-js-typescript-checker.api.md b/packages/stryker-js-typescript-checker/etc/stryker-js-typescript-checker.api.md new file mode 100644 index 000000000..5b674789d --- /dev/null +++ b/packages/stryker-js-typescript-checker/etc/stryker-js-typescript-checker.api.md @@ -0,0 +1,17 @@ +## API Report File for "@systemfsoftware/stryker-js-typescript-checker" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { PluginLayerContribution } from '@systemfsoftware/stryker-js-plugin-interface'; + +// @public (undocumented) +export const strykerPlugins: PluginLayerContribution<"Checker">[]; + +// @public (undocumented) +export const strykerValidationSchema: Record; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/stryker-js-typescript-checker/package.json b/packages/stryker-js-typescript-checker/package.json index 97b24a97f..8de131813 100644 --- a/packages/stryker-js-typescript-checker/package.json +++ b/packages/stryker-js-typescript-checker/package.json @@ -20,11 +20,13 @@ }, "scripts": { "clean": "rimraf dist", - "build": "tsdown", + "build": "tsdown && pnpm api:check", "typecheck": "tsc --noEmit --incremental && tsc -p tsconfig.node.json --noEmit", "attw": "attw --pack .", "test": "vitest run", - "lint": "f=${OXLINT_FORMAT:-${AGENT:+agent}}; oxlint . --format=${f:-default}" + "lint": "f=${OXLINT_FORMAT:-${AGENT:+agent}}; oxlint . --format=${f:-default}", + "api:check": "tsdown && api-extractor run", + "api:update": "api-extractor run --local" }, "license": "Apache-2.0", "dependencies": { @@ -37,6 +39,7 @@ "devDependencies": { "@effect/platform-node": "catalog:", "@effect/platform-node-shared": "catalog:", + "@microsoft/api-extractor": "catalog:", "@std/jsonc": "catalog:", "@systemfsoftware/all": "catalog:", "@systemfsoftware/arethetypeswrong-cli": "catalog:", diff --git a/packages/stryker-js-vitest-runner/api-extractor.json b/packages/stryker-js-vitest-runner/api-extractor.json new file mode 100644 index 000000000..ade491742 --- /dev/null +++ b/packages/stryker-js-vitest-runner/api-extractor.json @@ -0,0 +1,55 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "projectFolder": ".", + "mainEntryPointFilePath": "/dist/index.d.mts", + "bundledPackages": [], + "compiler": { + "tsconfigFilePath": "/tsconfig.json" + }, + "apiReport": { + "enabled": true, + "reportFolder": "/etc/", + "reportTempFolder": "/temp/main/", + "reportFileName": "stryker-js-vitest-runner.api.md" + }, + "docModel": { + "enabled": false + }, + "dtsRollup": { + "enabled": true, + "untrimmedFilePath": "/dist/index.d.mts" + }, + "tsdocMetadata": { + "enabled": false + }, + "messages": { + "extractorMessageReporting": { + "default": { + "logLevel": "warning" + }, + "ae-forgotten-export": { + "logLevel": "error" + }, + "ae-missing-release-tag": { + "logLevel": "none" + }, + "ae-internal-missing-underscore": { + "logLevel": "none" + } + }, + "tsdocMessageReporting": { + "default": { + "logLevel": "warning" + }, + "tsdoc-undefined-tag": { + "logLevel": "none" + }, + "tsdoc-unsupported-tag": { + "logLevel": "none" + }, + "tsdoc-param-tag-missing-hyphen": { + "logLevel": "none" + } + } + } +} diff --git a/packages/stryker-js-vitest-runner/api-extractor.stryker-setup.json b/packages/stryker-js-vitest-runner/api-extractor.stryker-setup.json new file mode 100644 index 000000000..746875357 --- /dev/null +++ b/packages/stryker-js-vitest-runner/api-extractor.stryker-setup.json @@ -0,0 +1,55 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "projectFolder": ".", + "mainEntryPointFilePath": "/dist/stryker-setup.d.mts", + "bundledPackages": [], + "compiler": { + "tsconfigFilePath": "/tsconfig.api-stryker-setup.json" + }, + "apiReport": { + "enabled": true, + "reportFolder": "/etc/", + "reportTempFolder": "/temp/stryker-setup/", + "reportFileName": "stryker-setup.api.md" + }, + "docModel": { + "enabled": false + }, + "dtsRollup": { + "enabled": true, + "untrimmedFilePath": "/dist/stryker-setup.d.mts" + }, + "tsdocMetadata": { + "enabled": false + }, + "messages": { + "extractorMessageReporting": { + "default": { + "logLevel": "warning" + }, + "ae-forgotten-export": { + "logLevel": "error" + }, + "ae-missing-release-tag": { + "logLevel": "none" + }, + "ae-internal-missing-underscore": { + "logLevel": "none" + } + }, + "tsdocMessageReporting": { + "default": { + "logLevel": "warning" + }, + "tsdoc-undefined-tag": { + "logLevel": "none" + }, + "tsdoc-unsupported-tag": { + "logLevel": "none" + }, + "tsdoc-param-tag-missing-hyphen": { + "logLevel": "none" + } + } + } +} diff --git a/packages/stryker-js-vitest-runner/etc/stryker-js-vitest-runner.api.md b/packages/stryker-js-vitest-runner/etc/stryker-js-vitest-runner.api.md new file mode 100644 index 000000000..9ee5ff1fc --- /dev/null +++ b/packages/stryker-js-vitest-runner/etc/stryker-js-vitest-runner.api.md @@ -0,0 +1,17 @@ +## API Report File for "@systemfsoftware/stryker-js-vitest-runner" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { PluginLayerContribution } from '@systemfsoftware/stryker-js-plugin-interface'; + +// @public +export const strykerPlugins: PluginLayerContribution<"TestRunner">[]; + +// @public +export const strykerValidationSchema: Record; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/stryker-js-vitest-runner/etc/stryker-setup.api.md b/packages/stryker-js-vitest-runner/etc/stryker-setup.api.md new file mode 100644 index 000000000..490378c13 --- /dev/null +++ b/packages/stryker-js-vitest-runner/etc/stryker-setup.api.md @@ -0,0 +1,9 @@ +## API Report File for "@systemfsoftware/stryker-js-vitest-runner" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/stryker-js-vitest-runner/package.json b/packages/stryker-js-vitest-runner/package.json index 2d0a96dfd..5b5a3c4f8 100644 --- a/packages/stryker-js-vitest-runner/package.json +++ b/packages/stryker-js-vitest-runner/package.json @@ -20,11 +20,13 @@ }, "scripts": { "clean": "rimraf dist", - "build": "tsdown", + "build": "tsdown && pnpm api:check", "typecheck": "tsc --noEmit --incremental && tsc -p tsconfig.node.json --noEmit", "attw": "attw --pack .", "test": "vitest run", - "lint": "f=${OXLINT_FORMAT:-${AGENT:+agent}}; oxlint . --format=${f:-default}" + "lint": "f=${OXLINT_FORMAT:-${AGENT:+agent}}; oxlint . --format=${f:-default}", + "api:check": "tsdown && concurrently --kill-others-on-fail \"api-extractor run\" \"api-extractor run --config api-extractor.stryker-setup.json\"", + "api:update": "concurrently --kill-others-on-fail \"api-extractor run --local\" \"api-extractor run --local --config api-extractor.stryker-setup.json\"" }, "license": "Apache-2.0", "engines": { @@ -50,6 +52,7 @@ "devDependencies": { "@effect/platform-node": "catalog:", "@effect/vitest": "catalog:", + "@microsoft/api-extractor": "catalog:", "@systemfsoftware/all": "catalog:", "@systemfsoftware/arethetypeswrong-cli": "catalog:", "@systemfsoftware/effect-gherkin-spec": "catalog:", @@ -57,6 +60,7 @@ "@systemfsoftware/vitest-config": "workspace:^", "@types/node": "catalog:", "@vitest/browser-playwright": "catalog:", + "concurrently": "catalog:", "effect": "catalog:", "oxlint": "catalog:", "rimraf": "catalog:", diff --git a/packages/stryker-js-vitest-runner/tsconfig.api-stryker-setup.json b/packages/stryker-js-vitest-runner/tsconfig.api-stryker-setup.json new file mode 100644 index 000000000..4cb77d618 --- /dev/null +++ b/packages/stryker-js-vitest-runner/tsconfig.api-stryker-setup.json @@ -0,0 +1,6 @@ +{ + "extends": "@systemfsoftware/tsconfig/tsc/no-dom/library", + "include": [ + "dist/stryker-setup.d.mts" + ] +} diff --git a/packages/stryker-plugins/AGENTS.md b/packages/stryker-plugins/AGENTS.md deleted file mode 100644 index d5faa8012..000000000 --- a/packages/stryker-plugins/AGENTS.md +++ /dev/null @@ -1,19 +0,0 @@ -# AGENTS.md — `@systemfsoftware/stryker-plugins` - -Stryker plugins for Effect-TS: ignores proven-equivalent mutants on Schema declarations (brands, `TaggedClass`/`TaggedError` tags). Root `AGENTS.md` governs. - -## Rules - -| ID | Rule | Gate | -| ------- | ------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | -| **SP1** | An ignored mutant is proven-equivalent: mutating the tag/brand field produces identical behavior. | `pnpm --filter @systemfsoftware/stryker-plugins test` | -| **SP2** | Every new ignore pattern arrives with a test demonstrating the equivalent mutant. | `pnpm --filter @systemfsoftware/stryker-plugins test` | -| **SP3** | Hook into Stryker's `resolveMutant` pipeline only; never bypass other mutation stages. | `review` | - -## Verification - -```bash -pnpm --filter @systemfsoftware/stryker-plugins typecheck -pnpm --filter @systemfsoftware/stryker-plugins test -pnpm --filter @systemfsoftware/stryker-plugins lint -``` diff --git a/packages/stryker-plugins/oxlint.config.ts b/packages/stryker-plugins/oxlint.config.ts deleted file mode 100644 index 11b5fc558..000000000 --- a/packages/stryker-plugins/oxlint.config.ts +++ /dev/null @@ -1,32 +0,0 @@ -import all from '@systemfsoftware/all' -import { defineConfig } from 'oxlint' - -export default defineConfig({ - extends: [all], - - rules: { - 'typescript/no-unnecessary-condition': 'error', - 'typescript/strict-boolean-expressions': 'error', - 'typescript/no-non-null-assertion': 'error', - 'no-ternary': 'off', - 'typescript/consistent-type-assertions': 'off', - }, - - overrides: [ - { - files: ['**/*.test.ts', '**/*.spec.ts'], - rules: { 'typescript/no-unsafe-type-assertion': 'off' }, - }, - { - files: ['**/fixtures/**', '**/__fixtures__/**', '**/testResources/**'], - rules: { - 'typescript/no-unsafe-argument': 'off', - 'typescript/no-unsafe-assignment': 'off', - 'typescript/no-unsafe-call': 'off', - 'typescript/no-unsafe-member-access': 'off', - 'typescript/no-unsafe-return': 'off', - 'typescript/no-unsafe-type-assertion': 'off', - }, - }, - ], -}) diff --git a/packages/stryker-plugins/package.json b/packages/stryker-plugins/package.json deleted file mode 100644 index 6160b8935..000000000 --- a/packages/stryker-plugins/package.json +++ /dev/null @@ -1,105 +0,0 @@ -{ - "name": "@systemfsoftware/stryker-plugins", - "license": "Apache-2.0", - "version": "3.1.0", - "author": "Ryan Lee ", - "repository": { - "type": "git", - "url": "git+https://github.com/systemfsoftware/stryker-js-effect.git", - "directory": "packages/stryker-plugins" - }, - "homepage": "https://github.com/systemfsoftware/stryker-js-effect/tree/main/packages/stryker-plugins#readme", - "bugs": "https://github.com/systemfsoftware/stryker-js-effect/issues", - "description": "Stryker mutation-testing plugins for Effect-TS — ignore equivalent mutants on Effect Schema declarations (brands, TaggedClass/TaggedError tags) so the score reflects behaviour, not data.", - "keywords": [ - "stryker", - "stryker-mutator", - "mutation-testing", - "effect", - "effect-ts", - "schema", - "ignorer", - "equivalent-mutants", - "test-quality", - "typescript" - ], - "type": "module", - "files": [ - "dist" - ], - "exports": { - ".": { - "types": "./dist/index.d.ts", - "default": "./dist/index.mjs" - }, - "./effect-schema-ignorer": { - "types": "./dist/effect-schema-ignorer.d.ts", - "default": "./dist/effect-schema-ignorer.mjs" - }, - "./in-source-test-ignorer": { - "types": "./dist/in-source-test-ignorer.d.ts", - "default": "./dist/in-source-test-ignorer.mjs" - }, - "./workflow-make-ignorer": { - "types": "./dist/workflow-make-ignorer.d.ts", - "default": "./dist/workflow-make-ignorer.mjs" - }, - "./package.json": "./package.json" - }, - "scripts": { - "build": "tsdown", - "clean": "rimraf dist", - "typecheck": "tsc --noEmit --incremental && tsc -p tsconfig.node.json --noEmit", - "test": "vitest run", - "lint": "f=${OXLINT_FORMAT:-${AGENT:+agent}}; oxlint . --format=${f:-default}", - "attw": "attw --pack ." - }, - "devDependencies": { - "@effect/vitest": "catalog:", - "@systemfsoftware/all": "catalog:", - "@systemfsoftware/arethetypeswrong-cli": "catalog:", - "@systemfsoftware/effect-gherkin-spec": "catalog:", - "@systemfsoftware/effect-schema-law": "catalog:", - "@systemfsoftware/effect-schema-vite": "catalog:", - "@systemfsoftware/tsconfig": "catalog:", - "@systemfsoftware/stryker-js-language": "workspace:^", - "@systemfsoftware/stryker-js-plugin-interface": "workspace:^", - "@systemfsoftware/vitest-config": "workspace:^", - "@types/estree": "1.0.8", - "@types/node": "catalog:", - "@vitest/coverage-v8": "catalog:", - "effect": "catalog:", - "oxlint": "catalog:", - "rimraf": "catalog:", - "tsdown": "catalog:", - "vitest": "catalog:" - }, - "peerDependencies": { - "@systemfsoftware/stryker-js-language": "workspace:^", - "@systemfsoftware/stryker-js-plugin-interface": "workspace:^", - "effect": "catalog:", - "typescript": ">=5.0.0" - }, - "publishConfig": { - "exports": { - ".": { - "types": "./dist/index.d.ts", - "default": "./dist/index.mjs" - }, - "./effect-schema-ignorer": { - "types": "./dist/effect-schema-ignorer.d.ts", - "default": "./dist/effect-schema-ignorer.mjs" - }, - "./in-source-test-ignorer": { - "types": "./dist/in-source-test-ignorer.d.ts", - "default": "./dist/in-source-test-ignorer.mjs" - }, - "./workflow-make-ignorer": { - "types": "./dist/workflow-make-ignorer.d.ts", - "default": "./dist/workflow-make-ignorer.mjs" - }, - "./package.json": "./package.json" - }, - "provenance": true - } -} diff --git a/packages/stryker-plugins/src/AncestorPath.ts b/packages/stryker-plugins/src/AncestorPath.ts deleted file mode 100644 index 9d5aabbdc..000000000 --- a/packages/stryker-plugins/src/AncestorPath.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * The path shape stryker hands an Ignore plugin: the mutant node plus its - * ancestor chain. Both Ignorer plugins walk the same linked list of parent - * paths, so the interface and the walk live here once. - */ -export interface IgnorerPath { - readonly node: unknown - readonly parentPath?: IgnorerPath | null -} - -/** The ancestor nodes of a path, nearest first, ending at the file root. - * @yields each ancestor node, nearest first - */ -export function* ancestorsOf(path: IgnorerPath): Generator { - for (let current = path.parentPath; current; current = current.parentPath) { - yield current.node - } -} diff --git a/packages/stryker-plugins/src/effect-schema-ignorer/AstNode.schema.ts b/packages/stryker-plugins/src/effect-schema-ignorer/AstNode.schema.ts deleted file mode 100644 index 23c98ab3c..000000000 --- a/packages/stryker-plugins/src/effect-schema-ignorer/AstNode.schema.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { Schema as S } from 'effect' - -export const Identifier = S.Struct({ - type: S.Literal('Identifier'), - name: S.String, -}) -export type Identifier = S.Schema.Type - -// ESTree renders string literals as Literal{value:string}; the semantic name records what this matches. -export const StringLiteral = S.Struct({ - type: S.Literal('Literal'), - value: S.String, -}) -export type StringLiteral = S.Schema.Type - -export const ObjectExpression = S.Struct({ - type: S.Literal('ObjectExpression'), -}) -export type ObjectExpression = S.Schema.Type - -export const ArrowFunctionExpression = S.Struct({ - type: S.Literal('ArrowFunctionExpression'), -}) -export type ArrowFunctionExpression = S.Schema.Type - -export const UnknownNode = S.Struct({ type: S.String }) -export type UnknownNode = S.Schema.Type - -export interface MemberExpression { - readonly type: 'MemberExpression' - readonly object: AstNode - readonly property: AstNode -} - -export interface CallExpression { - readonly type: 'CallExpression' - readonly callee: AstNode - readonly arguments: readonly AstNode[] -} - -export type AstNode = - | Identifier - | StringLiteral - | ObjectExpression - | ArrowFunctionExpression - | MemberExpression - | CallExpression - | UnknownNode - -export const AstNode: S.Schema = S.suspend( - (): S.Schema => - S.Union([ - Identifier, - StringLiteral, - ObjectExpression, - ArrowFunctionExpression, - MemberExpression, - CallExpression, - UnknownNode, - ]), -).annotate({ - identifier: 'systemfsoftware.stryker-plugins.effect-schema-ignorer.AstNode', - recursionBudget: { maxDepth: 6, depthSize: 'small' }, -}) - -export const MemberExpression: S.Schema = S.Struct({ - type: S.Literal('MemberExpression'), - object: AstNode, - property: AstNode, -}) - -export const CallExpression: S.Schema = S.Struct({ - type: S.Literal('CallExpression'), - callee: AstNode, - arguments: S.Array(AstNode), -}) - -export const DocumentationKey = S.Literals(['identifier', 'description', 'title', 'documentation', 'examples']) - -export const DocumentationProperty = S.Struct({ - type: S.Literal('Property'), - computed: S.Literal(false), - key: S.Union([ - S.Struct({ type: S.Literal('Identifier'), name: DocumentationKey }), - S.Struct({ type: S.Literal('Literal'), value: DocumentationKey }), - ]), - value: S.Unknown, -}) - -export const DocumentationObject = S.Struct({ - type: S.Literal('ObjectExpression'), - properties: S.NonEmptyArray(DocumentationProperty), -}) - -/** Derived recognisers, declared beside the shapes they decide. */ -export const isIdentifier = S.is(Identifier) -export const isStringLiteral = S.is(StringLiteral) -export const isObjectExpression = S.is(ObjectExpression) -export const isArrowFunctionExpression = S.is(ArrowFunctionExpression) -export const isMemberExpression = S.is(MemberExpression) -export const isCallExpression = S.is(CallExpression) -export const isDocumentationProperty = S.is(DocumentationProperty) -export const isDocumentationObject = S.is(DocumentationObject) diff --git a/packages/stryker-plugins/src/effect-schema-ignorer/SchemaDeclarationIgnore.ts b/packages/stryker-plugins/src/effect-schema-ignorer/SchemaDeclarationIgnore.ts deleted file mode 100644 index 3c1ce9c9c..000000000 --- a/packages/stryker-plugins/src/effect-schema-ignorer/SchemaDeclarationIgnore.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { - type CallExpression, - isArrowFunctionExpression, - isCallExpression, - isDocumentationObject, - isDocumentationProperty, - isIdentifier, - isMemberExpression, - isObjectExpression, - isStringLiteral, - type MemberExpression, -} from './AstNode.schema.js' - -export const SYMBOL_DESCRIPTION_IGNORED = 'Symbol.for() brand description is identity-only data, not behaviour' as const -export const TAGGED_TAG_IGNORED = 'TaggedClass/TaggedError _tag is a declaration discriminant, not behaviour' as const -export const TAGGED_FIELDS_IGNORED = 'TaggedClass/TaggedError field schema is a declaration, not behaviour' as const -export const CLASS_ID_IGNORED = 'Schema.Class identifier is a declaration name, not behaviour' as const -export const CLASS_FIELDS_IGNORED = 'Schema.Class field schema is a declaration, not behaviour' as const -export const BRAND_NAME_IGNORED = 'Schema.brand name is identity-only data, not behaviour' as const -export const OPTIONAL_DEFAULT_IGNORED = 'optionalWith default value is config, not behaviour' as const -export const ANNOTATION_OBJECT_IGNORED = - 'annotations object holding only documentation is a declaration, not behaviour' as const -export const ANNOTATION_TEXT_IGNORED = 'annotation documentation value is declaration data, not behaviour' as const - -/** - * The Effect `Schema` annotations that describe a schema without changing what - * it does. `arbitrary`, `pretty`, `equivalence`, `message`, `jsonSchema` and - * `parseIssueTitle` are absent by design: each one alters observable behaviour, - * so a surviving mutant of one is a test gap to close, never an equivalent - * mutant to ignore. - */ - -/** - * An object literal whose every entry documents. One behaviour-bearing entry - - * an `arbitrary` beside a `title` - fails the schema, so the object keeps its - * mutants: emptying it would delete a generator, which a test can observe. - */ - -const TAGGED_FACTORIES: readonly string[] = ['TaggedClass', 'TaggedError'] - -/** - * `Schema.Class` is curried the other way round from `Schema.TaggedClass`. - * - * `S.TaggedClass()('tag', fields)` puts both the discriminant and the fields on the outer - * call, so one callee predicate reaches both. `S.Class('Id')(fields)` puts the identifier on - * the *inner* call and the fields on the outer one, so the same declaration data needs two - * predicates. Missing that shape is why a class-shaped schema kept fourteen mutants a - * tag-shaped one never had. - */ -const CLASS_FACTORY = 'Class' - -const isIdentifierNamed = (node: unknown, name: string): boolean => isIdentifier(node) && node.name === name - -const isIdentifierIn = (node: unknown, names: readonly string[]): boolean => - isIdentifier(node) && names.includes(node.name) - -const isMemberNamed = (member: MemberExpression, object: string, property: string): boolean => - isIdentifierNamed(member.object, object) && isIdentifierNamed(member.property, property) - -const isNamedMember = (node: unknown, object: string, property: string): boolean => - isMemberExpression(node) && isMemberNamed(node, object, property) - -const isSymbolForCallee = (callee: unknown): boolean => isNamedMember(callee, 'Symbol', 'for') - -const isNamedFactoryReference = (reference: unknown, names: readonly string[]): boolean => - isMemberExpression(reference) && isIdentifierIn(reference.property, names) - -const isTaggedFactoryReference = (reference: unknown): boolean => isNamedFactoryReference(reference, TAGGED_FACTORIES) - -/** The inner `S.Class('Id')` call, which carries the identifier. */ -const isClassFactoryReference = (reference: unknown): boolean => isNamedFactoryReference(reference, [CLASS_FACTORY]) - -/** - * There is deliberately no rule for a `Schema.Class` *fields* object, and the reason is measured. - * - * Stryker's ignorer suppresses a node's whole subtree, so ignoring the fields object also ignores - * every literal inside it - and a field's accepted value set is behaviour: emptying - * `Schema.Literal('permanent', 'transient')` changes what decodes. Adding that rule turned - * fourteen survivors green in one run while hiding the accepted-set decisions the survivors were - * pointing at. Those belong to a decode test, not to an ignorer. - */ - -/** `S.brand('Name')` - the brand name is identity data, like a `Symbol.for` description. */ -const isBrandCallee = (callee: unknown): boolean => - isMemberExpression(callee) && isIdentifierNamed(callee.property, 'brand') - -const isTaggedFactoryCallee = (callee: unknown): boolean => - isCallExpression(callee) && isTaggedFactoryReference(callee.callee) - -const isArgumentAt = ( - node: unknown, - call: CallExpression, - index: number, - calleeMatches: (callee: unknown) => boolean, -): boolean => calleeMatches(call.callee) && call.arguments[index] === node - -const isArgumentOf = ( - node: unknown, - parent: unknown, - index: number, - calleeMatches: (callee: unknown) => boolean, -): boolean => isCallExpression(parent) && isArgumentAt(node, parent, index, calleeMatches) - -interface IgnoreRule { - readonly matches: (node: unknown, parent: unknown, grandparent: unknown, ancestor: unknown) => boolean - readonly reason: string -} - -const isOptionalWithCallee = (callee: unknown): boolean => isNamedMember(callee, 'S', 'optionalWith') - -const isAnnotationsCallee = (callee: unknown): boolean => - isMemberExpression(callee) && isIdentifierNamed(callee.property, 'annotations') - -const argumentRule = ( - is: (node: unknown) => boolean, - argumentIndex: number, - calleeMatches: (callee: unknown) => boolean, - reason: string, -): IgnoreRule => ({ - matches: (node, parent) => is(node) && isArgumentOf(node, parent, argumentIndex, calleeMatches), - reason, -}) - -/** - * A documentation-keyed entry of an `annotations` call. Unlike the object rule - * this does not care what sits beside it: `title` is documentation whether or - * not an `arbitrary` shares the object, because replacing the title cannot - * change what the schema does. Emptying the whole object could, which is why - * that rule is the stricter of the two. - */ -const isDocumentationValue = (node: unknown, parent: unknown): boolean => - isDocumentationProperty(parent) && parent.value === node - -const documentationValueRule: IgnoreRule = { - matches: (node, parent, grandparent, ancestor) => - isDocumentationValue(node, parent) && isArgumentOf(grandparent, ancestor, 0, isAnnotationsCallee), - reason: ANNOTATION_TEXT_IGNORED, -} - -const RULES: readonly IgnoreRule[] = [ - argumentRule(isStringLiteral, 0, isSymbolForCallee, SYMBOL_DESCRIPTION_IGNORED), - argumentRule(isStringLiteral, 0, isTaggedFactoryCallee, TAGGED_TAG_IGNORED), - argumentRule(isObjectExpression, 1, isTaggedFactoryCallee, TAGGED_FIELDS_IGNORED), - argumentRule(isStringLiteral, 0, isClassFactoryReference, CLASS_ID_IGNORED), - argumentRule(isStringLiteral, 0, isBrandCallee, BRAND_NAME_IGNORED), - argumentRule(isArrowFunctionExpression, 1, isOptionalWithCallee, OPTIONAL_DEFAULT_IGNORED), - argumentRule(isDocumentationObject, 0, isAnnotationsCallee, ANNOTATION_OBJECT_IGNORED), - documentationValueRule, -] - -export const decideSchemaDeclarationIgnore = ( - node: unknown, - parent: unknown, - grandparent?: unknown, - ancestor?: unknown, -): string | undefined => RULES.find((rule) => rule.matches(node, parent, grandparent, ancestor))?.reason diff --git a/packages/stryker-plugins/src/effect-schema-ignorer/index.ts b/packages/stryker-plugins/src/effect-schema-ignorer/index.ts deleted file mode 100644 index 109959f7c..000000000 --- a/packages/stryker-plugins/src/effect-schema-ignorer/index.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Ignorer } from '@systemfsoftware/stryker-js-language' -import { declarePlugin } from '@systemfsoftware/stryker-js-plugin-interface' -import * as Layer from 'effect/Layer' -import * as Option from 'effect/Option' -import { ancestorsOf, type IgnorerPath } from '../AncestorPath.js' -import { - ANNOTATION_OBJECT_IGNORED, - ANNOTATION_TEXT_IGNORED, - BRAND_NAME_IGNORED, - CLASS_FIELDS_IGNORED, - CLASS_ID_IGNORED, - decideSchemaDeclarationIgnore, - OPTIONAL_DEFAULT_IGNORED, - SYMBOL_DESCRIPTION_IGNORED, - TAGGED_FIELDS_IGNORED, - TAGGED_TAG_IGNORED, -} from './SchemaDeclarationIgnore.js' - -const decisionAt = (chain: readonly unknown[], position: number): string | undefined => - decideSchemaDeclarationIgnore(chain[position], chain[position + 1], chain[position + 2], chain[position + 3]) - -const firstIgnoreReason = (path: IgnorerPath): string | undefined => { - const chain = [path.node, ...ancestorsOf(path)] - return chain.reduce((found, _, position) => found ?? decisionAt(chain, position), undefined) -} - -export const strykerPlugins = [ - declarePlugin( - 'Ignore', - 'effect-schema-declarations', - Layer.succeed(Ignorer, { - shouldIgnore: (path: IgnorerPath) => Option.fromUndefinedOr(firstIgnoreReason(path)), - }), - ), -] - -// Public-surface decision: tests reach the decision function through the -// barrel rather than deep-importing the .kernel.ts cell. -export { - ANNOTATION_OBJECT_IGNORED, - ANNOTATION_TEXT_IGNORED, - BRAND_NAME_IGNORED, - CLASS_FIELDS_IGNORED, - CLASS_ID_IGNORED, - decideSchemaDeclarationIgnore, - OPTIONAL_DEFAULT_IGNORED, - SYMBOL_DESCRIPTION_IGNORED, - TAGGED_FIELDS_IGNORED, - TAGGED_TAG_IGNORED, -} diff --git a/packages/stryker-plugins/src/in-source-test-ignorer/AstNode.schema.ts b/packages/stryker-plugins/src/in-source-test-ignorer/AstNode.schema.ts deleted file mode 100644 index 8ecdd4d28..000000000 --- a/packages/stryker-plugins/src/in-source-test-ignorer/AstNode.schema.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Schema as S } from 'effect' - -export const Identifier = S.Struct({ - type: S.Literal('Identifier'), - name: S.String, -}) -export type Identifier = S.Schema.Type - -export const AstLike = S.Struct({ type: S.String }) -export type AstLike = S.Schema.Type - -export const MetaProperty = S.Struct({ - type: S.Literal('MetaProperty'), - meta: Identifier, - property: Identifier, -}) -export type MetaProperty = S.Schema.Type - -export const ImportMetaMember = S.Struct({ - type: S.Literal('MemberExpression'), - object: MetaProperty, - property: Identifier, -}) -export type ImportMetaMember = S.Schema.Type - -export const BinaryExpression = S.Struct({ - type: S.Literal('BinaryExpression'), - left: AstLike, - right: AstLike, -}) -export type BinaryExpression = S.Schema.Type - -export const IfStatement = S.Struct({ - type: S.Literal('IfStatement'), - test: AstLike, -}) -export type IfStatement = S.Schema.Type - -/** Derived recognisers, declared beside the shapes they decide. */ -export const isImportMetaMember = S.is(ImportMetaMember) -export const isBinaryExpression = S.is(BinaryExpression) -export const isIfStatement = S.is(IfStatement) diff --git a/packages/stryker-plugins/src/in-source-test-ignorer/InSourceTestIgnore.ts b/packages/stryker-plugins/src/in-source-test-ignorer/InSourceTestIgnore.ts deleted file mode 100644 index ecbadd728..000000000 --- a/packages/stryker-plugins/src/in-source-test-ignorer/InSourceTestIgnore.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { - type BinaryExpression, - type Identifier, - isBinaryExpression, - isIfStatement, - isImportMetaMember, -} from './AstNode.schema.js' - -export const IN_SOURCE_TEST_IGNORED = - 'inside an `if (import.meta.vitest)` block — test code, not production behaviour' as const - -export const VITEST_META_PROPERTY = 'vitest' as const - -const allNamed = (pairs: readonly (readonly [Identifier, string])[]): boolean => - pairs.every(([access, name]) => access.name === name) - -const isImportMetaVitest = (node: unknown): boolean => - isImportMetaMember(node) && - allNamed([ - [node.object.meta, 'import'], - [node.object.property, 'meta'], - [node.property, VITEST_META_PROPERTY], - ]) - -const hasImportMetaVitestOperand = (test: BinaryExpression): boolean => - isImportMetaVitest(test.left) || isImportMetaVitest(test.right) - -const isBinaryImportMetaVitest = (test: unknown): boolean => - isBinaryExpression(test) && hasImportMetaVitestOperand(test) - -const guardsOnImportMetaVitest = (test: unknown): boolean => isImportMetaVitest(test) || isBinaryImportMetaVitest(test) - -export const isInSourceTestGuard = (node: unknown): boolean => - isIfStatement(node) && guardsOnImportMetaVitest(node.test) - -export const decideInSourceTestIgnore = (ancestors: Iterable): string | undefined => { - if ([...ancestors].some(isInSourceTestGuard)) return IN_SOURCE_TEST_IGNORED - return undefined -} diff --git a/packages/stryker-plugins/src/in-source-test-ignorer/index.ts b/packages/stryker-plugins/src/in-source-test-ignorer/index.ts deleted file mode 100644 index 99e8122d6..000000000 --- a/packages/stryker-plugins/src/in-source-test-ignorer/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Ignorer } from '@systemfsoftware/stryker-js-language' -import { declarePlugin } from '@systemfsoftware/stryker-js-plugin-interface' -import * as Layer from 'effect/Layer' -import * as Option from 'effect/Option' - -import { ancestorsOf, type IgnorerPath } from '../AncestorPath.js' -import { decideInSourceTestIgnore, IN_SOURCE_TEST_IGNORED, isInSourceTestGuard } from './InSourceTestIgnore.js' - -export const strykerPlugins = [ - declarePlugin( - 'Ignore', - 'in-source-vitest-block', - Layer.succeed(Ignorer, { - // The decision returns a reason or nothing; `Option` is how the port - // states that difference, so absence cannot be read as a reason. - shouldIgnore: (path: IgnorerPath) => Option.fromUndefinedOr(decideInSourceTestIgnore(ancestorsOf(path))), - }), - ), -] - -// Public-surface decision: tests reach the decision function through the -// barrel rather than deep-importing the .kernel.ts cell. -export { decideInSourceTestIgnore, IN_SOURCE_TEST_IGNORED, isInSourceTestGuard } diff --git a/packages/stryker-plugins/src/mod.ts b/packages/stryker-plugins/src/mod.ts deleted file mode 100644 index 62c34b59f..000000000 --- a/packages/stryker-plugins/src/mod.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { strykerPlugins as effectSchemaIgnorer } from './effect-schema-ignorer/index.js' -import { strykerPlugins as inSourceTestIgnorer } from './in-source-test-ignorer/index.js' -import { strykerPlugins as workflowMakeBoundaryIgnorer } from './workflow-make-ignorer/index.js' - -export const strykerPlugins = [ - ...effectSchemaIgnorer, - ...inSourceTestIgnorer, - ...workflowMakeBoundaryIgnorer, -] diff --git a/packages/stryker-plugins/src/schema-laws.test.ts b/packages/stryker-plugins/src/schema-laws.test.ts deleted file mode 100644 index 7fa4d5baa..000000000 --- a/packages/stryker-plugins/src/schema-laws.test.ts +++ /dev/null @@ -1,5 +0,0 @@ -// Generated: @systemfsoftware/effect-schema-vite rewrites this file, injecting a -// `ruleOfSchemas` law pair for every exported Effect Schema in this package. -// The generated body carries real import edges to each schema file so vitest's -// related-file walk (and therefore `stryker --related`) reaches them. -export {} diff --git a/packages/stryker-plugins/src/workflow-make-ignorer/AstNode.schema.ts b/packages/stryker-plugins/src/workflow-make-ignorer/AstNode.schema.ts deleted file mode 100644 index e17bb7e15..000000000 --- a/packages/stryker-plugins/src/workflow-make-ignorer/AstNode.schema.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { Schema as S } from 'effect' - -export const Identifier = S.Struct({ - type: S.Literal('Identifier'), - name: S.String, -}) -export type Identifier = S.Schema.Type - -// ESTree renders string literals as Literal{value:string}; the semantic name records what this matches. -export const StringLiteral = S.Struct({ - type: S.Literal('Literal'), - value: S.String, -}) -export type StringLiteral = S.Schema.Type - -/** - * The `Workflow.make(...)` decision argument. Effect Schema S.structs are inexact, so a - * body carrying parameters, statements, and nested expressions passes the `{ type }` shape - * while its children stay unmodeled — the boundary containment is identity, not shape. - */ -export const ArrowFunctionExpression = S.Struct({ - type: S.Literal('ArrowFunctionExpression'), -}) -export type ArrowFunctionExpression = S.Schema.Type - -/** A named-function decision argument, the `function (command) { … }` form of a make body. */ -export const FunctionExpression = S.Struct({ - type: S.Literal('FunctionExpression'), -}) -export type FunctionExpression = S.Schema.Type - -/** Any AST node outside the modeled vocabulary — accepts every node type structurally. */ -export const UnknownNode = S.Struct({ type: S.String }) -export type UnknownNode = S.Schema.Type - -export interface MemberExpression { - readonly type: 'MemberExpression' - readonly object: AstNode - readonly property: AstNode -} - -export interface CallExpression { - readonly type: 'CallExpression' - readonly callee: AstNode - /** Any expression can sit in an argument slot; the schema stays loose so exotic calls validate. */ - readonly arguments: readonly unknown[] -} - -export type AstNode = - | Identifier - | StringLiteral - | ArrowFunctionExpression - | FunctionExpression - | MemberExpression - | CallExpression - | UnknownNode - -export const AstNode: S.Schema = S.suspend( - (): S.Schema => - S.Union([ - Identifier, - StringLiteral, - ArrowFunctionExpression, - FunctionExpression, - MemberExpression, - CallExpression, - UnknownNode, - ]), -).annotate({ - identifier: 'systemfsoftware.stryker-plugins.workflow-make-ignorer.AstNode', - recursionBudget: { maxDepth: 6, depthSize: 'small' }, -}) - -export const MemberExpression: S.Schema = S.Struct({ - type: S.Literal('MemberExpression'), - object: AstNode, - property: AstNode, -}) - -export const CallExpression: S.Schema = S.Struct({ - type: S.Literal('CallExpression'), - callee: AstNode, - arguments: S.Array(S.Unknown), -}) - -export const ImportSpecifier = S.Struct({ - type: S.Literal('ImportSpecifier'), - imported: Identifier, - local: Identifier, -}) -export type ImportSpecifier = S.Schema.Type - -export const ImportNamespaceSpecifier = S.Struct({ - type: S.Literal('ImportNamespaceSpecifier'), - local: Identifier, -}) -export type ImportNamespaceSpecifier = S.Schema.Type - -/** - * A specifier union admitting the named and namespace forms. A default specifier cannot bind - * `Workflow` — `@systemfsoftware/effect-cell-types` ships no default export — so it is left - * out and an import carrying one is not a workflow import. - */ -export const ImportDeclaration = S.Struct({ - type: S.Literal('ImportDeclaration'), - source: StringLiteral, - specifiers: S.Array(S.Union([ImportSpecifier, ImportNamespaceSpecifier])), -}) -export type ImportDeclaration = S.Schema.Type - -/** The file root. Body holds every statement, so the element schema is intentionally loose. */ -export const Program = S.Struct({ - type: S.Literal('Program'), - body: S.Array(S.Unknown), -}) -export type Program = S.Schema.Type - -/** Derived recognisers, declared beside the shapes they decide. */ -export const isProgram = S.is(Program) -export const isImportDeclaration = S.is(ImportDeclaration) -export const isImportSpecifier = S.is(ImportSpecifier) -export const isImportNamespaceSpecifier = S.is(ImportNamespaceSpecifier) -export const isIdentifier = S.is(Identifier) -export const isStringLiteral = S.is(StringLiteral) -export const isMemberExpression = S.is(MemberExpression) -export const isCallExpression = S.is(CallExpression) -export const isArrowFunction = S.is(ArrowFunctionExpression) -export const isFunctionExpression = S.is(FunctionExpression) diff --git a/packages/stryker-plugins/src/workflow-make-ignorer/MakeBoundaryIgnore.ts b/packages/stryker-plugins/src/workflow-make-ignorer/MakeBoundaryIgnore.ts deleted file mode 100644 index 5c9937baa..000000000 --- a/packages/stryker-plugins/src/workflow-make-ignorer/MakeBoundaryIgnore.ts +++ /dev/null @@ -1,601 +0,0 @@ -import { - CallExpression, - Identifier, - ImportDeclaration, - ImportNamespaceSpecifier, - ImportSpecifier, - isArrowFunction, - isCallExpression, - isFunctionExpression, - isIdentifier, - isImportDeclaration, - isImportNamespaceSpecifier, - isImportSpecifier, - isMemberExpression, - isProgram, - isStringLiteral, - MemberExpression, - Program, -} from './AstNode.schema.js' - -export const NOT_INSIDE_WORKFLOW_MAKE = - 'mutant is outside every Workflow.make and Workflow.total decision body; only decider bodies are the mutation population' as const - -/** The module whose `Workflow` value owns the workflow construction boundary. */ -const WORKFLOW_SOURCE = '@systemfsoftware/effect-cell-types' as const - -/** The import name a specifier must carry to be the workflow value. */ -const WORKFLOW_IMPORT_NAME = 'Workflow' as const - -/** - * The members of the workflow value — `make` and `total` construct a workflow from a - * decision; `andThen` composes one workflow's decision output into the next workflow's - * command. Re-derived in this module's own vocabulary: the kernel's set is single-homed - * and not importable (MB1), so nothing here can mirror it by construction. - */ -const WORKFLOW_CONSTRUCTOR_MEMBERS: Readonly> = { - make: true, - total: true, - andThen: true, -} - -/** - * The constructor members whose signature takes constructed workflows in place of a - * decider: an `andThen` construction holds no decision body in the file that opens it, - * so nothing in its argument list joins the mutation population. A member added above - * without a line here keeps its decider body — and its mutants — in the population. - */ -const COMPOSING_MEMBERS: Readonly> = { andThen: true } - -const NO_WORKFLOW_LOCALS: ReadonlySet = new Set() -const NO_CONSTRUCTOR_ARGUMENT_BODIES: ReadonlySet = new Set() - -// -- unvalidated node shape probes ----------------------------------------------------------- -// Four node kinds this gate reads — a named function declaration, a variable declaration, a -// declarator, and an export wrapper — have no schema in AstNode.schema.ts, so their shapes are -// probed off the raw parser node. Each probe asks one question about one value: a question that -// needs two goes through two names, because a logical operator is itself a path the mutation -// score must cover. - -/** Any object the identity walk can descend into. */ -const isWalkable = (value: unknown): value is Record => typeof value === 'object' && value !== null - -/** Read a property off an unvalidated value; `undefined` when the value is not an object. */ -const propertyOf = (value: unknown, key: string): unknown => { - if (!isWalkable(value)) return undefined - return value[key] -} - -/** The string property at `key`, or `undefined` when it is absent or not a string. */ -const stringPropertyOf = (value: unknown, key: string): string | undefined => { - const raw = propertyOf(value, key) - if (typeof raw !== 'string') return undefined - return raw -} - -/** True when an unvalidated value carries the given AST node tag. */ -const hasNodeType = (value: unknown, type: string): boolean => stringPropertyOf(value, 'type') === type - -/** True when the key is present on an unvalidated value. */ -const hasProperty = (value: unknown, key: string): boolean => { - if (!isWalkable(value)) return false - return key in value -} - -/** True when a resolved value is absent: `null` or `undefined`. */ -const isAbsent = (value: unknown): boolean => value === undefined || value === null - -/** A named `function` declaration statement. */ -interface FunctionDeclarationNode { - readonly type: 'FunctionDeclaration' - readonly id: Identifier -} - -/** A `const`/`let`/`var` declaration statement carrying declarators. */ -interface VariableDeclarationNode { - readonly type: 'VariableDeclaration' - readonly kind: string - readonly declarations: readonly unknown[] -} - -/** A `name = init` declarator whose binding is a plain identifier. */ -interface NamedDeclaratorNode { - readonly type: 'VariableDeclarator' - readonly id: Identifier - readonly init: unknown -} - -/** An `export`-wrapped statement; only its declaration carries a binding. */ -interface ExportWrapperNode { - readonly type: 'ExportNamedDeclaration' | 'ExportDefaultDeclaration' - readonly declaration?: unknown -} - -/** A named `function` declaration statement. */ -const isFunctionDeclaration = (value: unknown): value is FunctionDeclarationNode => - hasNodeType(value, 'FunctionDeclaration') && isIdentifier(propertyOf(value, 'id')) - -/** A declaration statement whose declarators hang off `declarations`. */ -const isVariableDeclaration = (value: unknown): value is VariableDeclarationNode => - hasNodeType(value, 'VariableDeclaration') && hasKindAndDeclarations(value) - -const hasKindAndDeclarations = (value: unknown): boolean => - hasProperty(value, 'kind') && Array.isArray(propertyOf(value, 'declarations')) - -/** An `export` statement in either named or default form. */ -const isExportWrapper = (value: unknown): value is ExportWrapperNode => - hasNodeType(value, 'ExportNamedDeclaration') || hasNodeType(value, 'ExportDefaultDeclaration') - -/** The declaration an `export` wraps; the wrapper itself when it wraps none. */ -const unwrapExport = (value: unknown): unknown => { - if (!isExportWrapper(value)) return value - return declaredValueOf(value) -} - -const declaredValueOf = (wrapper: ExportWrapperNode): unknown => { - if (isAbsent(wrapper.declaration)) return wrapper - return wrapper.declaration -} - -// -- Program-keyed memoization --------------------------------------------------------------- - -/** - * A pure derivation of one Program, memoized: the probe loop visits every mutant in the file, - * and each derivation is a pure function of the Program it was keyed on. - */ -const memoizedForProgram = ( - cache: WeakMap, - program: Program, - derive: (program: Program) => T, -): T => { - const cached = cache.get(program) - if (cached !== undefined) return cached - const derived = derive(program) - cache.set(program, derived) - return derived -} - -// -- the workflow import resolution ---------------------------------------------------------- - -/** Program -> its workflow local names, so repeated mutant probes never re-walk the body. */ -const WORKFLOW_LOCALS_BY_PROGRAM = new WeakMap>() - -/** - * The local binding names that resolve to the `Workflow` value of the cell-types module: the - * canonical `import { Workflow }`, an alias `import { Workflow as W }`, or the namespace form - * `import * as Workflow`. This is deliberately a file-level import resolution, not a scope - * analysis — the boundary is a mechanical gate, so a local shadowing the imported name - * shadows the boundary too (no production site does this). Memoized keyed by the Program - * node: the probe loop visits every mutant in the file, and the import set is a pure - * function of the Program. - */ -const workflowLocalNamesOf = (program: unknown): ReadonlySet => { - if (!isProgram(program)) return NO_WORKFLOW_LOCALS - return memoizedForProgram(WORKFLOW_LOCALS_BY_PROGRAM, program, workflowLocalNamesIn) -} - -/** Every local name the file's cell-types imports bind. */ -const workflowLocalNamesIn = (program: Program): ReadonlySet => { - const names = new Set() - for (const statement of program.body) collectImportLocalNames(statement, names) - return names -} - -/** Adds the locals one statement binds, when that statement imports the workflow module. */ -const collectImportLocalNames = (statement: unknown, names: Set): void => { - if (!isWorkflowImport(statement)) return - addSpecifierLocalNames(statement.specifiers, names) -} - -const addSpecifierLocalNames = (specifiers: readonly unknown[], names: Set): void => { - for (const specifier of specifiers) addWorkflowLocalName(specifier, names) -} - -const addWorkflowLocalName = (specifier: unknown, names: Set): void => { - if (!isWorkflowImportSpecifier(specifier)) return - names.add(specifier.local.name) -} - -/** An import of the cell-types module, whatever its specifiers bind. */ -const isWorkflowImport = (statement: unknown): statement is ImportDeclaration => - isImportDeclaration(statement) && carriesWorkflowSource(statement) - -const carriesWorkflowSource = (statement: ImportDeclaration): boolean => - isStringLiteral(statement.source) && statement.source.value === WORKFLOW_SOURCE - -/** A specifier binding a local name to the workflow value: the named or the namespace form. */ -const isWorkflowImportSpecifier = ( - specifier: unknown, -): specifier is ImportSpecifier | ImportNamespaceSpecifier => - isImportNamespaceSpecifier(specifier) || isWorkflowNamedImport(specifier) - -/** `{ Workflow as local }` — the imported name decides, the local alias is free. */ -const isWorkflowNamedImport = (specifier: unknown): specifier is ImportSpecifier => - isImportSpecifier(specifier) && importsWorkflowName(specifier) - -const importsWorkflowName = (specifier: ImportSpecifier): boolean => - isIdentifier(specifier.imported) && specifier.imported.name === WORKFLOW_IMPORT_NAME - -// -- the boundary call shape ----------------------------------------------------------------- - -const isWorkflowConstructorCallee = ( - callee: unknown, - localNames: ReadonlySet, -): callee is MemberExpression => isMemberExpression(callee) && isWorkflowBodyBearingMember(callee, localNames) - -const isWorkflowBodyBearingMember = (member: MemberExpression, localNames: ReadonlySet): boolean => - isWorkflowLocalMember(member, localNames) && isBodyBearingMemberName(member.property) - -const isWorkflowLocalMember = (member: MemberExpression, localNames: ReadonlySet): boolean => - isIdentifier(member.object) && localNames.has(member.object.name) - -const isBodyBearingMemberName = (property: unknown): boolean => - isConstructorMemberName(property) && !isComposingMemberName(property) - -const isConstructorMemberName = (property: unknown): boolean => - isIdentifier(property) && WORKFLOW_CONSTRUCTOR_MEMBERS[property.name] === true - -const isComposingMemberName = (property: unknown): boolean => - isIdentifier(property) && COMPOSING_MEMBERS[property.name] === true - -const isWorkflowConstructorCall = (node: unknown, localNames: ReadonlySet): node is CallExpression => - isCallExpression(node) && isWorkflowConstructorCallee(node.callee, localNames) - -// -- module-scope function reference resolution --------------------------------------------- -// A `Workflow.make(decision)` or `Workflow.total(decision)` call whose argument is an -// identifier resolving to a same-file function keeps that function's body inside the -// mutation population, mirroring the oxlint kernel's followIdentifier walk (depth-8 cycle -// bound). This is deliberately a file-level mechanical resolution, not a scope analysis — -// the boundary is a mechanical gate (see workflowLocalNamesOf), so a same-named local -// shadowing the module binding shadows the resolution too (no production site does this). - -/** Follow depth bound, mirroring the oxlint kernel's cycle guard. */ -const MAX_FOLLOW_DEPTH = 8 - -/** - * The same-file follow walk: a constructor-argument name resolves through module-scope - * `const` bindings (function initializers and identifier aliases) and named - * function declarations, with the depth bound breaking alias cycles. - */ -const followFunctionReference = ( - name: string, - bindings: ReadonlyMap, - depth: number, -): unknown => { - if (depth > MAX_FOLLOW_DEPTH) return null - return resolveFollowingBinding(bindings.get(name), bindings, depth) -} - -/** `null` for a name the file never binds; otherwise the binding's own resolution. */ -const resolveFollowingBinding = ( - binding: unknown, - bindings: ReadonlyMap, - depth: number, -): unknown => { - if (isAbsent(binding)) return null - return followableResolution(binding, bindings, depth) -} - -/** A function node is the body itself; an identifier alias continues the walk. */ -const followableResolution = ( - binding: unknown, - bindings: ReadonlyMap, - depth: number, -): unknown => { - if (isFunctionLike(binding)) return binding - return aliasResolution(binding, bindings, depth) -} - -const aliasResolution = ( - binding: unknown, - bindings: ReadonlyMap, - depth: number, -): unknown => { - if (!isIdentifier(binding)) return null - return followFunctionReference(binding.name, bindings, depth + 1) -} - -/** Any of the forms a decision body takes: an arrow, a `function` expression, or a declaration. */ -const isFunctionLike = (value: unknown): boolean => isFunctionDeclaration(value) || isAnonymousFunction(value) - -/** The anonymous decision forms: an arrow function or a `function` expression. */ -const isAnonymousFunction = (value: unknown): boolean => isArrowFunction(value) || isFunctionExpression(value) - -/** The module-scope bindings of a file: name -> function-like node, alias identifier, or null. */ -const moduleBindingsOf = (programBody: readonly unknown[]): ReadonlyMap => { - const bindings = new Map() - for (const rawStatement of programBody) addStatementBinding(bindings, unwrapExport(rawStatement)) - return bindings -} - -/** A named function declaration binds its own name; anything else binds through a declaration. */ -const addStatementBinding = (bindings: Map, statement: unknown): void => { - if (isFunctionDeclaration(statement)) { - bindings.set(statement.id.name, statement) - return - } - addConstBindings(bindings, statement) -} - -const addConstBindings = (bindings: Map, statement: unknown): void => { - if (!isConstDeclaration(statement)) return - addDeclaratorBindings(bindings, statement.declarations) -} - -const isConstDeclaration = (value: unknown): value is VariableDeclarationNode => - isVariableDeclaration(value) && value.kind === 'const' - -const addDeclaratorBindings = ( - bindings: Map, - declarations: readonly unknown[], -): void => { - for (const declarator of declarations) addDeclaratorBinding(bindings, declarator) -} - -const addDeclaratorBinding = (bindings: Map, declarator: unknown): void => { - if (!isNamedDeclarator(declarator)) return - addInitialisedBinding(bindings, declarator) -} - -/** A declarator whose binding is a plain identifier: the only shape the follow walk keys on. */ -const isNamedDeclarator = (value: unknown): value is NamedDeclaratorNode => - hasNodeType(value, 'VariableDeclarator') && hasIdentifierBinding(value) - -/** The identifier and initialiser properties a keyable declarator carries. */ -const hasIdentifierBinding = (value: unknown): boolean => - isIdentifier(propertyOf(value, 'id')) && hasProperty(value, 'init') - -/** The initialiser decides: a function or alias the follow walk can use, otherwise null. */ -const addInitialisedBinding = ( - bindings: Map, - declarator: NamedDeclaratorNode, -): void => { - if (isAbsent(declarator.init)) return - bindings.set(declarator.id.name, followableBindingOf(declarator.init)) -} - -/** The initialiser as a binding: a function-like node or alias identifier, otherwise null. */ -const followableBindingOf = (init: unknown): unknown => { - if (!isFollowableBinding(init)) return null - return init -} - -const isFollowableBinding = (init: unknown): boolean => isFunctionLike(init) || isIdentifier(init) - -// -- the identity walk ----------------------------------------------------------------------- - -const walkAllNodes = (root: unknown, visit: (node: unknown) => void): void => - walkUnvisited(root, visit, new Set()) - -/** - * The cycle-guarded walk: a node is visited once, then every property value and array entry it - * carries. A mutant's ancestor chain and a resolved decision body are matched by identity, so - * the walk must reach every nested node the parser hung off the Program. - */ -const walkUnvisited = ( - value: unknown, - visit: (node: unknown) => void, - visited: Set, -): void => { - if (!claimNode(value, visited)) return - visit(value) - walkPropertyValues(value, visit, visited) -} - -/** Claims a node for the walk; false when the value is not a node, or was already visited. */ -const claimNode = (value: unknown, visited: Set): value is Record => { - if (!isUnvisitedWalkable(value, visited)) return false - visited.add(value) - return true -} - -const isUnvisitedWalkable = (value: unknown, visited: Set): value is object => - isWalkable(value) && !visited.has(value) - -const walkPropertyValues = ( - node: Record, - visit: (node: unknown) => void, - visited: Set, -): void => { - for (const child of Object.values(node)) walkPropertyValue(child, visit, visited) -} - -/** An array property is a list of nodes; every other property value is a node on its own. */ -const walkPropertyValue = ( - child: unknown, - visit: (node: unknown) => void, - visited: Set, -): void => { - if (Array.isArray(child)) return walkEntries(child, visit, visited) - walkUnvisited(child, visit, visited) -} - -const walkEntries = ( - entries: readonly unknown[], - visit: (node: unknown) => void, - visited: Set, -): void => { - for (const entry of entries) walkUnvisited(entry, visit, visited) -} - -// -- the resolved decision bodies ------------------------------------------------------------ - -/** - * Program -> the same-file function bodies a `Workflow.make` or `Workflow.total` - * identifier argument resolves to. - */ -const CONSTRUCTOR_ARGUMENT_BODIES_BY_PROGRAM = new WeakMap>() - -/** - * The function nodes a constructor call names by identifier, memoized keyed by the Program: - * the probe loop visits every mutant in the file, and the resolution is a pure - * function of the Program. The bodies are the container objects — a mutant whose - * ancestor chain includes one stays inside the population. - */ -const constructorArgumentBodiesOf = (program: unknown): ReadonlySet => { - if (!isProgram(program)) return NO_CONSTRUCTOR_ARGUMENT_BODIES - return memoizedForProgram(CONSTRUCTOR_ARGUMENT_BODIES_BY_PROGRAM, program, argumentBodiesIn) -} - -const argumentBodiesIn = (program: Program): ReadonlySet => { - const bodies = new Set() - addConstructorArgumentBodies(program, bodies) - return bodies -} - -/** A file whose `Workflow` binding is not the cell-types value opens no boundary at all. */ -const addConstructorArgumentBodies = (program: Program, bodies: Set): void => { - const localNames = workflowLocalNamesOf(program) - if (localNames.size === 0) return - collectConstructorArgumentBodies(program, localNames, bodies) -} - -const collectConstructorArgumentBodies = ( - program: Program, - localNames: ReadonlySet, - bodies: Set, -): void => { - const bindings = moduleBindingsOf(program.body) - walkAllNodes(program, (node) => addFirstConstructorArgumentBody(node, localNames, bindings, bodies)) -} - -const addFirstConstructorArgumentBody = ( - node: unknown, - localNames: ReadonlySet, - bindings: ReadonlyMap, - bodies: Set, -): void => { - if (!isWorkflowConstructorCall(node, localNames)) return - addResolvedBody(firstResolvedBodyIn(node.arguments, bindings), bodies) -} - -/** - * The body of the first argument that resolves to one. The decider is found by SHAPE, never by - * slot index: a constructor that takes a decider takes the command schema class first and the - * decider second, so a resolver pinned to slot 0 resolves a class to nothing and silently drops - * the referenced decision body out of the mutation population — every mutant in it stops being - * tested while the score still reports green. - */ -const firstResolvedBodyIn = ( - args: readonly unknown[], - bindings: ReadonlyMap, -): object | undefined => args.map((argument) => resolvedBodyOf(argument, bindings)).find(isDefined) - -/** The object one constructor argument resolves to; a non-identifier argument resolves to nothing. */ -const resolvedBodyOf = ( - argument: unknown, - bindings: ReadonlyMap, -): object | undefined => { - if (!isIdentifier(argument)) return undefined - return resolvedObjectOf(followFunctionReference(argument.name, bindings, 0)) -} - -/** A resolved reference counts only when it is a node: containment is object identity. */ -const resolvedObjectOf = (value: unknown): object | undefined => { - if (!isWalkable(value)) return undefined - return value -} - -const isDefined = (value: T | undefined): value is T => value !== undefined - -const addResolvedBody = (body: object | undefined, bodies: Set): void => { - if (body === undefined) return - bodies.add(body) -} - -// -- the inverted population selector -------------------------------------------------------- - -/** - * True when the mutant descends from an argument slot of a body-bearing constructor call — the - * parser puts the decider body under the call's `arguments` array, so identity containment through - * the walk is the boundary test. A mutant ON the call (its callee or the call itself) is outside - * the argument and therefore outside the population, which is the point of the inverted gate. - */ -const insideConstructorBoundary = (node: unknown, ancestors: readonly unknown[]): boolean => { - const root = workflowRootOf(ancestors) - const localNames = workflowLocalNamesOf(root) - if (localNames.size === 0) return false - return insideConstructorArgument(node, ancestors, localNames, root) -} - -/** The outermost Program in the chain — the file the mutant was parsed from. */ -const workflowRootOf = (ancestors: readonly unknown[]): unknown => - ancestors.reduce(outermostProgramOf, undefined) - -const outermostProgramOf = (root: unknown, ancestor: unknown): unknown => { - if (isProgram(ancestor)) return ancestor - return root -} - -/** Direct containment first: an ancestor constructor call whose argument slot holds the mutant. */ -const insideConstructorArgument = ( - node: unknown, - ancestors: readonly unknown[], - localNames: ReadonlySet, - root: unknown, -): boolean => { - if (ancestorsContainConstructorArgument(node, ancestors, localNames)) return true - return insideResolvedBody(node, ancestors, root) -} - -const ancestorsContainConstructorArgument = ( - node: unknown, - ancestors: readonly unknown[], - localNames: ReadonlySet, -): boolean => - ancestors.some((ancestor, index) => - isConstructorArgumentBoundary(ancestor, chainChildOf(node, ancestors, index), localNames) - ) - -/** An ancestor call is the boundary when the node one step below it sits in an argument slot. */ -const isConstructorArgumentBoundary = ( - ancestor: unknown, - child: unknown, - localNames: ReadonlySet, -): boolean => isWorkflowConstructorCall(ancestor, localNames) && ancestor.arguments.includes(child) - -/** The node one step below `ancestors[index]`: the mutant itself for its immediate parent. */ -const chainChildOf = (node: unknown, ancestors: readonly unknown[], index: number): unknown => { - if (index === 0) return node - return ancestors[index - 1] -} - -/** - * A constructor argument naming a same-file function keeps that function's body inside the population - * even though the call is a sibling statement, not an ancestor: the resolved body's identity in - * the mutant's ancestor chain is the containment. - */ -const insideResolvedBody = ( - node: unknown, - ancestors: readonly unknown[], - root: unknown, -): boolean => { - const resolvedBodies = constructorArgumentBodiesOf(root) - if (resolvedBodies.size === 0) return false - return chainContainsResolvedBody(node, ancestors, resolvedBodies) -} - -const chainContainsResolvedBody = ( - node: unknown, - ancestors: readonly unknown[], - resolvedBodies: ReadonlySet, -): boolean => { - if (isResolvedBody(node, resolvedBodies)) return true - return ancestors.some((ancestor) => isResolvedBody(ancestor, resolvedBodies)) -} - -const isResolvedBody = (value: unknown, resolvedBodies: ReadonlySet): boolean => - isWalkable(value) && resolvedBodies.has(value) - -/** - * The inverted population selector: every mutant whose ancestor chain contains no - * `Workflow.make(...)` or `Workflow.total(...)` call argument is excised with the named - * reason, and every mutant inside any make boundary passes through to the next ignorer. - */ -export const decideWorkflowMakeBoundaryIgnore = ( - node: unknown, - ancestors: readonly unknown[], -): string | undefined => { - if (insideConstructorBoundary(node, ancestors)) return undefined - return NOT_INSIDE_WORKFLOW_MAKE -} diff --git a/packages/stryker-plugins/src/workflow-make-ignorer/index.ts b/packages/stryker-plugins/src/workflow-make-ignorer/index.ts deleted file mode 100644 index cd1b18910..000000000 --- a/packages/stryker-plugins/src/workflow-make-ignorer/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Ignorer } from '@systemfsoftware/stryker-js-language' -import { declarePlugin } from '@systemfsoftware/stryker-js-plugin-interface' -import * as Layer from 'effect/Layer' -import * as Option from 'effect/Option' - -import { ancestorsOf, type IgnorerPath } from '../AncestorPath.js' -import { decideWorkflowMakeBoundaryIgnore, NOT_INSIDE_WORKFLOW_MAKE } from './MakeBoundaryIgnore.js' - -export const strykerPlugins = [ - declarePlugin( - 'Ignore', - 'workflow-make-boundary', - Layer.succeed(Ignorer, { - shouldIgnore: (path: IgnorerPath) => - Option.fromUndefinedOr(decideWorkflowMakeBoundaryIgnore(path.node, [...ancestorsOf(path)])), - }), - ), -] - -// Public-surface decision: tests reach the decision function through the -// barrel rather than deep-importing the .kernel.ts cell. -export { decideWorkflowMakeBoundaryIgnore, NOT_INSIDE_WORKFLOW_MAKE } diff --git a/packages/stryker-plugins/tests/__fixtures__/WorkflowMakeAst.fixtures.ts b/packages/stryker-plugins/tests/__fixtures__/WorkflowMakeAst.fixtures.ts deleted file mode 100644 index 17a0d9a71..000000000 --- a/packages/stryker-plugins/tests/__fixtures__/WorkflowMakeAst.fixtures.ts +++ /dev/null @@ -1,133 +0,0 @@ -interface Identifier { - readonly type: 'Identifier' - readonly name: string -} - -interface MemberExpression { - readonly type: 'MemberExpression' - readonly object: Identifier - readonly property: Identifier -} - -interface CallExpression { - readonly type: 'CallExpression' - readonly callee: AstNode - readonly arguments: readonly unknown[] -} - -interface ArrowFunctionExpression { - readonly type: 'ArrowFunctionExpression' - readonly body: unknown -} - -interface Program { - readonly type: 'Program' - readonly body: readonly unknown[] -} - -interface StringLiteral { - readonly type: 'Literal' - readonly value: string -} - -type AstNode = unknown - -export const identifier = (name: string): Identifier => ({ type: 'Identifier', name }) - -export const stringLiteral = (value: string): StringLiteral => ({ type: 'Literal', value }) - -export const importSpecifierOf = (imported: string, local: string) => ({ - type: 'ImportSpecifier' as const, - imported: identifier(imported), - local: identifier(local), -}) - -export const namespaceSpecifier = (local: string) => ({ - type: 'ImportNamespaceSpecifier' as const, - local: identifier(local), -}) - -export const importDeclarationOf = (source: string, specifiers: readonly unknown[]) => ({ - type: 'ImportDeclaration' as const, - source: stringLiteral(source), - specifiers, -}) - -/** `import { Workflow } from '@systemfsoftware/effect-cell-types'` — the canonical production shape. */ -export const workflowNamedImport = () => - importDeclarationOf('@systemfsoftware/effect-cell-types', [importSpecifierOf('Workflow', 'Workflow')]) - -/** `import { Workflow as W } from ...` — the alias keeps the same binding semantics. */ -export const workflowAliasedImport = (local: string) => - importDeclarationOf('@systemfsoftware/effect-cell-types', [importSpecifierOf('Workflow', local)]) - -/** `import * as Workflow from ...` — the namespace form the boundary also honours. */ -export const workflowNamespaceImport = (local: string) => - importDeclarationOf('@systemfsoftware/effect-cell-types', [namespaceSpecifier(local)]) - -/** A same-named `Workflow` coming from anywhere else must NOT open a boundary. */ -export const unrelatedImport = (source: string, local: string) => - importDeclarationOf(source, [importSpecifierOf(local, local)]) - -export const programOf = (body: readonly unknown[]): Program => ({ type: 'Program', body }) - -export const memberOf = (object: string, property: string): MemberExpression => ({ - type: 'MemberExpression', - object: identifier(object), - property: identifier(property), -}) - -export const callOf = (callee: AstNode, args: readonly unknown[]): CallExpression => ({ - type: 'CallExpression', - callee, - arguments: args, -}) - -/** `Workflow.make(argument)` — the boundary call, when `object` names the imported local. */ -export const workflowMakeCallOf = (arg: unknown, objectName = 'Workflow'): CallExpression => - callOf(memberOf(objectName, 'make'), [arg]) - -/** - * `Workflow.make(Command, argument)` — the two-argument boundary call. The - * command schema class occupies slot 0 and the decider slot 1, so a resolver - * pinned to slot 0 finds a class, resolves no function, and silently drops the - * referenced decision body out of the mutation population. - */ -export const workflowMakeCallOfTwo = ( - command: unknown, - arg: unknown, - objectName = 'Workflow', -): CallExpression => callOf(memberOf(objectName, 'make'), [command, arg]) - -export const workflowTotalCallOf = (arg: unknown, objectName = 'Workflow'): CallExpression => - callOf(memberOf(objectName, 'total'), [arg]) - -export const workflowAndThenCallOf = (args: readonly unknown[], objectName = 'Workflow'): CallExpression => - callOf(memberOf(objectName, 'andThen'), args) - -/** `class {}` — a command class in slot 0; never a decision body. */ -export const classDeclarationOf = (name: string) => ({ - type: 'ClassDeclaration' as const, - id: identifier(name), - body: { type: 'ClassBody' as const, body: [] }, -}) - -/** - * The decision body of a `Workflow.make(...)` argument — the object that must be - * reference-identical with the call's first argument for containment to hold. - */ -export const makeBodyOf = (inner: unknown): ArrowFunctionExpression & { readonly body: unknown } => ({ - type: 'ArrowFunctionExpression', - body: inner, -}) - -/** - * `const = ` — a module-scope binding a `Workflow.make` identifier - * argument can resolve to. The declarator layer is elided: the parser's ancestry - * goes body -> declarator -> declaration, and the kernels model the statement. - */ -export const constBindingOf = (name: string, init: unknown) => ({ - type: 'VariableDeclaration', - kind: 'const', - declarations: [{ type: 'VariableDeclarator', id: identifier(name), init }], -}) diff --git a/packages/stryker-plugins/tests/effect-schema-ignorer/schema-declaration-ignore-property.integration.test.ts b/packages/stryker-plugins/tests/effect-schema-ignorer/schema-declaration-ignore-property.integration.test.ts deleted file mode 100644 index 8901be005..000000000 --- a/packages/stryker-plugins/tests/effect-schema-ignorer/schema-declaration-ignore-property.integration.test.ts +++ /dev/null @@ -1,614 +0,0 @@ -import { Gherkin, Given, it, layer, makeFeature, Then, When } from '@systemfsoftware/effect-gherkin-spec' -import { Effect } from 'effect' -import { expect } from 'vitest' - -import { - ANNOTATION_OBJECT_IGNORED, - ANNOTATION_TEXT_IGNORED, - decideSchemaDeclarationIgnore, - SYMBOL_DESCRIPTION_IGNORED, - TAGGED_FIELDS_IGNORED, - TAGGED_TAG_IGNORED, -} from '@systemfsoftware/stryker-plugins/effect-schema-ignorer' - -import { - annotationsCall, - callOf, - identifier, - memberOf, - namedProperty, - objectExpression, - objectOf, - propertyOf, - stringLiteral, - symbolForCall, - taggedCall, -} from '../__fixtures__/EffectSchemaAst.fixtures.js' - -const Feature = makeFeature({ it, layer }) - -Feature('Effect Schema declarations — invariant coverage as named examples') - .body(({ scenario }) => { - scenario( - 'A `Symbol.for` description string is recognised as ignorable', - Gherkin.Do.pipe( - Given('`Symbol.for("MyBrand")`')('node', () => - Effect.sync(() => { - const description = stringLiteral('MyBrand') - const call = symbolForCall(description) - return { description, call } - })), - When('decideSchemaDeclarationIgnore examines the description argument')( - 'reason', - (s) => Effect.sync(() => decideSchemaDeclarationIgnore(s.node.description, s.node.call)), - ), - Then('it returns SYMBOL_DESCRIPTION_IGNORED')((s) => - Effect.sync(() => { - expect(s.reason).toBe(SYMBOL_DESCRIPTION_IGNORED) - }) - ), - ), - ) - - scenario( - 'A `Schema.TaggedClass` tag string is recognised as ignorable', - Gherkin.Do.pipe( - Given('`Schema.TaggedClass("myTag", {})`')('node', () => - Effect.sync(() => { - const tag = stringLiteral('myTag') - const fields = objectExpression() - const call = taggedCall('TaggedClass', tag, fields) - return { tag, call } - })), - When('decideSchemaDeclarationIgnore examines the tag argument')( - 'reason', - (s) => Effect.sync(() => decideSchemaDeclarationIgnore(s.node.tag, s.node.call)), - ), - Then('it returns TAGGED_TAG_IGNORED')((s) => - Effect.sync(() => { - expect(s.reason).toBe(TAGGED_TAG_IGNORED) - }) - ), - ), - ) - - scenario( - 'A `Schema.TaggedError` tag string is recognised as ignorable', - Gherkin.Do.pipe( - Given('`Schema.TaggedError("err", {})`')('node', () => - Effect.sync(() => { - const tag = stringLiteral('err') - const fields = objectExpression() - const call = taggedCall('TaggedError', tag, fields) - return { tag, call } - })), - When('decideSchemaDeclarationIgnore examines the tag argument')( - 'reason', - (s) => Effect.sync(() => decideSchemaDeclarationIgnore(s.node.tag, s.node.call)), - ), - Then('it returns TAGGED_TAG_IGNORED')((s) => - Effect.sync(() => { - expect(s.reason).toBe(TAGGED_TAG_IGNORED) - }) - ), - ), - ) - - scenario( - 'A `Schema.TaggedClass` fields object is recognised as ignorable', - Gherkin.Do.pipe( - Given('`Schema.TaggedClass("myTag", {})`')('node', () => - Effect.sync(() => { - const tag = stringLiteral('myTag') - const fields = objectExpression() - const call = taggedCall('TaggedClass', tag, fields) - return { fields, call } - })), - When('decideSchemaDeclarationIgnore examines the fields argument')( - 'reason', - (s) => Effect.sync(() => decideSchemaDeclarationIgnore(s.node.fields, s.node.call)), - ), - Then('it returns TAGGED_FIELDS_IGNORED')((s) => - Effect.sync(() => { - expect(s.reason).toBe(TAGGED_FIELDS_IGNORED) - }) - ), - ), - ) - - scenario( - 'A `Schema.TaggedError` fields object is recognised as ignorable', - Gherkin.Do.pipe( - Given('`Schema.TaggedError("err", {})`')('node', () => - Effect.sync(() => { - const tag = stringLiteral('err') - const fields = objectExpression() - const call = taggedCall('TaggedError', tag, fields) - return { fields, call } - })), - When('decideSchemaDeclarationIgnore examines the fields argument')( - 'reason', - (s) => Effect.sync(() => decideSchemaDeclarationIgnore(s.node.fields, s.node.call)), - ), - Then('it returns TAGGED_FIELDS_IGNORED')((s) => - Effect.sync(() => { - expect(s.reason).toBe(TAGGED_FIELDS_IGNORED) - }) - ), - ), - ) - - scenario( - 'A `Symbol.iterator` member is not an exact-discriminant match', - Gherkin.Do.pipe( - Given('`Symbol.iterator("desc")`')('node', () => - Effect.sync(() => { - const description = stringLiteral('desc') - const call = callOf(memberOf('Symbol', 'iterator'), [description]) - return { description, call } - })), - When('decideSchemaDeclarationIgnore examines the description argument')( - 'reason', - (s) => Effect.sync(() => decideSchemaDeclarationIgnore(s.node.description, s.node.call)), - ), - Then('it returns undefined (member name is not `for`)')((s) => - Effect.sync(() => { - expect(s.reason).toBeUndefined() - }) - ), - ), - ) - - scenario( - 'A non-tagged `Schema.Struct` factory is not an exact-discriminant match', - Gherkin.Do.pipe( - Given('`Schema.Struct("tag", {})` — non-tagged factory')('node', () => - Effect.sync(() => { - const tag = stringLiteral('tag') - const fields = objectExpression() - const call = taggedCall('Struct', tag, fields) - return { tag, fields, call } - })), - When('decideSchemaDeclarationIgnore examines the tag and fields arguments')( - 'results', - (s) => - Effect.sync(() => ({ - tag: decideSchemaDeclarationIgnore(s.node.tag, s.node.call), - fields: decideSchemaDeclarationIgnore(s.node.fields, s.node.call), - })), - ), - Then('both return undefined')((s) => - Effect.sync(() => { - expect(s.results.tag).toBeUndefined() - expect(s.results.fields).toBeUndefined() - }) - ), - ), - ) - - scenario( - 'An object expression at the tag slot is position-load-bearing', - Gherkin.Do.pipe( - Given('`Schema.TaggedClass({}, "tag")` — arguments swapped')('node', () => - Effect.sync(() => { - const tag = stringLiteral('tag') - const fields = objectExpression() - const call = taggedCall('TaggedClass', fields, tag) - return { fields, call } - })), - When('decideSchemaDeclarationIgnore examines the first argument')( - 'reason', - (s) => Effect.sync(() => decideSchemaDeclarationIgnore(s.node.fields, s.node.call)), - ), - Then('it returns undefined (the object expression is at the tag slot, not fields)')((s) => - Effect.sync(() => { - expect(s.reason).toBeUndefined() - }) - ), - ), - ) - - scenario( - 'A non-string at the `Symbol.for` argument slot is position-load-bearing', - Gherkin.Do.pipe( - Given('`Symbol.for({})` — the argument is an object expression, not a string literal')( - 'node', - () => - Effect.sync(() => { - const node = objectExpression() - const call = symbolForCall(node) - return { node, call } - }), - ), - When('decideSchemaDeclarationIgnore examines the argument')( - 'reason', - (s) => Effect.sync(() => decideSchemaDeclarationIgnore(s.node.node, s.node.call)), - ), - Then('it returns undefined (not a string literal)')((s) => - Effect.sync(() => { - expect(s.reason).toBeUndefined() - }) - ), - ), - ) - - scenario( - 'A string beside a `Symbol.for` call is position-load-bearing', - Gherkin.Do.pipe( - Given('a string literal examined against a Symbol.for call that holds a different string')( - 'node', - () => - Effect.sync(() => { - const description = stringLiteral('MyBrand') - const other = stringLiteral('Other') - const call = symbolForCall(other) - return { description, call } - }), - ), - When('decideSchemaDeclarationIgnore examines the description string')( - 'reason', - (s) => Effect.sync(() => decideSchemaDeclarationIgnore(s.node.description, s.node.call)), - ), - Then('it returns undefined (this string is not the Symbol.for argument)')((s) => - Effect.sync(() => { - expect(s.reason).toBeUndefined() - }) - ), - ), - ) - - scenario( - 'A `Match.tag` call as behaviour input is not ignored', - Gherkin.Do.pipe( - Given('`Match.tag("a")`')('node', () => - Effect.sync(() => { - const tag = stringLiteral('a') - const call = callOf(memberOf('Match', 'tag'), [tag]) - return { tag, call } - })), - When('decideSchemaDeclarationIgnore examines the tag argument')( - 'reason', - (s) => Effect.sync(() => decideSchemaDeclarationIgnore(s.node.tag, s.node.call)), - ), - Then('it returns undefined (Match.tag is a runtime discriminator, not a declaration)')((s) => - Effect.sync(() => { - expect(s.reason).toBeUndefined() - }) - ), - ), - ) - - scenario( - 'An orphan node without a parent as behaviour input is not ignored', - Gherkin.Do.pipe( - Given('an identifier examined without a parent')('node', () => - Effect.sync(() => { - const node = identifier('x') - return { node } - })), - When('decideSchemaDeclarationIgnore runs with parent=undefined')( - 'reason', - (s) => Effect.sync(() => decideSchemaDeclarationIgnore(s.node.node, undefined)), - ), - Then('it returns undefined (no parent context means no decision)')((s) => - Effect.sync(() => { - expect(s.reason).toBeUndefined() - }) - ), - ), - ) - - scenario( - 'An arbitrary node and parent pair returns known reasons or undefined', - Gherkin.Do.pipe( - Given('a representative `Symbol.for("MyBrand")` call')('node', () => - Effect.sync(() => { - const node = stringLiteral('MyBrand') - const parent = symbolForCall(node) - return { node, parent } - })), - When('decideSchemaDeclarationIgnore examines the description argument')( - 'reason', - (s) => Effect.sync(() => decideSchemaDeclarationIgnore(s.node.node, s.node.parent)), - ), - Then('it returns SYMBOL_DESCRIPTION_IGNORED or undefined — never an arbitrary new reason')((s) => - Effect.sync(() => { - expect( - s.reason === undefined || s.reason === SYMBOL_DESCRIPTION_IGNORED || - s.reason === TAGGED_TAG_IGNORED || s.reason === TAGGED_FIELDS_IGNORED, - ).toBe(true) - }) - ), - ), - ) - - scenario( - 'An identifier key in a documentation object is ignored', - Gherkin.Do.pipe( - Given('`S.annotations({ identifier: "HexBytes" })`')('node', () => - Effect.sync(() => { - const object = objectOf([ - namedProperty('identifier', stringLiteral('HexBytes')), - ]) - const call = annotationsCall(object) - return { object, call } - })), - When('decideSchemaDeclarationIgnore examines the object and the value')('reasons', (s) => - Effect.sync(() => { - const objectReason = decideSchemaDeclarationIgnore(s.node.object, s.node.call) - const value = s.node.object.properties[0] - const valueReason = decideSchemaDeclarationIgnore(value?.value, value, s.node.object, s.node.call) - return { objectReason, valueReason } - })), - Then('the object is ignored and the value is ignored')((s) => - Effect.sync(() => { - expect(s.reasons.objectReason).toBe(ANNOTATION_OBJECT_IGNORED) - expect(s.reasons.valueReason).toBe(ANNOTATION_TEXT_IGNORED) - }) - ), - ), - ) - - scenario( - 'A quoted string-literal key in a documentation object is ignored', - Gherkin.Do.pipe( - Given('`S.annotations({ "identifier": "HexBytes" })`')('node', () => - Effect.sync(() => { - const object = objectOf([ - propertyOf(stringLiteral('identifier'), stringLiteral('HexBytes')), - ]) - const call = annotationsCall(object) - return { object, call } - })), - When('decideSchemaDeclarationIgnore examines the object')( - 'reason', - (s) => Effect.sync(() => decideSchemaDeclarationIgnore(s.node.object, s.node.call)), - ), - Then('it returns ANNOTATION_OBJECT_IGNORED')((s) => - Effect.sync(() => { - expect(s.reason).toBe(ANNOTATION_OBJECT_IGNORED) - }) - ), - ), - ) - - scenario( - 'Documentation beside a behaviour key keeps the object mutated', - Gherkin.Do.pipe( - Given('`S.annotations({ description: "x", arbitrary: () => ... })`')('node', () => - Effect.sync(() => { - const object = objectOf([ - namedProperty('description', stringLiteral('x')), - namedProperty('arbitrary', { type: 'ArrowFunctionExpression' }), - ]) - const call = annotationsCall(object) - return { object, call } - })), - When('decideSchemaDeclarationIgnore examines the object and the description value')( - 'reasons', - (s) => { - const objectReason = decideSchemaDeclarationIgnore(s.node.object, s.node.call) - const descriptionProperty = s.node.object.properties[0] - const valueReason = decideSchemaDeclarationIgnore( - descriptionProperty?.value, - descriptionProperty, - s.node.object, - s.node.call, - ) - return Effect.sync(() => ({ objectReason, valueReason })) - }, - ), - Then('the object is NOT ignored but the documentation value is ignored')((s) => - Effect.sync(() => { - expect(s.reasons.objectReason).toBeUndefined() - expect(s.reasons.valueReason).toBe(ANNOTATION_TEXT_IGNORED) - }) - ), - ), - ) - - scenario( - 'An object holding only a behaviour value is not ignored', - Gherkin.Do.pipe( - Given('`S.annotations({ arbitrary: () => ... })`')('node', () => - Effect.sync(() => { - const object = objectOf([ - namedProperty('arbitrary', { type: 'ArrowFunctionExpression' }), - ]) - const call = annotationsCall(object) - return { object, call } - })), - When('decideSchemaDeclarationIgnore examines the value')('reason', (s) => - Effect.sync(() => { - const property = s.node.object.properties[0] - return decideSchemaDeclarationIgnore(property?.value, property, s.node.object, s.node.call) - })), - Then('it returns undefined (behaviour values are never ignored)')((s) => - Effect.sync(() => { - expect(s.reason).toBeUndefined() - }) - ), - ), - ) - - scenario( - 'A behaviour key alone in the object is not ignored', - Gherkin.Do.pipe( - Given('`S.annotations({ arbitrary: () => ... })`')('node', () => - Effect.sync(() => { - const object = objectOf([ - namedProperty('arbitrary', { type: 'ArrowFunctionExpression' }), - ]) - const call = annotationsCall(object) - return { object, call } - })), - When('decideSchemaDeclarationIgnore examines the object')( - 'reason', - (s) => Effect.sync(() => decideSchemaDeclarationIgnore(s.node.object, s.node.call)), - ), - Then('it returns undefined (the object holds behaviour, not documentation)')((s) => - Effect.sync(() => { - expect(s.reason).toBeUndefined() - }) - ), - ), - ) - - scenario( - 'A computed documentation key is not ignored', - Gherkin.Do.pipe( - Given('`S.annotations({ [key]: "x" })`')('node', () => - Effect.sync(() => { - const key = identifier('identifier') - const value = stringLiteral('x') - const object = objectOf([propertyOf(key, value, true)]) - const call = annotationsCall(object) - return { object, call } - })), - When('decideSchemaDeclarationIgnore examines the object and the value')('reasons', (s) => - Effect.sync(() => { - const objectReason = decideSchemaDeclarationIgnore(s.node.object, s.node.call) - const property = s.node.object.properties[0] - const valueReason = decideSchemaDeclarationIgnore(property?.value, property, s.node.object, s.node.call) - return { objectReason, valueReason } - })), - Then('neither the object nor the value is ignored (the documentation key is not a string literal)')((s) => - Effect.sync(() => { - expect(s.reasons.objectReason).toBeUndefined() - expect(s.reasons.valueReason).toBeUndefined() - }) - ), - ), - ) - - scenario( - 'A documentation object outside an annotations call is not ignored', - Gherkin.Do.pipe( - Given('`S.filter({ identifier: "x" })`')('node', () => - Effect.sync(() => { - const object = objectOf([ - namedProperty('identifier', stringLiteral('x')), - ]) - const call = callOf(memberOf('S', 'filter'), [object]) - return { object, call } - })), - When('decideSchemaDeclarationIgnore examines the argument')( - 'reason', - (s) => Effect.sync(() => decideSchemaDeclarationIgnore(s.node.object, s.node.call)), - ), - Then('it returns undefined (the callee is `S.filter`, not `S.annotations`)')((s) => - Effect.sync(() => { - expect(s.reason).toBeUndefined() - }) - ), - ), - ) - - scenario( - 'A property key itself in a non-annotations position is not ignored', - Gherkin.Do.pipe( - Given('`S.annotations({ identifier: "HexBytes" })` — examining the key node')('node', () => - Effect.sync(() => { - const object = objectOf([ - namedProperty('identifier', stringLiteral('HexBytes')), - ]) - const call = annotationsCall(object) - return { object, call } - })), - When('decideSchemaDeclarationIgnore examines the property key')('reason', (s) => - Effect.sync(() => { - const property = s.node.object.properties[0] - return decideSchemaDeclarationIgnore(property?.key, property, s.node.object, s.node.call) - })), - Then('it returns undefined (the key slot is not the documentation value slot)')((s) => - Effect.sync(() => { - expect(s.reason).toBeUndefined() - }) - ), - ), - ) - - scenario( - 'A documentation value at a non-annotations call is not ignored', - Gherkin.Do.pipe( - Given('`S.filter({ identifier: "x" })` — the value sits at a non-annotations call')( - 'node', - () => - Effect.sync(() => { - const object = objectOf([ - namedProperty('identifier', stringLiteral('x')), - ]) - const call = callOf(memberOf('S', 'filter'), [object]) - return { object, call } - }), - ), - When('decideSchemaDeclarationIgnore examines the value')('reason', (s) => - Effect.sync(() => { - const property = s.node.object.properties[0] - return decideSchemaDeclarationIgnore(property?.value, property, s.node.object, s.node.call) - })), - Then('it returns undefined (no annotations call owns the slot)')((s) => - Effect.sync(() => { - expect(s.reason).toBeUndefined() - }) - ), - ), - ) - - scenario( - 'A documentation value without its enclosing call is not ignored', - Gherkin.Do.pipe( - Given('a documentation value examined without passing the annotations call expression')( - 'node', - () => - Effect.sync(() => { - const object = objectOf([ - namedProperty('identifier', stringLiteral('x')), - ]) - return { object } - }), - ), - When('decideSchemaDeclarationIgnore examines the value with ancestor=undefined')( - 'reason', - (s) => - Effect.sync(() => { - const property = s.node.object.properties[0] - return decideSchemaDeclarationIgnore(property?.value, property, s.node.object, undefined) - }), - ), - Then('it returns undefined (no call expression in scope means no annotations rule applies)')((s) => - Effect.sync(() => { - expect(s.reason).toBeUndefined() - }) - ), - ), - ) - - scenario( - 'A documentation object at the second argument slot is not ignored', - Gherkin.Do.pipe( - Given('`S.annotations("other", { identifier: "x" })` — documentation object at argument index 1')( - 'node', - () => - Effect.sync(() => { - const object = objectOf([ - namedProperty('identifier', stringLiteral('x')), - ]) - const value = stringLiteral('other') - const call = callOf(memberOf('S', 'annotations'), [value, object]) - return { object, call } - }), - ), - When('decideSchemaDeclarationIgnore examines the value at index 1')('reason', (s) => - Effect.sync(() => { - const property = s.node.object.properties[0] - return decideSchemaDeclarationIgnore(property?.value, property, s.node.object, s.node.call) - })), - Then('it returns undefined (the annotations rule only fires at argument index 0)')((s) => - Effect.sync(() => { - expect(s.reason).toBeUndefined() - }) - ), - ), - ) - }) diff --git a/packages/stryker-plugins/tests/effect-schema-ignorer/schema-declaration-ignore.integration.test.ts b/packages/stryker-plugins/tests/effect-schema-ignorer/schema-declaration-ignore.integration.test.ts deleted file mode 100644 index d2f16776c..000000000 --- a/packages/stryker-plugins/tests/effect-schema-ignorer/schema-declaration-ignore.integration.test.ts +++ /dev/null @@ -1,403 +0,0 @@ -import { Gherkin, Given, it, layer, makeFeature, Then, When } from '@systemfsoftware/effect-gherkin-spec' -import { Effect } from 'effect' -import { expect } from 'vitest' - -import { - ANNOTATION_OBJECT_IGNORED, - ANNOTATION_TEXT_IGNORED, - BRAND_NAME_IGNORED, - CLASS_ID_IGNORED, - decideSchemaDeclarationIgnore, - OPTIONAL_DEFAULT_IGNORED, -} from '@systemfsoftware/stryker-plugins/effect-schema-ignorer' - -import { - annotationsCall, - bareFactoryCall, - brandCall, - callOf, - classCall, - memberOf, - namedProperty, - objectExpression, - objectOf, - stringLiteral, -} from '../__fixtures__/EffectSchemaAst.fixtures.js' - -const Feature = makeFeature({ it, layer }) - -Feature('Effect Schema declarations — ignored mutants on tags, brands, optionals, and annotation objects') - .body(({ scenario }) => { - scenario( - 'A bare identifier factory keeps its tagged arguments live', - Gherkin.Do.pipe( - Given('a bare `TaggedClass("tag", {})` call expression (no `Schema.` prefix)')('node', () => - Effect.sync(() => { - const tag = stringLiteral('someTag') - const fields = objectExpression() - return { tag, fields, call: bareFactoryCall('TaggedClass', tag, fields) } - })), - When('decideSchemaDeclarationIgnore examines the tag and fields')('results', (s) => - Effect.sync(() => { - const tag = decideSchemaDeclarationIgnore(s.node.tag, s.node.call) - const fields = decideSchemaDeclarationIgnore(s.node.fields, s.node.call) - return { tag, fields } - })), - Then('both positions return undefined (only `Schema.TaggedClass` qualifies)')((s) => - Effect.sync(() => { - expect(s.results.tag).toBeUndefined() - expect(s.results.fields).toBeUndefined() - }) - ), - ), - ) - - scenario( - 'A description string in a Symbol dot keyFor call stays live', - Gherkin.Do.pipe( - Given('a `Symbol.keyFor("desc")` call')('node', () => - Effect.sync(() => { - const description = stringLiteral('desc') - return { description, call: callOf(memberOf('Symbol', 'keyFor'), [description]) } - })), - When('decideSchemaDeclarationIgnore examines the argument')( - 'result', - (s) => Effect.sync(() => decideSchemaDeclarationIgnore(s.node.description, s.node.call)), - ), - Then('it returns undefined (member name is not `for`)')((s) => - Effect.sync(() => { - expect(s.result).toBeUndefined() - }) - ), - ), - ) - - scenario( - 'A description string in an Object dot for call stays live', - Gherkin.Do.pipe( - Given('a `Object.for("desc")` call')('node', () => - Effect.sync(() => { - const description = stringLiteral('desc') - return { description, call: callOf(memberOf('Object', 'for'), [description]) } - })), - When('decideSchemaDeclarationIgnore examines the argument')( - 'result', - (s) => Effect.sync(() => decideSchemaDeclarationIgnore(s.node.description, s.node.call)), - ), - Then('it returns undefined (object is not `Symbol`)')((s) => - Effect.sync(() => { - expect(s.result).toBeUndefined() - }) - ), - ), - ) - - scenario( - 'An arrow function default for an optional field is ignored', - Gherkin.Do.pipe( - Given('`S.optionalWith(S.String, () => default)`')('node', () => - Effect.sync(() => { - const defaultFn = { type: 'ArrowFunctionExpression' } - const schemaArg = memberOf('S', 'String') - return { defaultFn, call: callOf(memberOf('S', 'optionalWith'), [schemaArg, defaultFn]) } - })), - When('decideSchemaDeclarationIgnore examines the default arrow function')( - 'result', - (s) => Effect.sync(() => decideSchemaDeclarationIgnore(s.node.defaultFn, s.node.call)), - ), - Then('it returns OPTIONAL_DEFAULT_IGNORED')((s) => - Effect.sync(() => { - expect(s.result).toBe(OPTIONAL_DEFAULT_IGNORED) - }) - ), - ), - ) - - scenario( - 'A string literal default for an optional field stays live', - Gherkin.Do.pipe( - Given('`S.optionalWith(S.String, "x")`')('node', () => - Effect.sync(() => { - const notFn = stringLiteral('x') - const schemaArg = memberOf('S', 'String') - return { notFn, call: callOf(memberOf('S', 'optionalWith'), [schemaArg, notFn]) } - })), - When('decideSchemaDeclarationIgnore examines the string-literal argument')( - 'result', - (s) => Effect.sync(() => decideSchemaDeclarationIgnore(s.node.notFn, s.node.call)), - ), - Then('it returns undefined (not an arrow)')((s) => - Effect.sync(() => { - expect(s.result).toBeUndefined() - }) - ), - ), - ) - - scenario( - 'An arrow function default outside optionalWith stays live', - Gherkin.Do.pipe( - Given('`S.optional(S.String, () => default)` — wrong callee')('node', () => - Effect.sync(() => { - const defaultFn = { type: 'ArrowFunctionExpression' } - const schemaArg = memberOf('S', 'String') - return { defaultFn, call: callOf(memberOf('S', 'optional'), [schemaArg, defaultFn]) } - })), - When('decideSchemaDeclarationIgnore examines the default arrow function')( - 'result', - (s) => Effect.sync(() => decideSchemaDeclarationIgnore(s.node.defaultFn, s.node.call)), - ), - Then('it returns undefined (callee is `optional`, not `optionalWith`)')((s) => - Effect.sync(() => { - expect(s.result).toBeUndefined() - }) - ), - ), - ) - - scenario( - 'An annotations object holding only documentation is ignored in full', - Gherkin.Do.pipe( - Given('an `S.annotations({ identifier, description, title })` call with documentation only')( - 'node', - () => - Effect.sync(() => { - const documentation = objectOf([ - namedProperty('identifier', stringLiteral('HexBytes')), - namedProperty('description', { - type: 'Literal', - value: 'Uint8Array encoded as a lowercase hex string', - }), - namedProperty('title', stringLiteral('Hex Bytes')), - ]) - const call = annotationsCall(documentation) - return { documentation, call } - }), - ), - When('decideSchemaDeclarationIgnore examines the object and each property value')( - 'results', - (s) => { - const objectReason = decideSchemaDeclarationIgnore(s.node.documentation, s.node.call) - const valueReasons = s.node.documentation.properties.map((property) => - decideSchemaDeclarationIgnore(property.value, property, s.node.documentation, s.node.call) - ) - return Effect.sync(() => ({ objectReason, valueReasons })) - }, - ), - Then('the object is ignored and every property value is ignored')((s) => - Effect.sync(() => { - expect(s.results.objectReason).toBe(ANNOTATION_OBJECT_IGNORED) - for (const reason of s.results.valueReasons) { - expect(reason).toBe(ANNOTATION_TEXT_IGNORED) - } - }) - ), - ), - ) - - scenario( - 'An annotations object mixing a generator with documentation keeps the generator live', - Gherkin.Do.pipe( - Given('`S.annotations({ arbitrary, identifier })` — one behaviour key plus a documentation key')( - 'node', - () => - Effect.sync(() => { - const mixed = objectOf([ - namedProperty('arbitrary', { type: 'ArrowFunctionExpression' }), - namedProperty('identifier', stringLiteral('HexStringInput')), - ]) - const call = annotationsCall(mixed) - const properties = mixed.properties - return { mixed, call, properties } - }), - ), - When('decideSchemaDeclarationIgnore examines the object and each property')('results', (s) => { - const objectReason = decideSchemaDeclarationIgnore(s.node.mixed, s.node.call) - const generatorReason = decideSchemaDeclarationIgnore( - s.node.properties[0]?.value, - s.node.properties[0], - s.node.mixed, - s.node.call, - ) - const documentationReason = decideSchemaDeclarationIgnore( - s.node.properties[1]?.value, - s.node.properties[1], - s.node.mixed, - s.node.call, - ) - return Effect.sync(() => ({ objectReason, generatorReason, documentationReason })) - }), - Then('the object keeps its mutants, the generator keeps its mutants, the documentation value is ignored')( - (s) => - Effect.sync(() => { - expect(s.results.objectReason).toBeUndefined() - expect(s.results.generatorReason).toBeUndefined() - expect(s.results.documentationReason).toBe(ANNOTATION_TEXT_IGNORED) - }), - ), - ), - ) - - scenario( - 'An annotations object holding only a generator stays live', - Gherkin.Do.pipe( - Given('`S.annotations({ arbitrary })` — only a generator')('node', () => - Effect.sync(() => { - const generator = objectOf([namedProperty('arbitrary', { type: 'ArrowFunctionExpression' })]) - return { generator, call: annotationsCall(generator) } - })), - When('decideSchemaDeclarationIgnore examines the object')( - 'result', - (s) => Effect.sync(() => decideSchemaDeclarationIgnore(s.node.generator, s.node.call)), - ), - Then('it returns undefined (the arbitrary drives a property-test generator)')((s) => - Effect.sync(() => { - expect(s.result).toBeUndefined() - }) - ), - ), - ) - - scenario( - 'An empty annotations object stays live', - Gherkin.Do.pipe( - Given('`S.annotations({})` — empty documentation object')('node', () => - Effect.sync(() => { - const empty = objectOf([]) - return { empty, call: annotationsCall(empty) } - })), - When('decideSchemaDeclarationIgnore examines the empty object')( - 'result', - (s) => Effect.sync(() => decideSchemaDeclarationIgnore(s.node.empty, s.node.call)), - ), - Then('it returns undefined (an empty object has no documentation entries to ignore)')((s) => - Effect.sync(() => { - expect(s.result).toBeUndefined() - }) - ), - ), - ) - - scenario( - 'A documentation object in the wrong argument position stays live', - Gherkin.Do.pipe( - Given('a documentation object passed to a non-annotations call')('node', () => - Effect.sync(() => { - const documentation = objectOf([namedProperty('title', stringLiteral('Hex Bytes'))]) - const call = callOf(memberOf('S', 'annotations'), [ - stringLiteral('other'), - documentation, - ]) - return { documentation, call } - })), - When('decideSchemaDeclarationIgnore examines the second argument')( - 'result', - (s) => Effect.sync(() => decideSchemaDeclarationIgnore(s.node.documentation, s.node.call)), - ), - Then('it returns undefined (the documentation object is at the wrong argument slot)')((s) => - Effect.sync(() => { - expect(s.result).toBeUndefined() - }) - ), - ), - ) - - scenario( - 'A documentation object passed to a bare annotations call stays live', - Gherkin.Do.pipe( - Given('a call to a bare `annotations` identifier (not `S.annotations`)')('node', () => - Effect.sync(() => { - const documentation = objectOf([namedProperty('title', stringLiteral('Hex Bytes'))]) - const call = callOf({ type: 'Identifier', name: 'annotations' }, [documentation]) - return { documentation, call } - })), - When('decideSchemaDeclarationIgnore examines the argument')( - 'result', - (s) => Effect.sync(() => decideSchemaDeclarationIgnore(s.node.documentation, s.node.call)), - ), - Then('it returns undefined (callee is not `S.annotations`)')((s) => - Effect.sync(() => { - expect(s.result).toBeUndefined() - }) - ), - ), - ) - - scenario( - 'A class identifier is ignored while its fields object stays live', - Gherkin.Do.pipe( - Given('a `Schema.Class("ChildPolicyConfig")({ … })` call, whose id rides the inner call')( - 'node', - () => - Effect.sync(() => { - const id = stringLiteral('ChildPolicyConfig') - const fields = objectExpression() - const outer = classCall(id, fields) - return { id, fields, outer, inner: outer.callee } - }), - ), - When('decideSchemaDeclarationIgnore examines the identifier and the fields object')( - 'results', - (s) => - Effect.sync(() => ({ - id: decideSchemaDeclarationIgnore(s.node.id, s.node.inner), - fields: decideSchemaDeclarationIgnore(s.node.fields, s.node.outer), - })), - ), - Then('the id is ignored and the fields object is not: a fields subtree carries accepted value sets')((s) => - Effect.sync(() => { - expect(s.results.id).toBe(CLASS_ID_IGNORED) - expect(s.results.fields).toBeUndefined() - }) - ), - ), - ) - - scenario( - 'A brand name string is ignored', - Gherkin.Do.pipe( - Given('an `S.brand("MaxChildren")` call')('node', () => - Effect.sync(() => { - const name = stringLiteral('MaxChildren') - return { name, call: brandCall(name) } - })), - When('decideSchemaDeclarationIgnore examines the brand name')( - 'result', - (s) => Effect.sync(() => decideSchemaDeclarationIgnore(s.node.name, s.node.call)), - ), - Then('it is ignored: the brand name is identity data, like a Symbol.for description')((s) => - Effect.sync(() => { - expect(s.result).toBe(BRAND_NAME_IGNORED) - }) - ), - ), - ) - - scenario( - 'Accepted literal values inside class fields stay live', - Gherkin.Do.pipe( - Given('a `Schema.Literal("permanent", "transient")` inside a `Schema.Class` fields object')( - 'node', - () => - Effect.sync(() => { - const member = stringLiteral('permanent') - const literal = callOf(memberOf('Schema', 'Literal'), [member, { - type: 'Literal', - value: 'transient', - }]) - const fields = objectOf([namedProperty('restart', literal)]) - return { member, literal, outer: classCall(stringLiteral('C'), fields) } - }), - ), - When('decideSchemaDeclarationIgnore examines one accepted literal')( - 'result', - (s) => Effect.sync(() => decideSchemaDeclarationIgnore(s.node.member, s.node.literal)), - ), - Then('it is NOT ignored: which values decode is behaviour, not declaration identity')((s) => - Effect.sync(() => { - expect(s.result).toBeUndefined() - }) - ), - ), - ) - }) diff --git a/packages/stryker-plugins/tests/in-source-test-ignorer/in-source-test-ignore.integration.test.ts b/packages/stryker-plugins/tests/in-source-test-ignorer/in-source-test-ignore.integration.test.ts deleted file mode 100644 index 8a46dc66b..000000000 --- a/packages/stryker-plugins/tests/in-source-test-ignorer/in-source-test-ignore.integration.test.ts +++ /dev/null @@ -1,211 +0,0 @@ -import { Gherkin, Given, it, layer, makeFeature, Then, When } from '@systemfsoftware/effect-gherkin-spec' -import { Effect } from 'effect' -import { expect } from 'vitest' - -import { - decideInSourceTestIgnore, - IN_SOURCE_TEST_IGNORED, - isInSourceTestGuard, -} from '@systemfsoftware/stryker-plugins/in-source-test-ignorer' - -import { binaryOf, guardOf, identifier, importMetaMember, metaOf } from '../__fixtures__/InSourceTestAst.fixtures.js' - -const Feature = makeFeature({ it, layer }) - -Feature('In-source Vitest test guard — the if-statement shape and the ancestor walk') - .body(({ scenario }) => { - scenario( - 'A bare vitest flag as the guard condition matches', - Gherkin.Do.pipe( - Given('an if-statement whose test is bare `import.meta.vitest`')( - 'node', - () => Effect.sync(() => guardOf(importMetaMember('vitest'))), - ), - When('isInSourceTestGuard runs')('result', (s) => Effect.sync(() => isInSourceTestGuard(s.node))), - Then('it returns true')((s) => - Effect.sync(() => { - expect(s.result).toBe(true) - }) - ), - ), - ) - - scenario( - 'A vitest flag on the left side of a comparison matches', - Gherkin.Do.pipe( - Given('an if-statement whose test is `import.meta.vitest === undefined`')( - 'node', - () => Effect.sync(() => guardOf(binaryOf(importMetaMember('vitest'), identifier('undefined')))), - ), - When('isInSourceTestGuard runs')('result', (s) => Effect.sync(() => isInSourceTestGuard(s.node))), - Then('it returns true')((s) => - Effect.sync(() => { - expect(s.result).toBe(true) - }) - ), - ), - ) - - scenario( - 'A vitest flag on the right side of a comparison matches', - Gherkin.Do.pipe( - Given('an if-statement whose test is `undefined === import.meta.vitest`')( - 'node', - () => Effect.sync(() => guardOf(binaryOf(identifier('undefined'), importMetaMember('vitest')))), - ), - When('isInSourceTestGuard runs')('result', (s) => Effect.sync(() => isInSourceTestGuard(s.node))), - Then('it returns true')((s) => - Effect.sync(() => { - expect(s.result).toBe(true) - }) - ), - ), - ) - - scenario( - 'A flag for a different meta property does not match', - Gherkin.Do.pipe( - Given('an if-statement whose test is `import.meta.env`')( - 'node', - () => Effect.sync(() => guardOf(importMetaMember('env'))), - ), - When('isInSourceTestGuard runs')('result', (s) => Effect.sync(() => isInSourceTestGuard(s.node))), - Then('it returns false')((s) => - Effect.sync(() => { - expect(s.result).toBe(false) - }) - ), - ), - ) - - scenario( - 'A vitest flag on a non-import meta does not match', - Gherkin.Do.pipe( - Given('an if-statement whose test is `require.meta.vitest`')('node', () => - Effect.sync(() => - guardOf({ - type: 'MemberExpression', - object: metaOf('require', 'meta'), - property: identifier('vitest'), - }) - )), - When('isInSourceTestGuard runs')('result', (s) => Effect.sync(() => isInSourceTestGuard(s.node))), - Then('it returns false')((s) => - Effect.sync(() => { - expect(s.result).toBe(false) - }) - ), - ), - ) - - scenario( - 'A vitest property on a non-meta object does not match', - Gherkin.Do.pipe( - Given('an if-statement whose test is `import.cache.vitest`')('node', () => - Effect.sync(() => - guardOf({ - type: 'MemberExpression', - object: metaOf('import', 'cache'), - property: identifier('vitest'), - }) - )), - When('isInSourceTestGuard runs')('result', (s) => Effect.sync(() => isInSourceTestGuard(s.node))), - Then('it returns false')((s) => - Effect.sync(() => { - expect(s.result).toBe(false) - }) - ), - ), - ) - - scenario( - 'A bare vitest expression without an if statement does not match', - Gherkin.Do.pipe( - Given('a bare `import.meta.vitest` member expression')( - 'node', - () => Effect.sync(() => importMetaMember('vitest')), - ), - When('isInSourceTestGuard runs')('result', (s) => Effect.sync(() => isInSourceTestGuard(s.node))), - Then('it returns false')((s) => - Effect.sync(() => { - expect(s.result).toBe(false) - }) - ), - ), - ) - - scenario( - 'A plain comparison without a vitest flag does not match', - Gherkin.Do.pipe( - Given('an if-statement whose test is a plain binary `a === b`')( - 'node', - () => Effect.sync(() => guardOf(binaryOf(identifier('a'), identifier('b')))), - ), - When('isInSourceTestGuard runs')('result', (s) => Effect.sync(() => isInSourceTestGuard(s.node))), - Then('it returns false')((s) => - Effect.sync(() => { - expect(s.result).toBe(false) - }) - ), - ), - ) - - scenario( - 'A mutant guarded by an ancestor vitest check is ignored', - Gherkin.Do.pipe( - Given('ancestors including an `if (import.meta.vitest)` guard deeper in the chain')( - 'ancestors', - () => - Effect.sync(() => [ - identifier('x'), - binaryOf(identifier('a'), identifier('b')), - guardOf(importMetaMember('vitest')), - ]), - ), - When('decideInSourceTestIgnore walks the chain')( - 'reason', - (s) => Effect.sync(() => decideInSourceTestIgnore(s.ancestors)), - ), - Then('it returns IN_SOURCE_TEST_IGNORED')((s) => - Effect.sync(() => { - expect(s.reason).toBe(IN_SOURCE_TEST_IGNORED) - }) - ), - ), - ) - - scenario( - 'A mutant with no guard in its ancestors stays live', - Gherkin.Do.pipe( - Given('ancestors none of which is a vitest guard')( - 'ancestors', - () => Effect.sync(() => [identifier('x'), guardOf(importMetaMember('env'))]), - ), - When('decideInSourceTestIgnore walks the chain')( - 'reason', - (s) => Effect.sync(() => decideInSourceTestIgnore(s.ancestors)), - ), - Then('it returns undefined')((s) => - Effect.sync(() => { - expect(s.reason).toBeUndefined() - }) - ), - ), - ) - - scenario( - 'A mutant with no ancestors at all stays live', - Gherkin.Do.pipe( - Given('an empty ancestor chain')('ancestors', () => Effect.sync(() => [] as readonly unknown[])), - When('decideInSourceTestIgnore walks the chain')( - 'reason', - (s) => Effect.sync(() => decideInSourceTestIgnore(s.ancestors)), - ), - Then('it returns undefined')((s) => - Effect.sync(() => { - expect(s.reason).toBeUndefined() - }) - ), - ), - ) - }) diff --git a/packages/stryker-plugins/tests/workflow-make-ignorer/workflow-make-boundary.integration.test.ts b/packages/stryker-plugins/tests/workflow-make-ignorer/workflow-make-boundary.integration.test.ts deleted file mode 100644 index bb0bae27d..000000000 --- a/packages/stryker-plugins/tests/workflow-make-ignorer/workflow-make-boundary.integration.test.ts +++ /dev/null @@ -1,575 +0,0 @@ -import { Gherkin, Given, it, layer, makeFeature, Then, When } from '@systemfsoftware/effect-gherkin-spec' -import { Effect } from 'effect' -import { expect } from 'vitest' - -import { strykerPlugins as composedPlugins } from '@systemfsoftware/stryker-plugins' -import { - decideSchemaDeclarationIgnore, - TAGGED_TAG_IGNORED, -} from '@systemfsoftware/stryker-plugins/effect-schema-ignorer' -import { - decideWorkflowMakeBoundaryIgnore, - NOT_INSIDE_WORKFLOW_MAKE, - strykerPlugins, -} from '@systemfsoftware/stryker-plugins/workflow-make-ignorer' - -import { taggedCall } from '../__fixtures__/EffectSchemaAst.fixtures.js' -import { - callOf, - classDeclarationOf, - constBindingOf, - identifier, - makeBodyOf, - memberOf, - programOf, - stringLiteral, - unrelatedImport, - workflowAliasedImport, - workflowAndThenCallOf, - workflowMakeCallOf, - workflowMakeCallOfTwo, - workflowNamedImport, - workflowNamespaceImport, - workflowTotalCallOf, -} from '../__fixtures__/WorkflowMakeAst.fixtures.js' - -const Feature = makeFeature({ it, layer }) - -const makeFixture = (mutant: unknown, ancestors: readonly unknown[]) => ({ mutant, ancestors }) - -Feature('Workflow.make boundary — the inverted mutation-population selector') - .body(({ scenario }) => { - scenario( - 'A mutant inside a Workflow.make body stays live', - Gherkin.Do.pipe( - Given('a file whose `Workflow.make(...)` argument body holds the mutant')('fixture', () => - Effect.sync(() => { - const mutant = stringLiteral('decide') - const body = makeBodyOf(mutant) - const call = workflowMakeCallOf(body) - const program = programOf([workflowNamedImport(), call]) - return makeFixture(mutant, [body, call, program]) - })), - When('the boundary decision runs on the mutant and its ancestor chain')( - 'reason', - (s) => Effect.sync(() => decideWorkflowMakeBoundaryIgnore(s.fixture.mutant, s.fixture.ancestors)), - ), - Then('it returns undefined — the mutant stays live')((s) => - Effect.sync(() => { - expect(s.reason).toBeUndefined() - }) - ), - ), - ) - - scenario( - 'A mutant nested several levels inside a make body stays live', - Gherkin.Do.pipe( - Given('a mutant several expression levels below a `Workflow.make(...)` argument')( - 'fixture', - () => - Effect.sync(() => { - const mutant = identifier('command') - const callInside = callOf(memberOf('Result', 'succeed'), [mutant]) - const body = makeBodyOf(callInside) - const call = workflowMakeCallOf(body) - const program = programOf([workflowNamedImport(), call]) - return makeFixture(mutant, [callInside, body, call, program]) - }), - ), - When('the boundary decision runs on that mutant')( - 'reason', - (s) => Effect.sync(() => decideWorkflowMakeBoundaryIgnore(s.fixture.mutant, s.fixture.ancestors)), - ), - Then('it returns undefined')((s) => - Effect.sync(() => { - expect(s.reason).toBeUndefined() - }) - ), - ), - ) - - scenario( - 'A mutant at module level outside any make body is ignored', - Gherkin.Do.pipe( - Given('a file importing Workflow whose module-level body holds the mutant')('fixture', () => - Effect.sync(() => { - const mutant = stringLiteral('admit') - const program = programOf([workflowNamedImport(), mutant]) - return makeFixture(mutant, [program]) - })), - When('the boundary decision runs on that mutant')( - 'reason', - (s) => Effect.sync(() => decideWorkflowMakeBoundaryIgnore(s.fixture.mutant, s.fixture.ancestors)), - ), - Then('it returns NOT_INSIDE_WORKFLOW_MAKE')((s) => - Effect.sync(() => { - expect(s.reason).toBe(NOT_INSIDE_WORKFLOW_MAKE) - }) - ), - ), - ) - - scenario( - 'A mutant in a file that imports no workflow is ignored', - Gherkin.Do.pipe( - Given('a file with no effect-cell-types import at all')('fixture', () => - Effect.sync(() => { - const mutant = stringLiteral('plug') - const program = programOf([unrelatedImport('../local.js', 'Workflow'), mutant]) - return makeFixture(mutant, [program]) - })), - When('the boundary decision runs on that mutant')( - 'reason', - (s) => Effect.sync(() => decideWorkflowMakeBoundaryIgnore(s.fixture.mutant, s.fixture.ancestors)), - ), - Then('it returns NOT_INSIDE_WORKFLOW_MAKE')((s) => - Effect.sync(() => { - expect(s.reason).toBe(NOT_INSIDE_WORKFLOW_MAKE) - }) - ), - ), - ) - - scenario( - 'A mutant in a make call bound to a local workflow is ignored', - Gherkin.Do.pipe( - Given('a `Workflow.make(...)` call whose `Workflow` binding comes from a local module')( - 'fixture', - () => - Effect.sync(() => { - const mutant = stringLiteral('local') - const body = makeBodyOf(mutant) - const call = workflowMakeCallOf(body) - const program = programOf([unrelatedImport('./local-workflow.js', 'Workflow'), call]) - return makeFixture(mutant, [body, call, program]) - }), - ), - When('the boundary decision runs on that mutant')( - 'reason', - (s) => Effect.sync(() => decideWorkflowMakeBoundaryIgnore(s.fixture.mutant, s.fixture.ancestors)), - ), - Then('it returns NOT_INSIDE_WORKFLOW_MAKE — only the cell-types value opens a boundary')((s) => - Effect.sync(() => { - expect(s.reason).toBe(NOT_INSIDE_WORKFLOW_MAKE) - }) - ), - ), - ) - - scenario( - 'A mutant in a function referenced by a make call stays live', - Gherkin.Do.pipe( - Given('a `Workflow.make(...)` call whose argument names a same-file function holding the mutant')( - 'fixture', - () => - Effect.sync(() => { - const mutant = stringLiteral('decide') - const body = makeBodyOf(mutant) - const decision = constBindingOf('decision', body) - const call = workflowMakeCallOf(identifier('decision')) - const program = programOf([workflowNamedImport(), decision, call]) - // The make call is a sibling statement, not an ancestor: the mutant's - // chain runs mutant -> body -> binding -> program, and the resolution - // must keep the body inside the population anyway. - return makeFixture(mutant, [mutant, body, decision, program]) - }), - ), - When('the boundary decision runs on that mutant')( - 'reason', - (s) => Effect.sync(() => decideWorkflowMakeBoundaryIgnore(s.fixture.mutant, s.fixture.ancestors)), - ), - Then('it returns undefined — the referenced function body stays inside the mutation population')((s) => - Effect.sync(() => { - expect(s.reason).toBeUndefined() - }) - ), - ), - ) - - scenario( - 'A mutant in the decider function of a two-argument make stays live', - Gherkin.Do.pipe( - Given( - 'a two-argument `Workflow.make(Command, decide)` whose second argument names the mutant-bearing function', - )( - 'fixture', - () => - Effect.sync(() => { - const mutant = stringLiteral('decide') - const body = makeBodyOf(mutant) - const decision = constBindingOf('decision', body) - const command = classDeclarationOf('Cmd') - const call = workflowMakeCallOfTwo(identifier('Cmd'), identifier('decision')) - const program = programOf([workflowNamedImport(), command, decision, call]) - // Slot 0 resolves to a class, never a function. A resolver pinned - // to that slot drops this body from the population and every - // mutant in the decision silently stops being tested. - return makeFixture(mutant, [mutant, body, decision, program]) - }), - ), - When('the boundary decision runs on that mutant')( - 'reason', - (s) => Effect.sync(() => decideWorkflowMakeBoundaryIgnore(s.fixture.mutant, s.fixture.ancestors)), - ), - Then('it returns undefined — the referenced decider stays inside the mutation population')((s) => - Effect.sync(() => { - expect(s.reason).toBeUndefined() - }) - ), - ), - ) - - scenario( - 'A mutant inline in the second argument of a two-argument make stays live', - Gherkin.Do.pipe( - Given('a two-argument `Workflow.make(Command, (c) => ...)` holding the mutant inline')( - 'fixture', - () => - Effect.sync(() => { - const mutant = stringLiteral('inline') - const body = makeBodyOf(mutant) - const call = workflowMakeCallOfTwo(identifier('Cmd'), body) - const program = programOf([workflowNamedImport(), classDeclarationOf('Cmd'), call]) - return makeFixture(mutant, [body, call, program]) - }), - ), - When('the boundary decision runs on that mutant')( - 'reason', - (s) => Effect.sync(() => decideWorkflowMakeBoundaryIgnore(s.fixture.mutant, s.fixture.ancestors)), - ), - Then('it returns undefined — an argument-slot ancestor is slot-agnostic')((s) => - Effect.sync(() => { - expect(s.reason).toBeUndefined() - }) - ), - ), - ) - - scenario( - 'A mutant naming a missing function is ignored', - Gherkin.Do.pipe( - Given('a make call naming an identifier that resolves to no function in the file')( - 'fixture', - () => - Effect.sync(() => { - const mutant = stringLiteral('admit') - const call = workflowMakeCallOf(identifier('decideElsewhere')) - const program = programOf([workflowNamedImport(), call]) - return makeFixture(mutant, [mutant, program]) - }), - ), - When('the boundary decision runs on that mutant')( - 'reason', - (s) => Effect.sync(() => decideWorkflowMakeBoundaryIgnore(s.fixture.mutant, s.fixture.ancestors)), - ), - Then('it returns NOT_INSIDE_WORKFLOW_MAKE')((s) => - Effect.sync(() => { - expect(s.reason).toBe(NOT_INSIDE_WORKFLOW_MAKE) - }) - ), - ), - ) - - scenario( - 'A mutant in the second of two make calls stays live', - Gherkin.Do.pipe( - Given('a file with two `Workflow.make` calls and a mutant inside the second body')( - 'fixture', - () => - Effect.sync(() => { - const firstBody = makeBodyOf(identifier('first')) - const mutant = stringLiteral('second') - const secondBody = makeBodyOf(mutant) - const secondCall = workflowMakeCallOf(secondBody) - const program = programOf([workflowNamedImport(), workflowMakeCallOf(firstBody), secondCall]) - return makeFixture(mutant, [secondBody, secondCall, program]) - }), - ), - When('the boundary decision runs on that mutant')( - 'reason', - (s) => Effect.sync(() => decideWorkflowMakeBoundaryIgnore(s.fixture.mutant, s.fixture.ancestors)), - ), - Then('it returns undefined — every make boundary holds mutation live')((s) => - Effect.sync(() => { - expect(s.reason).toBeUndefined() - }) - ), - ), - ) - - scenario( - 'A mutant inside a nested make stays live', - Gherkin.Do.pipe( - Given('a `Workflow.make` call inside another make body, with the mutant in the inner body')( - 'fixture', - () => - Effect.sync(() => { - const mutant = stringLiteral('inner') - const innerBody = makeBodyOf(mutant) - const innerCall = workflowMakeCallOf(innerBody) - const outerBody = makeBodyOf(innerCall) - const outerCall = workflowMakeCallOf(outerBody) - const program = programOf([workflowNamedImport(), outerCall]) - return makeFixture(mutant, [innerBody, innerCall, outerBody, outerCall, program]) - }), - ), - When('the boundary decision runs on that mutant')( - 'reason', - (s) => Effect.sync(() => decideWorkflowMakeBoundaryIgnore(s.fixture.mutant, s.fixture.ancestors)), - ), - Then('it returns undefined — the inner make argument is inside a boundary too')((s) => - Effect.sync(() => { - expect(s.reason).toBeUndefined() - }) - ), - ), - ) - scenario( - 'A mutant inside a make called through a namespace import stays live', - Gherkin.Do.pipe( - Given('`import * as Workflow` with the mutant inside the make argument')('fixture', () => - Effect.sync(() => { - const mutant = stringLiteral('namespace') - const body = makeBodyOf(mutant) - const call = workflowMakeCallOf(body) - const program = programOf([workflowNamespaceImport('Workflow'), call]) - return makeFixture(mutant, [body, call, program]) - })), - When('the boundary decision runs on that mutant')( - 'reason', - (s) => Effect.sync(() => decideWorkflowMakeBoundaryIgnore(s.fixture.mutant, s.fixture.ancestors)), - ), - Then('it returns undefined')((s) => - Effect.sync(() => { - expect(s.reason).toBeUndefined() - }) - ), - ), - ) - - scenario( - 'A mutant inside a make called through an aliased import stays live', - Gherkin.Do.pipe( - Given('`import { Workflow as W }` with the mutant inside `W.make(...)`')('fixture', () => - Effect.sync(() => { - const mutant = stringLiteral('aliased') - const body = makeBodyOf(mutant) - const call = workflowMakeCallOf(body, 'W') - const program = programOf([workflowAliasedImport('W'), call]) - return makeFixture(mutant, [body, call, program]) - })), - When('the boundary decision runs on that mutant')( - 'reason', - (s) => Effect.sync(() => decideWorkflowMakeBoundaryIgnore(s.fixture.mutant, s.fixture.ancestors)), - ), - Then('it returns undefined')((s) => - Effect.sync(() => { - expect(s.reason).toBeUndefined() - }) - ), - ), - ) - - scenario( - 'A mutant inside a Workflow.total body stays live like the make-body equivalent', - Gherkin.Do.pipe( - Given('a `Workflow.total(...)` decider body holding the mutant')('fixture', () => - Effect.sync(() => { - const mutant = stringLiteral('total') - const body = makeBodyOf(mutant) - const call = workflowTotalCallOf(body) - const program = programOf([workflowNamedImport(), call]) - return makeFixture(mutant, [body, call, program]) - })), - When('the boundary decision runs on that mutant')( - 'reason', - (s) => Effect.sync(() => decideWorkflowMakeBoundaryIgnore(s.fixture.mutant, s.fixture.ancestors)), - ), - Then('it returns undefined — a total body is a decider body exactly as a make body is')((s) => - Effect.sync(() => { - expect(s.reason).toBeUndefined() - }) - ), - ), - ) - - scenario( - 'A mutant inside a total called through a namespace import stays live', - Gherkin.Do.pipe( - Given('`import * as Workflow` with the mutant inside the total argument')('fixture', () => - Effect.sync(() => { - const mutant = stringLiteral('namespace') - const body = makeBodyOf(mutant) - const call = workflowTotalCallOf(body) - const program = programOf([workflowNamespaceImport('Workflow'), call]) - return makeFixture(mutant, [body, call, program]) - })), - When('the boundary decision runs on that mutant')( - 'reason', - (s) => Effect.sync(() => decideWorkflowMakeBoundaryIgnore(s.fixture.mutant, s.fixture.ancestors)), - ), - Then('it returns undefined')((s) => - Effect.sync(() => { - expect(s.reason).toBeUndefined() - }) - ), - ), - ) - - scenario( - 'A mutant inside a total called through an aliased import stays live', - Gherkin.Do.pipe( - Given('`import { Workflow as W }` with the mutant inside `W.total(...)`')('fixture', () => - Effect.sync(() => { - const mutant = stringLiteral('aliased') - const body = makeBodyOf(mutant) - const call = workflowTotalCallOf(body, 'W') - const program = programOf([workflowAliasedImport('W'), call]) - return makeFixture(mutant, [body, call, program]) - })), - When('the boundary decision runs on that mutant')( - 'reason', - (s) => Effect.sync(() => decideWorkflowMakeBoundaryIgnore(s.fixture.mutant, s.fixture.ancestors)), - ), - Then('it returns undefined')((s) => - Effect.sync(() => { - expect(s.reason).toBeUndefined() - }) - ), - ), - ) - - scenario( - 'A decider-shaped andThen argument contributes no mutation population', - Gherkin.Do.pipe( - Given('the same decider-shaped argument under `Workflow.make` and under `Workflow.andThen`')( - 'fixtures', - () => - Effect.sync(() => { - const mutant = stringLiteral('step') - const body = makeBodyOf(mutant) - const makeCall = workflowMakeCallOf(body) - const andThenCall = workflowAndThenCallOf([identifier('first'), body]) - const program = programOf([workflowNamedImport(), makeCall, andThenCall]) - return { - mutant, - makeAncestors: [body, makeCall, program], - andThenAncestors: [body, andThenCall, program], - } - }), - ), - When('the boundary decision runs on the mutant against each ancestor chain')( - 'verdicts', - (s) => - Effect.sync(() => ({ - make: decideWorkflowMakeBoundaryIgnore(s.fixtures.mutant, s.fixtures.makeAncestors), - andThen: decideWorkflowMakeBoundaryIgnore(s.fixtures.mutant, s.fixtures.andThenAncestors), - })), - ), - Then('the make boundary holds the mutant live and the composing boundary is not a decider body')((s) => - Effect.sync(() => { - expect(s.verdicts.make).toBeUndefined() - expect(s.verdicts.andThen).toBe(NOT_INSIDE_WORKFLOW_MAKE) - }) - ), - ), - ) - - scenario( - 'A composing andThen operand that resolves to a same-file function joins no population', - Gherkin.Do.pipe( - Given('a `Workflow.andThen(...)` operand naming a same-file function holding the mutant')( - 'fixture', - () => - Effect.sync(() => { - const mutant = stringLiteral('compose') - const operandBody = makeBodyOf(mutant) - const operand = constBindingOf('upstreamStep', operandBody) - const andThenCall = workflowAndThenCallOf([identifier('upstreamStep'), identifier('second')]) - const program = programOf([workflowNamedImport(), operand, andThenCall]) - return makeFixture(mutant, [mutant, operandBody, operand, program]) - }), - ), - When('the boundary decision runs on that mutant')( - 'reason', - (s) => Effect.sync(() => decideWorkflowMakeBoundaryIgnore(s.fixture.mutant, s.fixture.ancestors)), - ), - Then('it returns NOT_INSIDE_WORKFLOW_MAKE')((s) => - Effect.sync(() => { - expect(s.reason).toBe(NOT_INSIDE_WORKFLOW_MAKE) - }) - ), - ), - ) - - scenario( - 'A schema tag outside every make body earns its own distinct reason from each ignorer', - Gherkin.Do.pipe( - Given('a `_tag` inside a TaggedClass declaration sitting outside any make body')( - 'fixture', - () => - Effect.sync(() => { - const tag = stringLiteral('Placed') - const fields = { type: 'ObjectExpression' as const } - const call = taggedCall('TaggedClass', tag, fields) - const program = programOf([workflowNamedImport(), call]) - return makeFixture(tag, [call, program]) - }), - ), - When('both the schema-declaration ignorer and the make-boundary ignorer decide it')( - 'tagverdict', - (s) => - Effect.sync(() => { - const schemaReason = decideSchemaDeclarationIgnore(s.fixture.mutant, s.fixture.ancestors[0]) - const makeReason = decideWorkflowMakeBoundaryIgnore(s.fixture.mutant, s.fixture.ancestors) - return { schemaReason, makeReason } - }), - ), - Then('each reports its own named reason, distinct from the other')((s) => - Effect.sync(() => { - expect(s.tagverdict.schemaReason).toBe(TAGGED_TAG_IGNORED) - expect(s.tagverdict.makeReason).toBe(NOT_INSIDE_WORKFLOW_MAKE) - expect(s.tagverdict.schemaReason).not.toBe(s.tagverdict.makeReason) - }) - ), - ), - ) - - scenario( - 'The entrypoint registers an ignore plugin named workflow-make-boundary', - Gherkin.Do.pipe( - Given('the entrypoint plugin array')('plugins', () => Effect.sync(() => strykerPlugins)), - When('the declared plugin is inspected')('plugin', (s) => Effect.sync(() => s.plugins[0])), - Then('it is an Ignore-kind plugin named workflow-make-boundary carrying a layer')((s) => - Effect.sync(() => { - expect(s.plugin).toMatchObject({ kind: 'Ignore', name: 'workflow-make-boundary' }) - // The contribution now carries a `Layer` rather than a bare value. - // That the layer actually provides `Ignorer` is proven where the - // engine composes it, since building it here would need fabricated - // `RunConfiguration` and `SandboxDirectory` services. - expect(s.plugin?.layer).toBeDefined() - }) - ), - ), - ) - - scenario( - 'The package barrel carries all three ignore plugins', - Gherkin.Do.pipe( - Given('the package barrel plugin array')('plugins', () => Effect.sync(() => composedPlugins)), - Then('it carries all three declarable ignorer names')((s) => - Effect.sync(() => { - const names = s.plugins.map((plugin) => plugin.name) - expect(names).toEqual( - expect.arrayContaining([ - 'effect-schema-declarations', - 'in-source-vitest-block', - 'workflow-make-boundary', - ]), - ) - }) - ), - ), - ) - }) diff --git a/packages/stryker-plugins/vitest-setup.ts b/packages/stryker-plugins/vitest-setup.ts deleted file mode 100644 index de7a3bf88..000000000 --- a/packages/stryker-plugins/vitest-setup.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { FastCheck as fc } from 'effect/testing' - -const isStrykerWorker = typeof process !== 'undefined' && process.env['STRYKER_MUTATOR_WORKER'] !== undefined -const isCi = typeof process !== 'undefined' && process.env['CI'] === 'true' - -const numRuns = isStrykerWorker ? 30 : isCi ? 1000 : 100 - -fc.configureGlobal({ numRuns }) diff --git a/packages/stryker-plugins/vitest.config.ts b/packages/stryker-plugins/vitest.config.ts deleted file mode 100644 index af667fa54..000000000 --- a/packages/stryker-plugins/vitest.config.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { inlineSchemaTests } from '@systemfsoftware/effect-schema-vite' -import { defineConfig, sharedConfig } from '@systemfsoftware/vitest-config' - -export default defineConfig({ - ...sharedConfig, - plugins: [inlineSchemaTests()], - test: { - ...sharedConfig.test, - include: ['src/**/*.test.ts', 'tests/**/*.integration.test.ts'], - setupFiles: ['vitest-setup.ts'], - }, -}) diff --git a/packages/stryker-test-contribution/api-extractor.json b/packages/stryker-test-contribution/api-extractor.json new file mode 100644 index 000000000..a0e41b12d --- /dev/null +++ b/packages/stryker-test-contribution/api-extractor.json @@ -0,0 +1,55 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "projectFolder": ".", + "mainEntryPointFilePath": "/dist/index.d.ts", + "bundledPackages": [], + "compiler": { + "tsconfigFilePath": "/tsconfig.json" + }, + "apiReport": { + "enabled": true, + "reportFolder": "/etc/", + "reportTempFolder": "/temp/main/", + "reportFileName": "stryker-test-contribution.api.md" + }, + "docModel": { + "enabled": false + }, + "dtsRollup": { + "enabled": true, + "untrimmedFilePath": "/dist/index.d.ts" + }, + "tsdocMetadata": { + "enabled": false + }, + "messages": { + "extractorMessageReporting": { + "default": { + "logLevel": "warning" + }, + "ae-forgotten-export": { + "logLevel": "error" + }, + "ae-missing-release-tag": { + "logLevel": "none" + }, + "ae-internal-missing-underscore": { + "logLevel": "none" + } + }, + "tsdocMessageReporting": { + "default": { + "logLevel": "warning" + }, + "tsdoc-undefined-tag": { + "logLevel": "none" + }, + "tsdoc-unsupported-tag": { + "logLevel": "none" + }, + "tsdoc-param-tag-missing-hyphen": { + "logLevel": "none" + } + } + } +} diff --git a/packages/stryker-test-contribution/etc/stryker-test-contribution.api.md b/packages/stryker-test-contribution/etc/stryker-test-contribution.api.md new file mode 100644 index 000000000..fe4ff791a --- /dev/null +++ b/packages/stryker-test-contribution/etc/stryker-test-contribution.api.md @@ -0,0 +1,71 @@ +## API Report File for "@systemfsoftware/stryker-test-contribution" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import * as Effect from 'effect/Effect'; +import { Evaluator } from '@systemfsoftware/stryker-js-language'; +import { EvaluatorFailed } from '@systemfsoftware/stryker-js-language'; +import { ExitClass } from '@systemfsoftware/stryker-js-language'; +import * as Layer from 'effect/Layer'; +import { PluginLayerContribution } from '@systemfsoftware/stryker-js-plugin-interface'; +import { RunConfiguration } from '@systemfsoftware/stryker-js-plugin-interface'; +import * as schema from '@systemfsoftware/stryker-js-language'; + +// @public (undocumented) +export const contributionByTestFile: (report: ReportView) => ReadonlyMap; + +// @public (undocumented) +export const defaultRequireTestContributionSuffixes: readonly ['.workflow.property.test.ts', '.policy.property.test.ts', '.kernel.property.test.ts']; + +// @public (undocumented) +export const judgeTestContribution: (report: ReportView, everyKillerRecorded: boolean, suffixes?: readonly string[]) => TestContributionVerdict; + +// @public (undocumented) +export const makeTestContributionEvaluatorService: (options: { + readonly disableBail: boolean; +}) => { + readonly evaluate: (report: schema.MutationTestResult) => Effect.Effect; +}; + +// @public (undocumented) +export type ReportView = Pick; + +// @public (undocumented) +export const strykerPlugins: PluginLayerContribution<"Evaluator">[]; + +// @public (undocumented) +export const testContributionEvaluatorLayer: Layer.Layer; + +// @public (undocumented) +export interface TestContributionInput { + readonly everyKillerRecorded: boolean; + // (undocumented) + readonly suffixes: readonly string[]; +} + +// @public (undocumented) +export interface TestContributionVerdict { + // (undocumented) + readonly failed: boolean; + // (undocumented) + readonly message: string; +} + +// @public (undocumented) +export interface TestFileContribution { + readonly coversUnattributedKill: boolean; + readonly killableCovered: number; + // (undocumented) + readonly soleKills: number; + // (undocumented) + readonly totalKills: number; +} + +// @public (undocumented) +export const toothlessTestFiles: (contribution: ReadonlyMap, input: TestContributionInput) => readonly string[]; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/stryker-test-contribution/package.json b/packages/stryker-test-contribution/package.json index 2b90cbd84..60a5608fe 100644 --- a/packages/stryker-test-contribution/package.json +++ b/packages/stryker-test-contribution/package.json @@ -31,12 +31,14 @@ "./package.json": "./package.json" }, "scripts": { - "build": "tsdown", + "build": "tsdown && pnpm api:check", "clean": "rimraf dist", "typecheck": "tsc --noEmit --incremental && tsc -p tsconfig.node.json --noEmit", "test": "vitest run", "lint": "f=${OXLINT_FORMAT:-${AGENT:+agent}}; oxlint . --format=${f:-default}", - "attw": "attw --pack ." + "attw": "attw --pack .", + "api:check": "tsdown && api-extractor run", + "api:update": "api-extractor run --local" }, "dependencies": { "@systemfsoftware/stryker-js-language": "workspace:^", @@ -47,6 +49,7 @@ }, "devDependencies": { "@effect/vitest": "catalog:", + "@microsoft/api-extractor": "catalog:", "@systemfsoftware/all": "catalog:", "@systemfsoftware/arethetypeswrong-cli": "catalog:", "@systemfsoftware/effect-gherkin-spec": "catalog:", diff --git a/packages/toolchain/oxlint-ignorer-config/AGENTS.md b/packages/toolchain/oxlint-ignorer-config/AGENTS.md new file mode 100644 index 000000000..4aa3017bc --- /dev/null +++ b/packages/toolchain/oxlint-ignorer-config/AGENTS.md @@ -0,0 +1,32 @@ +# AGENTS.md — `@systemfsoftware/oxlint-ignorer-config` + +The published lint preset for an ignorer package. It extends nothing: the rule +set is assembled in `lib/base.js` from the linter's built-in plugins and +categories, so an ignorer authored outside this repository is graded by the +same bar as the ones inside it. + +## Rules + +| ID | Rule | Gate | +| --- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | +| OC1 | A decision stays one path: `complexity` max 2 on `**/src/**`, ternary form allowed | `pnpm lint` | +| OC2 | Foreign data is decoded, never asserted: `consistent-type-assertions` (`never`), `no-unsafe-*`, `no-explicit-any`, `no-non-null-assertion` | `pnpm lint` | +| OC3 | No runtime behind an ignorer: `no-restricted-imports` rejects Effect, `@effect/*`, the family Effect presets, the family rule packs and `@systemfsoftware/stryker-js-*` | `pnpm lint` | +| OC4 | Tests observe decisions: `vitest/no-focused-tests`, `vitest/no-conditional-expect` | `pnpm lint` | +| OC5 | The published shape is declared: `lib/base.d.ts` matches `etc/oxlint-ignorer-config.api.md` | `pnpm api:check` | +| OC6 | `lib/base.js` is type-checked, so its JSDoc `@type` annotations are load-bearing | `pnpm typecheck` | + +## Commands + +```bash +pnpm --filter @systemfsoftware/oxlint-ignorer-config typecheck +pnpm --filter @systemfsoftware/oxlint-ignorer-config api:update # after an intentional surface change +pnpm --filter @systemfsoftware/oxlint-ignorer-config api:check +``` + +## Notes + +- `lib/base.js` is the source. There is no build step; the JSDoc annotations are + what `tsc` checks, so removing one is a behaviour change, not a tidy-up. +- Changing the rule set changes every adopting package's gate. Treat a rule + addition as a breaking change to the preset, and say so in the changeset. diff --git a/packages/toolchain/oxlint-ignorer-config/LICENSE b/packages/toolchain/oxlint-ignorer-config/LICENSE new file mode 100644 index 000000000..896c3747c --- /dev/null +++ b/packages/toolchain/oxlint-ignorer-config/LICENSE @@ -0,0 +1,203 @@ +Copyright (c) 2026 Ryan Lee (systemfsoftware) + +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2026 Ryan Lee (systemfsoftware) + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/toolchain/oxlint-ignorer-config/README.md b/packages/toolchain/oxlint-ignorer-config/README.md new file mode 100644 index 000000000..74e26026e --- /dev/null +++ b/packages/toolchain/oxlint-ignorer-config/README.md @@ -0,0 +1,50 @@ +# @systemfsoftware/oxlint-ignorer-config + +The lint preset for an ignorer package. It imports no family preset: the rule +set is assembled from oxlint's built-in plugins and categories, so an ignorer +authored outside this repo is graded by the same bar as the ones in it. + +## Install + +```bash +pnpm add -D @systemfsoftware/oxlint-ignorer-config oxlint +``` + +## Use + +`oxlint.config.ts` + +```ts +import preset from '@systemfsoftware/oxlint-ignorer-config' +import { defineConfig } from 'oxlint' + +export default defineConfig({ + extends: [preset], +}) +``` + +Add `"lint": "oxlint . --format=${f:-default}"` to your scripts. + +## What the preset holds you to + +| Rule | Why it is here | +| --------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | +| `complexity` `['error', { max: 2, variant: 'modified' }]` on `**/src/**` | One path per decision: `match`/`map`/`fold` are calls, not control flow. | +| `typescript/consistent-type-assertions` (`never`), `no-unsafe-*`, `no-explicit-any`, `no-non-null-assertion` | An ignorer reads foreign AST data, so it decodes instead of asserting. | +| `no-restricted-imports` | No `effect`, `@effect/*`, the family Effect presets, the family lint packs, or `@systemfsoftware/stryker-js-*` in an ignorer. | +| `vitest/no-focused-tests`, `vitest/no-conditional-expect` | A test earns its place by observing a decision. | +| `eqeqeq`, `prefer-const`, `no-var`, `no-else-return`, `no-lonely-if`, `unicorn/*`, `import/no-cycle`, `promise/param-names` | Ordinary strictness, plus `@oxc-plugin`-free correctness/suspicious/perf at error and `nursery` at warn. | + +Test and fixture paths relax the assertion rules, because a case table may +build a fixture by assertion — the decision may not. + +## Writing an ignorer + +Author against `@systemfsoftware/stryker-ignorer-interface`: it publishes the +entry contract (`{ name, shouldIgnore(path) }`), `NodePath`, and the AST +vocabulary types. This preset is the lint half of that pair. + +## Gates + +- `pnpm typecheck` — `tsc --noEmit --incremental`, and `checkJs` covers `lib/base.js`. +- `pnpm api:check` — API Extractor validates `lib/base.d.ts` against `etc/oxlint-ignorer-config.api.md`. diff --git a/packages/toolchain/oxlint-ignorer-config/api-extractor.json b/packages/toolchain/oxlint-ignorer-config/api-extractor.json new file mode 100644 index 000000000..774405209 --- /dev/null +++ b/packages/toolchain/oxlint-ignorer-config/api-extractor.json @@ -0,0 +1,55 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "projectFolder": ".", + "mainEntryPointFilePath": "/lib/base.d.ts", + "bundledPackages": [], + "compiler": { + "tsconfigFilePath": "/tsconfig.json" + }, + "apiReport": { + "enabled": true, + "reportFolder": "/etc/", + "reportTempFolder": "/temp/main/", + "reportFileName": "oxlint-ignorer-config.api.md" + }, + "docModel": { + "enabled": false + }, + "dtsRollup": { + "enabled": true, + "untrimmedFilePath": "/lib/base.d.ts" + }, + "tsdocMetadata": { + "enabled": false + }, + "messages": { + "extractorMessageReporting": { + "default": { + "logLevel": "warning" + }, + "ae-forgotten-export": { + "logLevel": "error" + }, + "ae-missing-release-tag": { + "logLevel": "none" + }, + "ae-internal-missing-underscore": { + "logLevel": "none" + } + }, + "tsdocMessageReporting": { + "default": { + "logLevel": "warning" + }, + "tsdoc-undefined-tag": { + "logLevel": "none" + }, + "tsdoc-unsupported-tag": { + "logLevel": "none" + }, + "tsdoc-param-tag-missing-hyphen": { + "logLevel": "none" + } + } + } +} diff --git a/packages/toolchain/oxlint-ignorer-config/etc/oxlint-ignorer-config.api.md b/packages/toolchain/oxlint-ignorer-config/etc/oxlint-ignorer-config.api.md new file mode 100644 index 000000000..6c0fe3b08 --- /dev/null +++ b/packages/toolchain/oxlint-ignorer-config/etc/oxlint-ignorer-config.api.md @@ -0,0 +1,27 @@ +## API Report File for "@systemfsoftware/oxlint-ignorer-config" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { OxlintConfig } from 'oxlint'; + +// @public (undocumented) +export const categories: NonNullable; + +// @public (undocumented) +export const ignorePatterns: readonly string[]; + +// @public (undocumented) +const ignorerConfig: OxlintConfig; +export default ignorerConfig; + +// @public (undocumented) +export const plugins: NonNullable; + +// @public (undocumented) +export const rules: NonNullable; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/toolchain/oxlint-ignorer-config/lib/base.d.ts b/packages/toolchain/oxlint-ignorer-config/lib/base.d.ts new file mode 100644 index 000000000..796ee3388 --- /dev/null +++ b/packages/toolchain/oxlint-ignorer-config/lib/base.d.ts @@ -0,0 +1,14 @@ +import { OxlintConfig } from 'oxlint'; + +export declare const categories: NonNullable; + +export declare const ignorePatterns: readonly string[]; + +declare const ignorerConfig: OxlintConfig; +export default ignorerConfig; + +export declare const plugins: NonNullable; + +export declare const rules: NonNullable; + +export { } diff --git a/packages/toolchain/oxlint-ignorer-config/lib/base.js b/packages/toolchain/oxlint-ignorer-config/lib/base.js new file mode 100644 index 000000000..9d696ee4c --- /dev/null +++ b/packages/toolchain/oxlint-ignorer-config/lib/base.js @@ -0,0 +1,128 @@ +/** @type {import('oxlint').OxlintConfig['plugins']} */ +const plugins = ['typescript', 'unicorn', 'oxc', 'import', 'promise', 'vitest'] + +/** @type {import('oxlint').OxlintConfig['categories']} */ +const categories = { + correctness: 'error', + suspicious: 'error', + perf: 'error', + nursery: 'warn', +} + +/** @type {import('oxlint').OxlintConfig['rules']} */ +const rules = { + 'no-ternary': 'off', + 'typescript/consistent-type-assertions': ['error', { assertionStyle: 'never' }], + 'typescript/no-explicit-any': 'error', + 'typescript/no-non-null-assertion': 'error', + 'typescript/no-unsafe-argument': 'error', + 'typescript/no-unsafe-assignment': 'error', + 'typescript/no-unsafe-call': 'error', + 'typescript/no-unsafe-return': 'error', + 'typescript/no-unsafe-type-assertion': 'error', + eqeqeq: ['error', 'always', { null: 'ignore' }], + 'no-var': 'error', + 'prefer-const': 'error', + 'no-else-return': 'error', + 'no-lonely-if': 'error', + 'no-implicit-coercion': 'error', + 'no-return-assign': 'error', + 'no-array-constructor': 'error', + 'unicorn/prefer-node-protocol': 'error', + 'unicorn/no-array-for-each': 'error', + 'import/no-cycle': 'error', + 'promise/param-names': 'error', + 'vitest/no-focused-tests': 'error', + 'vitest/no-conditional-expect': 'error', + 'no-restricted-imports': ['error', { + patterns: [ + { + regex: '^effect(?:/.*)?$', + message: 'An ignorer package carries no Effect: its decision is plain data in, a value or a reason string out.', + }, + { + regex: '^@effect/.*$', + message: 'An ignorer package carries no @effect/* import.', + }, + { + regex: '^@systemfsoftware/(?:all|effect-.*)$', + message: 'The family Effect presets belong to the StrykerJS runtime, not to an ignorer package.', + }, + { + regex: '^@systemfsoftware/oxlint-plugin(?:-recommended)?$', + message: + 'An ignorer package is graded by its own preset (@systemfsoftware/oxlint-ignorer-config); it does not import the family rule packs.', + }, + { + regex: '^@systemfsoftware/stryker-js-.*$', + message: + 'An ignorer package depends on @systemfsoftware/stryker-ignorer-interface and nothing else from the family.', + }, + ], + }], +} + +/** @type {readonly string[]} */ +const ignorePatterns = [ + '**/node_modules/**', + '**/dist/**', + '**/build/**', + '**/out/**', + '**/coverage/**', + '**/.turbo/**', + '**/.stryker-tmp/**', + '**/*.d.ts', + '**/*.tsbuildinfo', +] + +/** @type {import('oxlint').OxlintConfig['overrides']} */ +const ignorerOverrides = [ + { + files: ['**/src/**'], + rules: { + complexity: ['error', { + max: 2, + variant: 'modified', + }], + }, + }, + { + files: [ + '**/*.test.ts', + '**/*.spec.ts', + '**/__tests__/**', + '**/tests/**', + ], + rules: { + complexity: 'off', + 'typescript/consistent-type-assertions': 'off', + 'typescript/no-unsafe-type-assertion': 'off', + }, + }, + { + files: [ + '**/__fixtures__/**', + '**/fixtures/**', + '**/testResources/**', + ], + rules: { + 'typescript/consistent-type-assertions': 'off', + 'typescript/no-unsafe-argument': 'off', + 'typescript/no-unsafe-assignment': 'off', + 'typescript/no-unsafe-call': 'off', + 'typescript/no-unsafe-return': 'off', + 'typescript/no-unsafe-type-assertion': 'off', + }, + }, +] + +/** @type {import('oxlint').OxlintConfig} */ +const ignorerConfig = { + plugins: [...plugins], + categories: { ...categories }, + rules: { ...rules }, + overrides: [...ignorerOverrides], + ignorePatterns: [...ignorePatterns], +} + +export { categories, ignorePatterns, ignorerConfig as default, plugins, rules } diff --git a/packages/toolchain/oxlint-ignorer-config/package.json b/packages/toolchain/oxlint-ignorer-config/package.json new file mode 100644 index 000000000..939ff46b5 --- /dev/null +++ b/packages/toolchain/oxlint-ignorer-config/package.json @@ -0,0 +1,61 @@ +{ + "name": "@systemfsoftware/oxlint-ignorer-config", + "version": "0.0.0", + "description": "The lint preset for an ignorer package: one path per decision, no casts on foreign data, and no StrykerJS runtime behind an ignorer", + "keywords": [ + "oxlint", + "oxlint-config", + "stryker", + "ignorer" + ], + "license": "Apache-2.0", + "author": "Ryan Lee ", + "type": "module", + "module": "./lib/base.js", + "files": [ + "lib" + ], + "exports": { + "./package.json": "./package.json", + ".": { + "import": { + "types": "./lib/base.d.ts", + "default": "./lib/base.js" + } + } + }, + "scripts": { + "typecheck": "tsc --noEmit --incremental", + "api:check": "api-extractor run", + "api:update": "api-extractor run --local" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/systemfsoftware/stryker-js-effect.git", + "directory": "packages/toolchain/oxlint-ignorer-config" + }, + "homepage": "https://github.com/systemfsoftware/stryker-js-effect/tree/main/packages/toolchain/oxlint-ignorer-config#readme", + "bugs": "https://github.com/systemfsoftware/stryker-js-effect/issues", + "engines": { + "node": ">=20.0.0" + }, + "publishConfig": { + "access": "public", + "provenance": true, + "exports": { + "./package.json": "./package.json", + ".": { + "import": { + "types": "./lib/base.d.ts", + "default": "./lib/base.js" + } + } + } + }, + "devDependencies": { + "@microsoft/api-extractor": "catalog:", + "@systemfsoftware/tsconfig": "catalog:", + "@types/node": "catalog:", + "oxlint": "catalog:" + } +} diff --git a/packages/toolchain/oxlint-ignorer-config/tsconfig.json b/packages/toolchain/oxlint-ignorer-config/tsconfig.json new file mode 100644 index 000000000..00e2183af --- /dev/null +++ b/packages/toolchain/oxlint-ignorer-config/tsconfig.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": ["@systemfsoftware/tsconfig/tsc/dom/library-monorepo"], + "compilerOptions": { + "noEmit": true, + "allowJs": true, + "checkJs": true + }, + "include": [ + "lib" + ] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f073b4670..fec882423 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,6 +24,9 @@ catalogs: '@effect/vitest': specifier: 4.0.0-rc.112 version: 4.0.0-rc.112 + '@microsoft/api-extractor': + specifier: ^7.59.1 + version: 7.59.1 '@noble/hashes': specifier: 2.4.0 version: 2.4.0 @@ -66,6 +69,9 @@ catalogs: '@vitest/coverage-v8': specifier: ^4 version: 4.1.11 + concurrently: + specifier: ^10.0.5 + version: 10.0.5 effect: specifier: ^4.0.0-rc.112 version: 4.0.0-rc.112 @@ -78,6 +84,9 @@ catalogs: oxc-parser: specifier: ^0.140.0 version: 0.140.0 + oxc-walker: + specifier: ^1.1.1 + version: 1.1.1 oxlint: specifier: ^1.77.0 version: 1.82.0 @@ -93,12 +102,12 @@ catalogs: turbo: specifier: ^2.10.12 version: 2.10.12 + type-fest: + specifier: ^4.41.0 + version: 4.41.0 typescript: specifier: ^7 version: 7.0.2 - vite-tsconfig-paths: - specifier: ^6.1.1 - version: 6.1.1 vitest: specifier: ^4 version: 4.1.11 @@ -145,6 +154,125 @@ importers: specifier: 'catalog:' version: 7.0.2 + packages/ignorers/effect-schema-declarations: + dependencies: + '@systemfsoftware/stryker-ignorer-interface': + specifier: workspace:^ + version: link:../interface + devDependencies: + '@microsoft/api-extractor': + specifier: 'catalog:' + version: 7.59.1(@types/node@24.13.3) + '@systemfsoftware/arethetypeswrong-cli': + specifier: 'catalog:' + version: 4.1.0 + '@systemfsoftware/oxlint-ignorer-config': + specifier: workspace:^ + version: link:../../toolchain/oxlint-ignorer-config + '@systemfsoftware/tsconfig': + specifier: 'catalog:' + version: 1.3.4 + '@systemfsoftware/vitest-config': + specifier: workspace:^ + version: link:../../toolchain/vitest-config + '@types/node': + specifier: 'catalog:' + version: 24.13.3 + oxlint: + specifier: 'catalog:' + version: 1.82.0(oxlint-tsgolint@7.0.2001) + rimraf: + specifier: 'catalog:' + version: 6.1.3 + tsdown: + specifier: 'catalog:' + version: 0.23.0(typescript@7.0.2) + typescript: + specifier: 'catalog:' + version: 7.0.2 + vitest: + specifier: 'catalog:' + version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(vite@8.2.2(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.9.0)) + + packages/ignorers/in-source-vitest-block: + dependencies: + '@systemfsoftware/stryker-ignorer-interface': + specifier: workspace:^ + version: link:../interface + devDependencies: + '@microsoft/api-extractor': + specifier: 'catalog:' + version: 7.59.1(@types/node@24.13.3) + '@systemfsoftware/arethetypeswrong-cli': + specifier: 'catalog:' + version: 4.1.0 + '@systemfsoftware/oxlint-ignorer-config': + specifier: workspace:^ + version: link:../../toolchain/oxlint-ignorer-config + '@systemfsoftware/tsconfig': + specifier: 'catalog:' + version: 1.3.4 + '@systemfsoftware/vitest-config': + specifier: workspace:^ + version: link:../../toolchain/vitest-config + '@types/node': + specifier: 'catalog:' + version: 24.13.3 + oxlint: + specifier: 'catalog:' + version: 1.82.0(oxlint-tsgolint@7.0.2001) + rimraf: + specifier: 'catalog:' + version: 6.1.3 + tsdown: + specifier: 'catalog:' + version: 0.23.0(typescript@7.0.2) + typescript: + specifier: 'catalog:' + version: 7.0.2 + vitest: + specifier: 'catalog:' + version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(vite@8.2.2(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.9.0)) + + packages/ignorers/interface: + devDependencies: + '@microsoft/api-extractor': + specifier: 'catalog:' + version: 7.59.1(@types/node@24.13.3) + '@oxc-project/types': + specifier: 'catalog:' + version: 0.140.0 + '@systemfsoftware/arethetypeswrong-cli': + specifier: 'catalog:' + version: 4.1.0 + '@systemfsoftware/oxlint-ignorer-config': + specifier: workspace:^ + version: link:../../toolchain/oxlint-ignorer-config + '@systemfsoftware/tsconfig': + specifier: 'catalog:' + version: 1.3.4 + '@systemfsoftware/vitest-config': + specifier: workspace:^ + version: link:../../toolchain/vitest-config + '@types/node': + specifier: 'catalog:' + version: 24.13.3 + oxlint: + specifier: 'catalog:' + version: 1.82.0(oxlint-tsgolint@7.0.2001) + rimraf: + specifier: 'catalog:' + version: 6.1.3 + tsdown: + specifier: 'catalog:' + version: 0.23.0(typescript@7.0.2) + typescript: + specifier: 'catalog:' + version: 7.0.2 + vitest: + specifier: 'catalog:' + version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(vite@8.2.2(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.9.0)) + packages/stryker-js-cli: devDependencies: '@effect/platform-node': @@ -189,18 +317,6 @@ importers: '@systemfsoftware/stryker-js-plugin-interface': specifier: workspace:^ version: link:../stryker-js-plugin-interface - '@systemfsoftware/stryker-js-typescript-checker': - specifier: workspace:^ - version: link:../stryker-js-typescript-checker - '@systemfsoftware/stryker-js-vitest-runner': - specifier: workspace:^ - version: link:../stryker-js-vitest-runner - '@systemfsoftware/stryker-plugins': - specifier: workspace:^ - version: link:../stryker-plugins - '@systemfsoftware/stryker-test-contribution': - specifier: workspace:^ - version: link:../stryker-test-contribution '@systemfsoftware/tsconfig': specifier: 'catalog:' version: 1.3.4 @@ -231,9 +347,6 @@ importers: tsdown: specifier: 'catalog:' version: 0.23.0(typescript@7.0.2) - vite-tsconfig-paths: - specifier: 'catalog:' - version: 6.1.1(supports-color@7.2.0)(typescript@7.0.2)(vite@8.2.2(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.9.0)) vitest: specifier: 'catalog:' version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(vite@8.2.2(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.9.0)) @@ -268,6 +381,9 @@ importers: '@effect/vitest': specifier: 'catalog:' version: 4.0.0-rc.112(effect@4.0.0-rc.112)(vitest@4.1.11) + '@microsoft/api-extractor': + specifier: 'catalog:' + version: 7.59.1(@types/node@24.13.3) '@std/jsonc': specifier: 'catalog:' version: '@jsr/std__jsonc@1.0.2' @@ -292,6 +408,9 @@ importers: '@types/node': specifier: 'catalog:' version: 24.13.3 + concurrently: + specifier: 'catalog:' + version: 10.0.5 effect: specifier: 'catalog:' version: 4.0.0-rc.112 @@ -329,6 +448,9 @@ importers: specifier: catalog:stryker version: 3.7.3 devDependencies: + '@microsoft/api-extractor': + specifier: 'catalog:' + version: 7.59.1(@types/node@24.13.3) '@systemfsoftware/all': specifier: 'catalog:' version: 2.0.2(effect@4.0.0-rc.112)(oxlint-tsgolint@7.0.2001)(oxlint@1.82.0(oxlint-tsgolint@7.0.2001))(typescript@7.0.2) @@ -392,7 +514,13 @@ importers: oxc-parser: specifier: 'catalog:' version: 0.140.0 + oxc-walker: + specifier: 'catalog:' + version: 1.1.1(@oxc-project/types@0.140.0)(oxc-parser@0.140.0)(rolldown@1.2.8) devDependencies: + '@microsoft/api-extractor': + specifier: 'catalog:' + version: 7.59.1(@types/node@24.13.3) '@systemfsoftware/all': specifier: 'catalog:' version: 2.0.2(effect@4.0.0-rc.112)(oxlint-tsgolint@7.0.2001)(oxlint@1.82.0(oxlint-tsgolint@7.0.2001))(typescript@7.0.2) @@ -408,15 +536,9 @@ importers: '@systemfsoftware/vitest-config': specifier: workspace:^ version: link:../toolchain/vitest-config - '@types/estree': - specifier: 1.0.8 - version: 1.0.8 '@types/node': specifier: 'catalog:' version: 24.13.3 - estree-walker: - specifier: 3.0.3 - version: 3.0.3 oxlint: specifier: 'catalog:' version: 1.82.0(oxlint-tsgolint@7.0.2001) @@ -429,6 +551,9 @@ importers: tsdown: specifier: 'catalog:' version: 0.23.0(typescript@7.0.2) + type-fest: + specifier: 'catalog:' + version: 4.41.0 typescript: specifier: 'catalog:' version: 7.0.2 @@ -448,6 +573,9 @@ importers: '@effect/vitest': specifier: 'catalog:' version: 4.0.0-rc.112(effect@4.0.0-rc.112)(vitest@4.1.11) + '@microsoft/api-extractor': + specifier: 'catalog:' + version: 7.59.1(@types/node@24.13.3) '@systemfsoftware/all': specifier: 'catalog:' version: 2.0.2(effect@4.0.0-rc.112)(oxlint-tsgolint@7.0.2001)(oxlint@1.82.0(oxlint-tsgolint@7.0.2001))(typescript@7.0.2) @@ -460,6 +588,9 @@ importers: '@systemfsoftware/effect-schema-vite': specifier: 'catalog:' version: 2.1.1(@systemfsoftware/effect-schema-law@2.1.1(@effect/vitest@4.0.0-rc.112(effect@4.0.0-rc.112)(vitest@4.1.11))(effect@4.0.0-rc.112)(vitest@4.1.11))(effect@4.0.0-rc.112)(vite@8.2.2(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.9.0))(vitest@4.1.11) + '@systemfsoftware/stryker-ignorer-interface': + specifier: workspace:^ + version: link:../ignorers/interface '@systemfsoftware/tsconfig': specifier: 'catalog:' version: 1.3.4 @@ -497,6 +628,9 @@ importers: specifier: 'catalog:' version: 4.0.0-rc.112 devDependencies: + '@microsoft/api-extractor': + specifier: 'catalog:' + version: 7.59.1(@types/node@24.13.3) '@systemfsoftware/all': specifier: 'catalog:' version: 2.0.2(effect@4.0.0-rc.112)(oxlint-tsgolint@7.0.2001)(oxlint@1.82.0(oxlint-tsgolint@7.0.2001))(typescript@7.0.2) @@ -561,6 +695,9 @@ importers: '@effect/platform-node-shared': specifier: 'catalog:' version: 4.0.0-rc.112(effect@4.0.0-rc.112) + '@microsoft/api-extractor': + specifier: 'catalog:' + version: 7.59.1(@types/node@24.13.3) '@std/jsonc': specifier: 'catalog:' version: '@jsr/std__jsonc@1.0.2' @@ -619,6 +756,9 @@ importers: '@effect/vitest': specifier: 'catalog:' version: 4.0.0-rc.112(effect@4.0.0-rc.112)(vitest@4.1.11) + '@microsoft/api-extractor': + specifier: 'catalog:' + version: 7.59.1(@types/node@24.13.3) '@systemfsoftware/all': specifier: 'catalog:' version: 2.0.2(effect@4.0.0-rc.112)(oxlint-tsgolint@7.0.2001)(oxlint@1.82.0(oxlint-tsgolint@7.0.2001))(typescript@7.0.2) @@ -640,6 +780,9 @@ importers: '@vitest/browser-playwright': specifier: 'catalog:' version: 4.1.11(playwright@1.63.0)(vite@8.2.2(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.9.0))(vitest@4.1.11) + concurrently: + specifier: 'catalog:' + version: 10.0.5 effect: specifier: 'catalog:' version: 4.0.0-rc.112 @@ -659,15 +802,21 @@ importers: specifier: 'catalog:' version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(vite@8.2.2(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.9.0)) - packages/stryker-plugins: + packages/stryker-test-contribution: dependencies: - typescript: - specifier: '>=5.0.0' - version: 7.0.2 + '@systemfsoftware/stryker-js-language': + specifier: workspace:^ + version: link:../stryker-js-language + '@systemfsoftware/stryker-js-plugin-interface': + specifier: workspace:^ + version: link:../stryker-js-plugin-interface devDependencies: '@effect/vitest': specifier: 'catalog:' version: 4.0.0-rc.112(effect@4.0.0-rc.112)(vitest@4.1.11) + '@microsoft/api-extractor': + specifier: 'catalog:' + version: 7.59.1(@types/node@24.13.3) '@systemfsoftware/all': specifier: 'catalog:' version: 2.0.2(effect@4.0.0-rc.112)(oxlint-tsgolint@7.0.2001)(oxlint@1.82.0(oxlint-tsgolint@7.0.2001))(typescript@7.0.2) @@ -683,27 +832,15 @@ importers: '@systemfsoftware/effect-schema-vite': specifier: 'catalog:' version: 2.1.1(@systemfsoftware/effect-schema-law@2.1.1(@effect/vitest@4.0.0-rc.112(effect@4.0.0-rc.112)(vitest@4.1.11))(effect@4.0.0-rc.112)(vitest@4.1.11))(effect@4.0.0-rc.112)(vite@8.2.2(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.9.0))(vitest@4.1.11) - '@systemfsoftware/stryker-js-language': - specifier: workspace:^ - version: link:../stryker-js-language - '@systemfsoftware/stryker-js-plugin-interface': - specifier: workspace:^ - version: link:../stryker-js-plugin-interface '@systemfsoftware/tsconfig': specifier: 'catalog:' version: 1.3.4 '@systemfsoftware/vitest-config': specifier: workspace:^ version: link:../toolchain/vitest-config - '@types/estree': - specifier: 1.0.8 - version: 1.0.8 '@types/node': specifier: 'catalog:' version: 24.13.3 - '@vitest/coverage-v8': - specifier: 'catalog:' - version: 4.1.11(@vitest/browser@4.1.11)(vitest@4.1.11) effect: specifier: 'catalog:' version: 4.0.0-rc.112 @@ -716,64 +853,27 @@ importers: tsdown: specifier: 'catalog:' version: 0.23.0(typescript@7.0.2) + typescript: + specifier: 'catalog:' + version: 7.0.2 vitest: specifier: 'catalog:' version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(vite@8.2.2(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.9.0)) - packages/stryker-test-contribution: - dependencies: - '@systemfsoftware/stryker-js-language': - specifier: workspace:^ - version: link:../stryker-js-language - '@systemfsoftware/stryker-js-plugin-interface': - specifier: workspace:^ - version: link:../stryker-js-plugin-interface + packages/toolchain/oxlint-ignorer-config: devDependencies: - '@effect/vitest': + '@microsoft/api-extractor': specifier: 'catalog:' - version: 4.0.0-rc.112(effect@4.0.0-rc.112)(vitest@4.1.11) - '@systemfsoftware/all': - specifier: 'catalog:' - version: 2.0.2(effect@4.0.0-rc.112)(oxlint-tsgolint@7.0.2001)(oxlint@1.82.0(oxlint-tsgolint@7.0.2001))(typescript@7.0.2) - '@systemfsoftware/arethetypeswrong-cli': - specifier: 'catalog:' - version: 4.1.0 - '@systemfsoftware/effect-gherkin-spec': - specifier: 'catalog:' - version: 4.0.2(@effect/vitest@4.0.0-rc.112(effect@4.0.0-rc.112)(vitest@4.1.11))(effect@4.0.0-rc.112)(vitest@4.1.11) - '@systemfsoftware/effect-schema-law': - specifier: 'catalog:' - version: 2.1.1(@effect/vitest@4.0.0-rc.112(effect@4.0.0-rc.112)(vitest@4.1.11))(effect@4.0.0-rc.112)(vitest@4.1.11) - '@systemfsoftware/effect-schema-vite': - specifier: 'catalog:' - version: 2.1.1(@systemfsoftware/effect-schema-law@2.1.1(@effect/vitest@4.0.0-rc.112(effect@4.0.0-rc.112)(vitest@4.1.11))(effect@4.0.0-rc.112)(vitest@4.1.11))(effect@4.0.0-rc.112)(vite@8.2.2(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.9.0))(vitest@4.1.11) + version: 7.59.1(@types/node@24.13.3) '@systemfsoftware/tsconfig': specifier: 'catalog:' version: 1.3.4 - '@systemfsoftware/vitest-config': - specifier: workspace:^ - version: link:../toolchain/vitest-config '@types/node': specifier: 'catalog:' version: 24.13.3 - effect: - specifier: 'catalog:' - version: 4.0.0-rc.112 oxlint: specifier: 'catalog:' version: 1.82.0(oxlint-tsgolint@7.0.2001) - rimraf: - specifier: 'catalog:' - version: 6.1.3 - tsdown: - specifier: 'catalog:' - version: 0.23.0(typescript@7.0.2) - typescript: - specifier: 'catalog:' - version: 7.0.2 - vitest: - specifier: 'catalog:' - version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(vite@8.2.2(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.9.0)) packages/toolchain/vitest-config: devDependencies: @@ -962,6 +1062,21 @@ packages: '@jsr/std__streams@1.1.2': resolution: {integrity: sha512-TNg+Mr/vE2Wg+JDdZpVj9umEyzcmSAOtcp0ildlyVCjHy0/fJ2BJZgO8PQXA/SwJOchvKLaMafZzfFkm9PO13A==, tarball: https://npm.jsr.io/~/11/@jsr/std__streams/1.1.2.tgz} + '@microsoft/api-extractor-model@7.33.12': + resolution: {integrity: sha512-TdKOYgwf98xLjNW+y3iXIiCf4ZLQbilGAlqkMTTAqqAWfgRXAn9YCSGMsaiB1U7OPRvslUWg6n08EqQPbP93tA==} + engines: {node: '>=20.9.0'} + + '@microsoft/api-extractor@7.59.1': + resolution: {integrity: sha512-GjRUqx1MTY7xuH36urwASkfBPzrdxYG+irVeV7C9JEdRtn509AgMmwit/BhvztQzIoFk9vhFDTVYOp2cjw+9Uw==} + engines: {node: '>=20.9.0'} + hasBin: true + + '@microsoft/tsdoc-config@0.18.1': + resolution: {integrity: sha512-9brPoVdfN9k9g0dcWkFeA7IH9bbcttzDJlXvkf8b2OBzd5MueR1V2wkKBL0abn0otvmkHJC6aapBOTJDDeMCZg==} + + '@microsoft/tsdoc@0.16.0': + resolution: {integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==} + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4': resolution: {integrity: sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==} cpu: [arm64] @@ -1437,6 +1552,39 @@ packages: '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + '@rushstack/node-core-library@5.24.1': + resolution: {integrity: sha512-ZlOrzv92MwnsCXA45qWfDj4L/kTasKghXezu+M2WmdtkbnXyPnZSCovfeBZx1Yc5qm+LkElbLw6IeSSWZDhZUg==} + engines: {node: '>=20.9.0'} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + + '@rushstack/problem-matcher@0.2.1': + resolution: {integrity: sha512-gulfhBs6n+I5b7DvjKRfhMGyUejtSgOHTclF/eONr8hcgF1APEDjhxIsfdUYYMzC3rvLwGluqLjbwCFZ8nxrog==} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + + '@rushstack/rig-package@0.7.3': + resolution: {integrity: sha512-aAA518n6wxxjCfnTAOjQnm7ngNE0FVHxHAw2pxKlIhxrMn0XQjGcXKF0oKWpjBgJOmsaJpVob/v+zr3zxgPWuA==} + + '@rushstack/terminal@0.24.4': + resolution: {integrity: sha512-3fRBWK0IMY293lBx5ycgit1DTMUi+nhOjALHlrIad9hQsqzM9Ak+XdBI1gJ/tZPxW+LraeAc4SsmMdcOflBmAQ==} + engines: {node: '>=20.9.0'} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + + '@rushstack/ts-command-line@5.3.14': + resolution: {integrity: sha512-lT2JKZk2dukBMp4GFOh4RaDfVzpZehGgQOGpzpSliUn317NgEmOOCXyd7/d0eU46HHsbRxizP83GAm39s0lAlg==} + engines: {node: '>=20.9.0'} + '@simple-libs/child-process-utils@2.0.0': resolution: {integrity: sha512-dvNoRKLijXnD0XoJAz94pbNuB5GQgDr55UhpSPhffDkTT0Cmcqh9jSCOtwfT2d4H6MI9E7c4SgtMuJXZ6F3c6A==} engines: {node: '>=22'} @@ -1595,6 +1743,9 @@ packages: '@tybys/wasm-util@0.10.3': resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + '@types/argparse@1.0.38': + resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} + '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -1925,6 +2076,25 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + ajv-draft-04@1.0.0: + resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==} + peerDependencies: + ajv: ^8.5.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@8.18.0: + resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + ajv@8.20.0: resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} @@ -1940,6 +2110,9 @@ packages: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -1982,6 +2155,10 @@ packages: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + cliui@9.0.1: resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==} engines: {node: '>=20'} @@ -1994,6 +2171,11 @@ packages: resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} engines: {node: '>=0.10.0'} + concurrently@10.0.5: + resolution: {integrity: sha512-JaP/CoftUrCcAFW/g//RbgEGwlelnEae6cfBLgH6ZdO6s8jPkn6p9SB9u6pdVxYXoiSnFqseOlHfrEfF82TVOg==} + engines: {node: '>=22'} + hasBin: true + conventional-changelog-angular@9.4.0: resolution: {integrity: sha512-HdxRxuS8bBXVIuo4V82gvSwAXT0vYQUizrjs/izmPg5JdDstr8v8I5hduGL3iQbG+o310dUDxC4+LetuS5hu9w==} engines: {node: '>=22'} @@ -2027,15 +2209,6 @@ packages: typescript: optional: true - debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - defu@6.1.7: resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} @@ -2049,6 +2222,10 @@ packages: diff-match-patch@1.0.5: resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==} + diff@8.0.4: + resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} + engines: {node: '>=0.3.1'} + dts-resolver@3.0.0: resolution: {integrity: sha512-1T1f+z+4tl9XD+m+0HBgWoL/nm0bOIffyWaUuUSBlFg/86IWvfx+wjNaO/ybU0AJzG9/Mi5hBUgGV6zCmWEN7Q==} engines: {node: ^22.18.0 || >=24.0.0} @@ -2075,6 +2252,10 @@ packages: error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + es-module-lexer@2.3.2: resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} @@ -2122,11 +2303,18 @@ packages: picomatch: optional: true + fs-extra@11.3.6: + resolution: {integrity: sha512-w8ZNZr2mKIc7qeNaQ9AVPT1+iFaI+Avd4xudVOvdDJ8VytREi1Ft5Ih7hd9jjehod8vAM5GMsfQ/TpPf4EyoEA==} + engines: {node: '>=14.14'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} @@ -2147,13 +2335,17 @@ packages: resolution: {integrity: sha512-1pgFdhK3J2LeM+dVf2Pd424yHx2ou338lC0ErNP2hPx4j8eW1Sp0XqSjNxtk6Tc4Kr5wlWtSvz8cn2yb7/SG/w==} engines: {node: '>=20'} - globrex@0.1.2: - resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + hookable@6.1.1: resolution: {integrity: sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==} @@ -2169,6 +2361,10 @@ packages: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} + import-lazy@4.0.0: + resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} + engines: {node: '>=8'} + import-without-cache@0.4.0: resolution: {integrity: sha512-NkJQA7oZ4YHQhd2+H3BoRFKF3d/XNsiKpHZCQEMH9pDX27hQQLsTyOocyRgaIVtf8gHX3Nt3LPkR4e5EdtPAGQ==} engines: {node: ^22.18.0 || >=24.0.0} @@ -2180,6 +2376,10 @@ packages: is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-core-module@2.16.2: + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} + engines: {node: '>= 0.4'} + is-plain-obj@4.1.0: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} @@ -2203,6 +2403,9 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true + jju@1.4.0: + resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} + js-tokens@10.0.0: resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} @@ -2219,6 +2422,9 @@ packages: json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + jsonfile@6.2.1: + resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} + lightningcss-android-arm64@1.33.0: resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} engines: {node: '>= 12.0.0'} @@ -2323,6 +2529,10 @@ packages: engines: {node: '>=16'} hasBin: true + minimatch@10.2.3: + resolution: {integrity: sha512-Rwi3pnapEqirPSbWbrZaa6N3nmqq4Xer/2XooiOKyV3q12ML06f7MOuc5DVH8ONZIFhwIYQ3yzPH4nt7iWHaTg==} + engines: {node: 18 || 20 || >=22} + minimatch@10.2.6: resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} engines: {node: 18 || 20 || >=22} @@ -2335,9 +2545,6 @@ packages: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - msgpackr-extract@3.0.4: resolution: {integrity: sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==} hasBin: true @@ -2365,6 +2572,20 @@ packages: resolution: {integrity: sha512-h6QFWd6lBMfjESqgQ27GjzrSDb0qbznp7VDQqp2zvgsrWut4vcchyMIzOVXvGQ2GMZgKw9RWrFNWv9WqGL0p7Q==} engines: {node: ^20.19.0 || >=22.12.0} + oxc-walker@1.1.1: + resolution: {integrity: sha512-Hwd7dq28zu/Er99+bpWp16CGwFrhyalJh0W3JOB7XpPvgWo3MqMi2ksWGKuzzA9zt50mJ2pIUwR5lpAdZ9ACNQ==} + peerDependencies: + '@oxc-project/types': '>=0.98.0' + oxc-parser: '>=0.98.0' + rolldown: '>=1.0.0' + peerDependenciesMeta: + '@oxc-project/types': + optional: true + oxc-parser: + optional: true + rolldown: + optional: true + oxlint-tsgolint@7.0.2001: resolution: {integrity: sha512-KjK/XLcXr1DSyonKhsuFqJRiuKqcyG9j3LJ8nkOsrLzGvodBPqzHOKauy10asLMDI0sUpvb+1sxlzff3udZvfg==} hasBin: true @@ -2393,6 +2614,9 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-scurry@2.0.2: resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} engines: {node: 18 || 20 || >=22} @@ -2450,6 +2674,11 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} + engines: {node: '>= 0.4'} + hasBin: true + rimraf@6.1.3: resolution: {integrity: sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==} engines: {node: 20 || >=22} @@ -2479,11 +2708,23 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + semver@7.8.5: resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} engines: {node: '>=10'} hasBin: true + shell-quote@1.9.0: + resolution: {integrity: sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==} + engines: {node: '>= 0.4'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -2495,6 +2736,13 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -2517,10 +2765,22 @@ packages: resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} engines: {node: '>=12'} + supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} + engines: {node: '>=18'} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + svelte@5.55.1: resolution: {integrity: sha512-QjvU7EFemf6mRzdMGlAFttMWtAAVXrax61SZYHdkD6yoVGQ89VeyKfZD4H1JrV1WLmJBxWhFch9H6ig/87VGjw==} engines: {node: '>=18'} @@ -2548,17 +2808,6 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true - tsconfck@3.1.6: - resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} - engines: {node: ^18 || >=20} - deprecated: unmaintained - hasBin: true - peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true - tsdown@0.23.0: resolution: {integrity: sha512-BaT+ep1xnj5hdyICLd5r5SYYE+TXnI1ATePLykv9vcKpHpFTWUWRH+x1AF/E2qcu3YB6+vI8IdyxIIIffEqw5Q==} engines: {node: ^22.18.0 || ^24.11.0 || >=26.0.0} @@ -2600,6 +2849,15 @@ packages: resolution: {integrity: sha512-AswgMPnpOoaVZHrrSBejETzEbuIA69OVGwfkHwfrY0A23VjWXBANzgq9+OymWOHAIArB7D1+1z498WY8fGg1Jw==} hasBin: true + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + typescript@7.0.2: resolution: {integrity: sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==} engines: {node: '>=16.20.0'} @@ -2615,15 +2873,14 @@ packages: resolution: {integrity: sha512-/y4/bH9YNU5hi9NIrpOuvGXFcxrj3CMrV+/AYpowAYTpHn8gX/XPFjNy766FPoYY0miQhdW977JFWKGNhBdwyQ==} engines: {node: '>=22.19.0'} + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + verkit@0.4.0: resolution: {integrity: sha512-sXMwN6DMHeouPfCxkxWkKAmxphWKEenHYY5H1nIBzU3PmDsmJp6kBXJdshjVdpMZuWCmL9SH7KFRx29AylpP6g==} engines: {node: '>=18.12.0'} - vite-tsconfig-paths@6.1.1: - resolution: {integrity: sha512-2cihq7zliibCCZ8P9cKJrQBkfgdvcFkOOc3Y02o3GWUDLgqjWsZudaoiuOwO/gzTzy17cS5F7ZPo4bsnS4DGkg==} - peerDependencies: - vite: '*' - vite@8.2.2: resolution: {integrity: sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2742,6 +2999,10 @@ packages: resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} engines: {node: ^20.19.0 || ^22.12.0 || >=23} + yargs@18.0.0: + resolution: {integrity: sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + yargs@18.1.0: resolution: {integrity: sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg==} engines: {node: ^20.19.0 || ^22.12.0 || >=23} @@ -2975,6 +3236,41 @@ snapshots: dependencies: '@jsr/std__bytes': 1.0.6 + '@microsoft/api-extractor-model@7.33.12(@types/node@24.13.3)': + dependencies: + '@microsoft/tsdoc': 0.16.0 + '@microsoft/tsdoc-config': 0.18.1 + '@rushstack/node-core-library': 5.24.1(@types/node@24.13.3) + transitivePeerDependencies: + - '@types/node' + + '@microsoft/api-extractor@7.59.1(@types/node@24.13.3)': + dependencies: + '@microsoft/api-extractor-model': 7.33.12(@types/node@24.13.3) + '@microsoft/tsdoc': 0.16.0 + '@microsoft/tsdoc-config': 0.18.1 + '@rushstack/node-core-library': 5.24.1(@types/node@24.13.3) + '@rushstack/rig-package': 0.7.3 + '@rushstack/terminal': 0.24.4(@types/node@24.13.3) + '@rushstack/ts-command-line': 5.3.14(@types/node@24.13.3) + diff: 8.0.4 + minimatch: 10.2.3 + resolve: 1.22.12 + semver: 7.7.4 + source-map: 0.6.1 + typescript: 5.9.3 + transitivePeerDependencies: + - '@types/node' + + '@microsoft/tsdoc-config@0.18.1': + dependencies: + '@microsoft/tsdoc': 0.16.0 + ajv: 8.18.0 + jju: 1.4.0 + resolve: 1.22.12 + + '@microsoft/tsdoc@0.16.0': {} + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4': optional: true @@ -3222,6 +3518,45 @@ snapshots: '@rolldown/pluginutils@1.0.1': {} + '@rushstack/node-core-library@5.24.1(@types/node@24.13.3)': + dependencies: + ajv: 8.20.0 + ajv-draft-04: 1.0.0(ajv@8.20.0) + ajv-formats: 3.0.1(ajv@8.20.0) + fs-extra: 11.3.6 + import-lazy: 4.0.0 + jju: 1.4.0 + resolve: 1.22.12 + semver: 7.7.4 + optionalDependencies: + '@types/node': 24.13.3 + + '@rushstack/problem-matcher@0.2.1(@types/node@24.13.3)': + optionalDependencies: + '@types/node': 24.13.3 + + '@rushstack/rig-package@0.7.3': + dependencies: + jju: 1.4.0 + resolve: 1.22.12 + + '@rushstack/terminal@0.24.4(@types/node@24.13.3)': + dependencies: + '@rushstack/node-core-library': 5.24.1(@types/node@24.13.3) + '@rushstack/problem-matcher': 0.2.1(@types/node@24.13.3) + supports-color: 8.1.1 + optionalDependencies: + '@types/node': 24.13.3 + + '@rushstack/ts-command-line@5.3.14(@types/node@24.13.3)': + dependencies: + '@rushstack/terminal': 0.24.4(@types/node@24.13.3) + '@types/argparse': 1.0.38 + argparse: 1.0.10 + string-argv: 0.3.2 + transitivePeerDependencies: + - '@types/node' + '@simple-libs/child-process-utils@2.0.0': dependencies: '@simple-libs/stream-utils': 2.0.0 @@ -3389,6 +3724,8 @@ snapshots: dependencies: tslib: 2.8.1 + '@types/argparse@1.0.38': {} + '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -3633,6 +3970,21 @@ snapshots: acorn@8.18.0: {} + ajv-draft-04@1.0.0(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + + ajv-formats@3.0.1(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + + ajv@8.18.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.7 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 @@ -3646,6 +3998,10 @@ snapshots: ansi-styles@6.2.3: {} + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + argparse@2.0.1: {} argue-cli@3.2.0: {} @@ -3674,6 +4030,8 @@ snapshots: chai@6.2.2: {} + chalk@5.6.2: {} + cliui@9.0.1: dependencies: string-width: 7.2.0 @@ -3684,6 +4042,15 @@ snapshots: cluster-key-slot@1.1.2: {} + concurrently@10.0.5: + dependencies: + chalk: 5.6.2 + rxjs: 7.8.2 + shell-quote: 1.9.0 + supports-color: 10.2.2 + tree-kill: 1.2.2 + yargs: 18.0.0 + conventional-changelog-angular@9.4.0: dependencies: '@conventional-changelog/template': 1.4.0 @@ -3715,12 +4082,6 @@ snapshots: optionalDependencies: typescript: 7.0.2 - debug@4.4.3(supports-color@7.2.0): - dependencies: - ms: 2.1.3 - optionalDependencies: - supports-color: 7.2.0 - defu@6.1.7: {} detect-libc@2.1.2: {} @@ -3729,6 +4090,8 @@ snapshots: diff-match-patch@1.0.5: {} + diff@8.0.4: {} + dts-resolver@3.0.0: {} effect@4.0.0-rc.112: @@ -3746,6 +4109,8 @@ snapshots: dependencies: is-arrayish: 0.2.1 + es-errors@1.3.0: {} + es-module-lexer@2.3.2: {} es-toolkit@1.52.0: {} @@ -3776,9 +4141,17 @@ snapshots: optionalDependencies: picomatch: 4.0.7 + fs-extra@11.3.6: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.1 + universalify: 2.0.1 + fsevents@2.3.3: optional: true + function-bind@1.1.2: {} + get-caller-file@2.0.5: {} get-east-asian-width@1.6.0: {} @@ -3797,10 +4170,14 @@ snapshots: dependencies: ini: 6.0.0 - globrex@0.1.2: {} + graceful-fs@4.2.11: {} has-flag@4.0.0: {} + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + hookable@6.1.1: {} html-escaper@2.0.2: {} @@ -3812,12 +4189,18 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 + import-lazy@4.0.0: {} + import-without-cache@0.4.0: {} ini@6.0.0: {} is-arrayish@0.2.1: {} + is-core-module@2.16.2: + dependencies: + hasown: 2.0.4 + is-plain-obj@4.1.0: {} is-reference@3.0.3: @@ -3839,6 +4222,8 @@ snapshots: jiti@2.6.1: {} + jju@1.4.0: {} + js-tokens@10.0.0: {} js-tokens@4.0.0: {} @@ -3851,6 +4236,12 @@ snapshots: json-schema-traverse@1.0.0: {} + jsonfile@6.2.1: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + lightningcss-android-arm64@1.33.0: optional: true @@ -3930,6 +4321,10 @@ snapshots: mime@4.1.0: {} + minimatch@10.2.3: + dependencies: + brace-expansion: 5.0.9 + minimatch@10.2.6: dependencies: brace-expansion: 5.0.9 @@ -3938,8 +4333,6 @@ snapshots: mrmime@2.0.1: {} - ms@2.1.3: {} - msgpackr-extract@3.0.4: dependencies: node-gyp-build-optional-packages: 5.2.2 @@ -3992,6 +4385,12 @@ snapshots: '@oxc-parser/binding-win32-ia32-msvc': 0.140.0 '@oxc-parser/binding-win32-x64-msvc': 0.140.0 + oxc-walker@1.1.1(@oxc-project/types@0.140.0)(oxc-parser@0.140.0)(rolldown@1.2.8): + optionalDependencies: + '@oxc-project/types': 0.140.0 + oxc-parser: 0.140.0 + rolldown: 1.2.8 + oxlint-tsgolint@7.0.2001: optionalDependencies: '@oxlint-tsgolint/darwin-arm64': 7.0.2001 @@ -4037,6 +4436,8 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + path-parse@1.0.7: {} + path-scurry@2.0.2: dependencies: lru-cache: 11.5.2 @@ -4085,6 +4486,13 @@ snapshots: resolve-pkg-maps@1.0.0: {} + resolve@1.22.12: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.2 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + rimraf@6.1.3: dependencies: glob: 13.0.6 @@ -4125,8 +4533,16 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.2.8 '@rolldown/binding-win32-x64-msvc': 1.2.8 + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + + semver@7.7.4: {} + semver@7.8.5: {} + shell-quote@1.9.0: {} + siginfo@2.0.0: {} sirv@3.0.2: @@ -4137,6 +4553,10 @@ snapshots: source-map-js@1.2.1: {} + source-map@0.6.1: {} + + sprintf-js@1.0.3: {} + stackback@0.0.2: {} std-env@4.2.0: {} @@ -4158,10 +4578,18 @@ snapshots: dependencies: ansi-regex: 6.3.0 + supports-color@10.2.2: {} + supports-color@7.2.0: dependencies: has-flag: 4.0.0 + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + svelte@5.55.1: dependencies: '@jridgewell/remapping': 2.3.5 @@ -4198,10 +4626,6 @@ snapshots: tree-kill@1.2.2: {} - tsconfck@3.1.6(typescript@7.0.2): - optionalDependencies: - typescript: 7.0.2 - tsdown@0.23.0(typescript@7.0.2): dependencies: cac: 7.0.0 @@ -4237,6 +4661,10 @@ snapshots: '@turbo/windows-64': 2.10.12 '@turbo/windows-arm64': 2.10.12 + type-fest@4.41.0: {} + + typescript@5.9.3: {} + typescript@7.0.2: optionalDependencies: '@typescript/typescript-aix-ppc64': 7.0.2 @@ -4269,17 +4697,9 @@ snapshots: undici@8.10.2: {} - verkit@0.4.0: {} + universalify@2.0.1: {} - vite-tsconfig-paths@6.1.1(supports-color@7.2.0)(typescript@7.0.2)(vite@8.2.2(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.9.0)): - dependencies: - debug: 4.4.3(supports-color@7.2.0) - globrex: 0.1.2 - tsconfck: 3.1.6(typescript@7.0.2) - vite: 8.2.2(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.9.0) - transitivePeerDependencies: - - supports-color - - typescript + verkit@0.4.0: {} vite@8.2.2(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.9.0): dependencies: @@ -4344,6 +4764,15 @@ snapshots: yargs-parser@22.0.0: {} + yargs@18.0.0: + dependencies: + cliui: 9.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + string-width: 7.2.0 + y18n: 5.0.8 + yargs-parser: 22.0.0 + yargs@18.1.0: dependencies: cliui: 9.0.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 3e1a1a73a..8bf9ab017 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -10,6 +10,7 @@ catalog: "@effect/platform-node": 4.0.0-rc.112 "@effect/platform-node-shared": 4.0.0-rc.112 "@effect/vitest": 4.0.0-rc.112 + "@microsoft/api-extractor": ^7.59.1 "@noble/hashes": 2.4.0 "@oxc-parser/binding-wasm32-wasi": ^0.140.0 "@oxc-project/types": ^0.140.0 @@ -20,6 +21,8 @@ catalog: "@systemfsoftware/effect-gherkin-spec": ^4.0.2 "@systemfsoftware/effect-schema-law": ^2.1.1 "@systemfsoftware/effect-schema-vite": ^2.1.1 + "@systemfsoftware/oxlint-plugin": ^4.0.2 + "@systemfsoftware/oxlint-plugin-recommended": ^1.3.1 "@systemfsoftware/tsconfig": ^1.3.4 "@types/node": ^24 "@vitest/browser-playwright": ^4 @@ -28,14 +31,16 @@ catalog: husky: ^9.1.7 lint-staged: ^17.1.0 oxc-parser: ^0.140.0 + oxc-walker: ^1.1.1 oxlint: ^1.77.0 oxlint-tsgolint: 7.0.2001 rimraf: ^6.1.3 tsdown: ^0.23.0 turbo: ^2.10.12 + type-fest: ^4.41.0 typescript: ^7 - vite-tsconfig-paths: ^6.1.1 vitest: ^4 + concurrently: ^10.0.5 catalogMode: prefer catalogs: stryker: @@ -63,3 +68,4 @@ packages: - apps/* - packages/* - packages/toolchain/* + - packages/ignorers/* diff --git a/turbo.json b/turbo.json index 98477a748..952d47bbb 100644 --- a/turbo.json +++ b/turbo.json @@ -11,13 +11,17 @@ "tsconfig.json", "tsconfig.*.json", "package.json", - "tsdown.config.*" + "tsdown.config.*", + "api-extractor.json", + "api-extractor.*.json", + "etc/*.api.md" ], "outputs": [ "dist/**" ], "dependsOn": [ - "^build" + "^build", + "^api:check" ] }, "typecheck": { @@ -31,7 +35,9 @@ "outputs": [], "dependsOn": [ "^build", - "build" + "^api:check", + "build", + "api:check" ], "cache": true }, @@ -52,18 +58,20 @@ "vitest.config.*", "src/**/*.test.ts", "src/**/*.spec.ts", + "$TURBO_ROOT$/packages/toolchain/vitest-config/lib/**", "!**/README.md", "!**/AGENTS.md", - "!**/CHANGELOG.md" + "!**/CHANGELOG.md", + "!**/etc/*.api.md" ], "outputLogs": "errors-only", "dependsOn": [ "^build", - "build" - ], - "outputs": [ - "coverage/**" + "^api:check", + "build", + "api:check" ], + "outputs": [], "env": [ "NODE_ENV", "CI", @@ -77,12 +85,17 @@ "oxlint.config.ts", "tsconfig.json", "tsconfig.*.json", - "!**/*.md" + "$TURBO_ROOT$/packages/toolchain/oxlint-ignorer-config/lib/**", + "$TURBO_ROOT$/packages/toolchain/oxlint-ignorer-config/package.json", + "!**/*.md", + "!**/etc/*.api.md" ], "outputs": [], "dependsOn": [ "^build", - "build" + "^api:check", + "build", + "api:check" ], "env": [ "NODE_ENV", @@ -99,11 +112,33 @@ "package.json" ], "dependsOn": [ - "build" + "^build", + "^api:check", + "build", + "api:check" ] }, "clean": { "cache": false + }, + "api:check": { + "outputLogs": "errors-only", + "inputs": [ + "src/**", + "api-extractor.json", + "api-extractor.*.json", + "tsconfig.build.json", + "tsconfig.*.json", + "etc/*.api.md" + ], + "outputs": [ + "dist/*.d.ts", + "etc/*.api.md", + "temp/**" + ], + "dependsOn": [ + "build" + ] } } }