Skip to main content

fallow_types/
suppress.rs

1//! Inline suppression comment types and issue kind definitions.
2
3pub use crate::issue_meta::{DEAD_CODE_FILTER_FLAGS, KNOWN_ISSUE_KIND_NAMES};
4
5/// Issue kind for suppression matching.
6///
7/// # Examples
8///
9/// ```
10/// use fallow_types::suppress::IssueKind;
11///
12/// let kind = IssueKind::parse("unused-export");
13/// assert_eq!(kind, Some(IssueKind::UnusedExport));
14///
15/// // Round-trip through discriminant
16/// let d = IssueKind::UnusedFile.to_discriminant();
17/// assert_eq!(IssueKind::from_discriminant(d), Some(IssueKind::UnusedFile));
18///
19/// // Unknown strings return None
20/// assert_eq!(IssueKind::parse("not-a-kind"), None);
21/// ```
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum IssueKind {
24    /// An unused file.
25    UnusedFile,
26    /// An unused export.
27    UnusedExport,
28    /// An unused type export.
29    UnusedType,
30    /// An exported signature that references a same-file private type.
31    PrivateTypeLeak,
32    /// An unused dependency.
33    UnusedDependency,
34    /// An unused dev dependency.
35    UnusedDevDependency,
36    /// An unused enum member.
37    UnusedEnumMember,
38    /// An unused class member.
39    UnusedClassMember,
40    /// An unresolved import.
41    UnresolvedImport,
42    /// An unlisted dependency.
43    UnlistedDependency,
44    /// A duplicate export name across modules.
45    DuplicateExport,
46    /// Code duplication.
47    CodeDuplication,
48    /// A circular dependency chain.
49    CircularDependency,
50    /// A cycle or self-loop in the re-export edge subgraph (barrel files
51    /// re-exporting from each other in a loop). Structurally always a bug:
52    /// chain propagation through the cycle is a no-op.
53    ReExportCycle,
54    /// A production dependency only imported via type-only imports.
55    TypeOnlyDependency,
56    /// A production dependency only imported by test files.
57    TestOnlyDependency,
58    /// An import that crosses an architecture boundary.
59    BoundaryViolation,
60    /// A runtime file or export with no test dependency path.
61    CoverageGaps,
62    /// A detected feature flag pattern.
63    FeatureFlag,
64    /// A function exceeding complexity thresholds (health command).
65    Complexity,
66    /// A suppression comment or JSDoc tag that no longer matches any issue.
67    StaleSuppression,
68    /// A pnpm catalog entry in pnpm-workspace.yaml not referenced by any workspace package.
69    PnpmCatalogEntry,
70    /// A named pnpm catalog group in pnpm-workspace.yaml with no entries.
71    EmptyCatalogGroup,
72    /// A workspace package.json reference (`catalog:` / `catalog:<name>`) pointing at
73    /// a catalog that does not declare the consumed package.
74    UnresolvedCatalogReference,
75    /// An entry in pnpm's `overrides:` / `pnpm.overrides` whose target package
76    /// is not declared in any workspace `package.json`.
77    UnusedDependencyOverride,
78    /// An entry in pnpm's `overrides:` / `pnpm.overrides` whose key or value
79    /// cannot be parsed into a valid pnpm shape.
80    MisconfiguredDependencyOverride,
81    /// A `"use client"` file that transitively imports a module reading a
82    /// non-public `process.env` secret (security candidate).
83    SecurityClientServerLeak,
84    /// A syntactic tainted-sink candidate matched against the data-driven
85    /// security matcher catalogue (`security_matchers.toml`). ONE suppression
86    /// token covers all catalogue categories.
87    SecuritySink,
88    /// A banned call or banned import matched by a declarative rule pack
89    /// (`rulePacks` config). The bare token covers every pack rule; scoped
90    /// tokens can target one `<pack>/<rule-id>` identity.
91    PolicyViolation,
92    /// A `"use client"` file that exports a Next.js server-only /
93    /// route-segment config name (e.g. `metadata`, `revalidate`, `GET`).
94    InvalidClientExport,
95    /// A barrel file that re-exports BOTH a `"use client"` origin module AND a
96    /// server-only origin module (Next.js App Router footgun: one import drags
97    /// the other's directive context across the boundary).
98    MixedClientServerBarrel,
99    /// A `"use client"` / `"use server"` directive string written as an
100    /// expression statement after a non-directive statement (an import, a
101    /// const). It is no longer in the leading prologue, so the RSC bundler
102    /// parses it as an ordinary string and silently ignores it.
103    MisplacedDirective,
104    /// A store member (Pinia `state` / `getters` / `actions` key, or a
105    /// setup-store returned key) declared but never accessed by any consumer
106    /// project-wide. Cross-graph: the store binding is imported (the module is
107    /// reachable) yet a specific member is dead.
108    UnusedStoreMember,
109    /// A Vue `inject(KEY)` or Svelte `getContext(KEY)` whose symbol KEY is
110    /// `provide`/`setContext`'d nowhere in the analyzed project. Cross-graph
111    /// dead-half DI link: at runtime the inject returns `undefined`.
112    UnprovidedInject,
113    /// Two or more Next.js App Router route files that resolve to the same URL
114    /// within one app-root (a guaranteed `next build` failure).
115    RouteCollision,
116    /// Sibling Next.js dynamic route segments at one tree position using
117    /// different param spellings (`[id]` vs `[slug]`; a dev / runtime error
118    /// that `next build` does NOT catch).
119    DynamicSegmentNameConflict,
120    /// A component defined in the project that is exported but never rendered
121    /// (no JSX usage) anywhere across the analyzed project.
122    UnrenderedComponent,
123    /// A Vue `<script setup>` `defineProps` declared prop that is referenced
124    /// NOWHERE inside its own single-file component (neither `<script>` nor
125    /// `<template>`). Single-file dead-input direction.
126    UnusedComponentProp,
127    /// A Vue `<script setup>` `defineEmits` declared event that is EMITTED
128    /// nowhere inside its own single-file component (no `emit('<name>')` call).
129    /// Single-file dead-input direction.
130    UnusedComponentEmit,
131    /// An Angular `@Input()` / signal `input()` / `model()` declared input that
132    /// is read NOWHERE inside its own component (neither the inline/external
133    /// template nor the class body). Single-file dead-input direction; the
134    /// Angular analogue of `unused-component-prop`.
135    UnusedComponentInput,
136    /// An Angular `@Output()` / signal `output()` declared output that is
137    /// EMITTED nowhere inside its own component (no `this.<output>.emit(...)`).
138    /// Single-file dead-output direction; the Angular analogue of
139    /// `unused-component-emit`.
140    UnusedComponentOutput,
141    /// A Next.js Server Action (an export of a `"use server"` file) that no code
142    /// in the project references (no import-and-call, no `action={fn}` binding,
143    /// no `<form action={fn}>`). Cross-graph dead-export direction, reclassified
144    /// from `unused-export` for `"use server"` files.
145    UnusedServerAction,
146    /// A SvelteKit `+page.{ts,server.ts,js,server.js}` `load()` return-object key
147    /// that no consumer reads: not off the sibling `+page.svelte`'s `data.<key>`,
148    /// nor project-wide via `page.data.<key>` / `$page.data.<key>`. A dead load
149    /// key runs a real server/DB fetch cost for data nothing renders.
150    UnusedLoadDataKey,
151    /// A React/Preact prop forwarded unchanged through `>= N` intermediate
152    /// pass-through components until a component that substantively consumes it.
153    /// Health signal, rule defaults to `off` (opt-in). Cross-graph: the chain
154    /// spans multiple components / files.
155    PropDrilling,
156    /// A React/Preact component whose entire body is `return <Child {...props}/>`
157    /// (a single spread-forwarded child render, no own value-add): pure
158    /// structural indirection, a candidate for inlining. Health signal, rule
159    /// defaults to `off` (opt-in).
160    ThinWrapper,
161    /// Three or more React/Preact components across two or more files whose
162    /// statically-harvested prop NAME set is identical after stripping ubiquitous
163    /// DOM / passthrough names (a missing shared `Props` type). Health signal,
164    /// rule defaults to `off` (opt-in). Cross-graph: the group spans multiple
165    /// components / files.
166    DuplicatePropShape,
167    /// A Svelte component dispatching a custom event via
168    /// `createEventDispatcher()` whose event name is listened to NOWHERE in the
169    /// analyzed project. Cross-file dead-output direction: the component fires an
170    /// event nothing handles.
171    UnusedSvelteEvent,
172}
173
174impl IssueKind {
175    /// Parse an issue kind from the string tokens used in CLI output and suppression comments.
176    #[must_use]
177    pub fn parse(s: &str) -> Option<Self> {
178        crate::issue_meta::issue_meta_for_token(s).and_then(|meta| meta.kind)
179    }
180
181    /// Convert to a u8 discriminant for compact cache storage.
182    #[must_use]
183    pub const fn to_discriminant(self) -> u8 {
184        match self {
185            Self::UnusedFile => 1,
186            Self::UnusedExport => 2,
187            Self::UnusedType => 3,
188            Self::PrivateTypeLeak => 4,
189            Self::UnusedDependency => 5,
190            Self::UnusedDevDependency => 6,
191            Self::UnusedEnumMember => 7,
192            Self::UnusedClassMember => 8,
193            Self::UnresolvedImport => 9,
194            Self::UnlistedDependency => 10,
195            Self::DuplicateExport => 11,
196            Self::CodeDuplication => 12,
197            Self::CircularDependency => 13,
198            Self::TypeOnlyDependency => 14,
199            Self::TestOnlyDependency => 15,
200            Self::BoundaryViolation => 16,
201            Self::CoverageGaps => 17,
202            Self::FeatureFlag => 18,
203            Self::Complexity => 19,
204            Self::StaleSuppression => 20,
205            Self::PnpmCatalogEntry => 21,
206            Self::UnresolvedCatalogReference => 22,
207            Self::UnusedDependencyOverride => 23,
208            Self::MisconfiguredDependencyOverride => 24,
209            Self::EmptyCatalogGroup => 25,
210            Self::ReExportCycle => 26,
211            Self::SecurityClientServerLeak => 27,
212            Self::SecuritySink => 28,
213            Self::PolicyViolation => 29,
214            Self::InvalidClientExport => 30,
215            Self::MixedClientServerBarrel => 31,
216            Self::MisplacedDirective => 32,
217            Self::UnusedStoreMember => 33,
218            Self::UnprovidedInject => 34,
219            Self::RouteCollision => 35,
220            Self::DynamicSegmentNameConflict => 36,
221            Self::UnrenderedComponent => 37,
222            Self::UnusedComponentProp => 38,
223            Self::UnusedComponentEmit => 39,
224            Self::UnusedServerAction => 40,
225            Self::UnusedLoadDataKey => 41,
226            Self::PropDrilling => 42,
227            Self::ThinWrapper => 43,
228            Self::DuplicatePropShape => 44,
229            Self::UnusedComponentInput => 45,
230            Self::UnusedComponentOutput => 46,
231            Self::UnusedSvelteEvent => 47,
232        }
233    }
234
235    /// Reconstruct from a cache discriminant.
236    #[must_use]
237    pub const fn from_discriminant(d: u8) -> Option<Self> {
238        match d {
239            1 => Some(Self::UnusedFile),
240            2 => Some(Self::UnusedExport),
241            3 => Some(Self::UnusedType),
242            4 => Some(Self::PrivateTypeLeak),
243            5 => Some(Self::UnusedDependency),
244            6 => Some(Self::UnusedDevDependency),
245            7 => Some(Self::UnusedEnumMember),
246            8 => Some(Self::UnusedClassMember),
247            9 => Some(Self::UnresolvedImport),
248            10 => Some(Self::UnlistedDependency),
249            11 => Some(Self::DuplicateExport),
250            12 => Some(Self::CodeDuplication),
251            13 => Some(Self::CircularDependency),
252            14 => Some(Self::TypeOnlyDependency),
253            15 => Some(Self::TestOnlyDependency),
254            16 => Some(Self::BoundaryViolation),
255            17 => Some(Self::CoverageGaps),
256            18 => Some(Self::FeatureFlag),
257            19 => Some(Self::Complexity),
258            20 => Some(Self::StaleSuppression),
259            21 => Some(Self::PnpmCatalogEntry),
260            22 => Some(Self::UnresolvedCatalogReference),
261            23 => Some(Self::UnusedDependencyOverride),
262            24 => Some(Self::MisconfiguredDependencyOverride),
263            25 => Some(Self::EmptyCatalogGroup),
264            26 => Some(Self::ReExportCycle),
265            27 => Some(Self::SecurityClientServerLeak),
266            28 => Some(Self::SecuritySink),
267            29 => Some(Self::PolicyViolation),
268            30 => Some(Self::InvalidClientExport),
269            31 => Some(Self::MixedClientServerBarrel),
270            32 => Some(Self::MisplacedDirective),
271            33 => Some(Self::UnusedStoreMember),
272            34 => Some(Self::UnprovidedInject),
273            35 => Some(Self::RouteCollision),
274            36 => Some(Self::DynamicSegmentNameConflict),
275            37 => Some(Self::UnrenderedComponent),
276            38 => Some(Self::UnusedComponentProp),
277            39 => Some(Self::UnusedComponentEmit),
278            40 => Some(Self::UnusedServerAction),
279            41 => Some(Self::UnusedLoadDataKey),
280            42 => Some(Self::PropDrilling),
281            43 => Some(Self::ThinWrapper),
282            44 => Some(Self::DuplicatePropShape),
283            45 => Some(Self::UnusedComponentInput),
284            46 => Some(Self::UnusedComponentOutput),
285            47 => Some(Self::UnusedSvelteEvent),
286            _ => None,
287        }
288    }
289}
290
291/// One scoped rule-pack policy suppression target.
292#[derive(Debug, Clone, PartialEq, Eq, Hash)]
293pub struct PolicyRuleSuppression {
294    /// Rule-pack name.
295    pub pack: String,
296    /// Rule id within the pack.
297    pub rule_id: String,
298}
299
300impl PolicyRuleSuppression {
301    /// Build a scoped policy suppression target.
302    #[must_use]
303    pub fn new(pack: impl Into<String>, rule_id: impl Into<String>) -> Self {
304        Self {
305            pack: pack.into(),
306            rule_id: rule_id.into(),
307        }
308    }
309
310    /// Canonical suppression token.
311    #[must_use]
312    pub fn token(&self) -> String {
313        format!("policy-violation:{}/{}", self.pack, self.rule_id)
314    }
315}
316
317/// A specific suppression target parsed from a comment token.
318#[derive(Debug, Clone, PartialEq, Eq)]
319pub enum SuppressionTarget {
320    /// A regular issue-kind token such as `unused-export` or bare
321    /// `policy-violation`.
322    Issue(IssueKind),
323    /// A scoped rule-pack policy token such as
324    /// `policy-violation:team-policy/no-child-process`.
325    PolicyRule(PolicyRuleSuppression),
326}
327
328impl SuppressionTarget {
329    /// Return the regular issue kind when this target is a bare issue-kind
330    /// token.
331    #[must_use]
332    pub const fn issue_kind(&self) -> Option<IssueKind> {
333        match self {
334            Self::Issue(kind) => Some(*kind),
335            Self::PolicyRule(_) => None,
336        }
337    }
338
339    /// Canonical suppression token for output and active-suppression capture.
340    #[must_use]
341    pub fn token(&self) -> String {
342        match self {
343            Self::Issue(kind) => issue_kind_to_kebab(*kind).to_owned(),
344            Self::PolicyRule(rule) => rule.token(),
345        }
346    }
347}
348
349/// Convert an [`IssueKind`] to its canonical suppression token.
350#[must_use]
351pub const fn issue_kind_to_kebab(kind: IssueKind) -> &'static str {
352    match kind {
353        IssueKind::UnusedFile => "unused-file",
354        IssueKind::UnusedExport => "unused-export",
355        IssueKind::UnusedType => "unused-type",
356        IssueKind::PrivateTypeLeak => "private-type-leak",
357        IssueKind::UnusedDependency => "unused-dependency",
358        IssueKind::UnusedDevDependency => "unused-dev-dependency",
359        IssueKind::UnusedEnumMember => "unused-enum-member",
360        IssueKind::UnusedClassMember => "unused-class-member",
361        IssueKind::UnresolvedImport => "unresolved-import",
362        IssueKind::UnlistedDependency => "unlisted-dependency",
363        IssueKind::DuplicateExport => "duplicate-export",
364        IssueKind::CodeDuplication => "code-duplication",
365        IssueKind::CircularDependency => "circular-dependency",
366        IssueKind::ReExportCycle => "re-export-cycle",
367        IssueKind::TypeOnlyDependency => "type-only-dependency",
368        IssueKind::TestOnlyDependency => "test-only-dependency",
369        IssueKind::BoundaryViolation => "boundary-violation",
370        IssueKind::CoverageGaps => "coverage-gaps",
371        IssueKind::FeatureFlag => "feature-flag",
372        IssueKind::Complexity => "complexity",
373        IssueKind::StaleSuppression => "stale-suppression",
374        IssueKind::PnpmCatalogEntry => "unused-catalog-entry",
375        IssueKind::EmptyCatalogGroup => "empty-catalog-group",
376        IssueKind::UnresolvedCatalogReference => "unresolved-catalog-reference",
377        IssueKind::UnusedDependencyOverride => "unused-dependency-override",
378        IssueKind::MisconfiguredDependencyOverride => "misconfigured-dependency-override",
379        IssueKind::SecurityClientServerLeak => "security-client-server-leak",
380        IssueKind::SecuritySink => "security-sink",
381        IssueKind::PolicyViolation => "policy-violation",
382        IssueKind::InvalidClientExport => "invalid-client-export",
383        IssueKind::MixedClientServerBarrel => "mixed-client-server-barrel",
384        IssueKind::MisplacedDirective => "misplaced-directive",
385        IssueKind::UnusedStoreMember => "unused-store-member",
386        IssueKind::UnprovidedInject => "unprovided-inject",
387        IssueKind::RouteCollision => "route-collision",
388        IssueKind::DynamicSegmentNameConflict => "dynamic-segment-name-conflict",
389        IssueKind::UnrenderedComponent => "unrendered-component",
390        IssueKind::UnusedComponentProp => "unused-component-prop",
391        IssueKind::UnusedComponentEmit => "unused-component-emit",
392        IssueKind::UnusedComponentInput => "unused-component-input",
393        IssueKind::UnusedComponentOutput => "unused-component-output",
394        IssueKind::UnusedServerAction => "unused-server-action",
395        IssueKind::UnusedLoadDataKey => "unused-load-data-key",
396        IssueKind::PropDrilling => "prop-drilling",
397        IssueKind::ThinWrapper => "thin-wrapper",
398        IssueKind::DuplicatePropShape => "duplicate-prop-shape",
399        IssueKind::UnusedSvelteEvent => "unused-svelte-event",
400    }
401}
402
403/// Parse a suppression token into a structured target.
404#[must_use]
405pub fn parse_suppression_target(token: &str) -> Option<SuppressionTarget> {
406    parse_policy_rule_suppression_token(token)
407        .map(SuppressionTarget::PolicyRule)
408        .or_else(|| IssueKind::parse(token).map(SuppressionTarget::Issue))
409}
410
411/// Parse canonical scoped policy suppression tokens.
412///
413/// The plural prefix is accepted for consistency with the bare legacy alias,
414/// but output always uses singular `policy-violation:`.
415#[must_use]
416pub fn parse_policy_rule_suppression_token(token: &str) -> Option<PolicyRuleSuppression> {
417    let identity = token
418        .strip_prefix("policy-violation:")
419        .or_else(|| token.strip_prefix("policy-violations:"))?;
420    let (pack, rule_id) = identity.split_once('/')?;
421    if rule_id.contains('/') {
422        return None;
423    }
424    if !is_valid_policy_identifier(pack) || !is_valid_policy_identifier(rule_id) {
425        return None;
426    }
427    Some(PolicyRuleSuppression::new(pack, rule_id))
428}
429
430/// Whether a rule-pack name or rule id can be used inside
431/// `policy-violation:<pack>/<rule-id>` without escaping.
432#[must_use]
433pub fn is_valid_policy_identifier(value: &str) -> bool {
434    !value.is_empty()
435        && value
436            .bytes()
437            .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'_' | b'-'))
438}
439
440/// A suppression directive parsed from a source comment.
441///
442/// # Examples
443///
444/// ```
445/// use fallow_types::suppress::{Suppression, IssueKind};
446///
447/// // File-wide suppression (line 0, no specific kind)
448/// let file_wide = Suppression::all(0, 1);
449/// assert_eq!(file_wide.line, 0);
450///
451/// // Line-specific suppression for unused exports
452/// let line_suppress = Suppression::issue(42, 41, IssueKind::UnusedExport);
453/// assert_eq!(line_suppress.issue_kind_target(), Some(IssueKind::UnusedExport));
454/// ```
455#[derive(Debug, Clone)]
456pub struct Suppression {
457    /// 1-based line this suppression applies to. 0 = file-wide suppression.
458    pub line: u32,
459    /// 1-based line where the suppression comment itself appears.
460    /// For `fallow-ignore-next-line`, this is `line - 1`.
461    /// For `fallow-ignore-file`, this is the actual line of the comment in the source.
462    pub comment_line: u32,
463    /// None = suppress all issue kinds on this line or file.
464    pub target: Option<SuppressionTarget>,
465    /// Human-authored reason after `--`, when present.
466    pub reason: Option<String>,
467}
468
469impl Suppression {
470    /// Build a blanket suppression.
471    #[must_use]
472    pub const fn all(line: u32, comment_line: u32) -> Self {
473        Self {
474            line,
475            comment_line,
476            target: None,
477            reason: None,
478        }
479    }
480
481    /// Build a regular issue-kind suppression.
482    #[must_use]
483    pub const fn issue(line: u32, comment_line: u32, kind: IssueKind) -> Self {
484        Self {
485            line,
486            comment_line,
487            target: Some(SuppressionTarget::Issue(kind)),
488            reason: None,
489        }
490    }
491
492    /// Build a scoped rule-pack policy suppression.
493    #[must_use]
494    pub fn policy_rule(
495        line: u32,
496        comment_line: u32,
497        pack: impl Into<String>,
498        rule_id: impl Into<String>,
499    ) -> Self {
500        Self {
501            line,
502            comment_line,
503            target: Some(SuppressionTarget::PolicyRule(PolicyRuleSuppression::new(
504                pack, rule_id,
505            ))),
506            reason: None,
507        }
508    }
509
510    /// Return a copy with a parsed suppression reason attached.
511    #[must_use]
512    pub fn with_reason(mut self, reason: Option<String>) -> Self {
513        self.reason = reason;
514        self
515    }
516
517    /// The bare issue kind if this suppression targets one.
518    #[must_use]
519    pub const fn issue_kind_target(&self) -> Option<IssueKind> {
520        match &self.target {
521            Some(SuppressionTarget::Issue(kind)) => Some(*kind),
522            Some(SuppressionTarget::PolicyRule(_)) | None => None,
523        }
524    }
525
526    /// The scoped policy target if this suppression targets one rule-pack rule.
527    #[must_use]
528    pub const fn policy_rule_target(&self) -> Option<&PolicyRuleSuppression> {
529        match &self.target {
530            Some(SuppressionTarget::PolicyRule(rule)) => Some(rule),
531            Some(SuppressionTarget::Issue(_)) | None => None,
532        }
533    }
534
535    /// Canonical token for this suppression, or `None` for blanket comments.
536    #[must_use]
537    pub fn target_token(&self) -> Option<String> {
538        self.target.as_ref().map(SuppressionTarget::token)
539    }
540
541    /// Whether the comment applies to `line`.
542    #[must_use]
543    pub const fn applies_to_line(&self, line: u32) -> bool {
544        self.line == 0 || self.line == line
545    }
546
547    /// Whether this suppression covers a regular issue kind on a line.
548    ///
549    /// Scoped policy-rule targets intentionally do not match this generic
550    /// predicate. Policy detection uses [`Self::matches_policy_rule`] so the
551    /// exact pack and rule id are available.
552    #[must_use]
553    pub fn matches_issue_kind(&self, line: u32, kind: IssueKind) -> bool {
554        self.applies_to_line(line)
555            && match &self.target {
556                None => true,
557                Some(SuppressionTarget::Issue(target_kind)) => *target_kind == kind,
558                Some(SuppressionTarget::PolicyRule(_)) => false,
559            }
560    }
561
562    /// Whether this suppression covers a policy finding on a line.
563    #[must_use]
564    pub fn matches_policy_rule(&self, line: u32, pack: &str, rule_id: &str) -> bool {
565        self.applies_to_line(line)
566            && match &self.target {
567                None | Some(SuppressionTarget::Issue(IssueKind::PolicyViolation)) => true,
568                Some(SuppressionTarget::Issue(_)) => false,
569                Some(SuppressionTarget::PolicyRule(target)) => {
570                    target.pack == pack && target.rule_id == rule_id
571                }
572            }
573    }
574}
575
576/// A suppression token that did not parse to any known `IssueKind`.
577///
578/// Emitted alongside `Suppression` when a `// fallow-ignore-*` marker contains
579/// a typo or an obsolete issue-kind name. The known tokens on the same marker
580/// are recorded as normal `Suppression` entries; this struct preserves the
581/// unknown token so the downstream `find_stale` pass can surface it as a
582/// `StaleSuppression` finding with `kind_known: false`. Without this, the
583/// entire suppression line would be discarded silently. See issue #449.
584#[derive(Debug, Clone)]
585pub struct UnknownSuppressionKind {
586    /// 1-based line where the suppression comment itself appears.
587    pub comment_line: u32,
588    /// Whether the marker was `fallow-ignore-file` (`true`) or
589    /// `fallow-ignore-next-line` (`false`).
590    pub is_file_level: bool,
591    /// The verbatim token from the marker that did not parse.
592    pub token: String,
593    /// Human-authored reason after `--`, when present.
594    pub reason: Option<String>,
595}
596
597/// Levenshtein edit distance between two ASCII-leaning strings.
598///
599/// Local duplicate of the config-crate helper (see
600/// `crates/config/src/config/rules.rs::levenshtein`) so `fallow-types` can
601/// compute "did you mean?" suggestions for unknown suppression tokens without
602/// taking a dependency on `fallow-config`. Issue-kind names are short
603/// (max ~33 chars) so allocation cost is negligible.
604fn levenshtein(a: &str, b: &str) -> usize {
605    let a_bytes = a.as_bytes();
606    let b_bytes = b.as_bytes();
607    let (a_len, b_len) = (a_bytes.len(), b_bytes.len());
608
609    if a_len == 0 {
610        return b_len;
611    }
612    if b_len == 0 {
613        return a_len;
614    }
615
616    let mut prev: Vec<usize> = (0..=b_len).collect();
617    let mut curr: Vec<usize> = vec![0; b_len + 1];
618
619    for i in 1..=a_len {
620        curr[0] = i;
621        for j in 1..=b_len {
622            let cost = usize::from(a_bytes[i - 1] != b_bytes[j - 1]);
623            curr[j] = (prev[j] + 1).min(curr[j - 1] + 1).min(prev[j - 1] + cost);
624        }
625        std::mem::swap(&mut prev, &mut curr);
626    }
627
628    prev[b_len]
629}
630
631/// Find the closest known issue-kind name to `input` when it is plausibly a typo.
632///
633/// Returns the best match when the Levenshtein distance is at most 2 AND
634/// the input is long enough that the match is not coincidental
635/// (`input.len() / 2 > distance`). Returns `None` for completely novel
636/// strings where a suggestion would be misleading.
637#[must_use]
638pub fn closest_known_kind_name(input: &str) -> Option<&'static str> {
639    let input_lower = input.to_ascii_lowercase();
640    let mut best: Option<(&'static str, usize)> = None;
641
642    for &candidate in KNOWN_ISSUE_KIND_NAMES {
643        let d = levenshtein(&input_lower, candidate);
644        if best.is_none_or(|(_, b_dist)| d < b_dist) {
645            best = Some((candidate, d));
646        }
647    }
648
649    best.filter(|&(_, d)| d > 0 && d <= 2 && input_lower.len() / 2 > d)
650        .map(|(name, _)| name)
651}
652
653const _: () = assert!(std::mem::size_of::<IssueKind>() == 1);
654
655#[cfg(test)]
656mod tests {
657    use super::*;
658
659    #[test]
660    #[expect(
661        clippy::too_many_lines,
662        reason = "exhaustive per-variant parse assertions; one block per issue kind"
663    )]
664    fn issue_kind_from_str_all_variants() {
665        assert_eq!(IssueKind::parse("unused-file"), Some(IssueKind::UnusedFile));
666        assert_eq!(
667            IssueKind::parse("unused-export"),
668            Some(IssueKind::UnusedExport)
669        );
670        assert_eq!(IssueKind::parse("unused-type"), Some(IssueKind::UnusedType));
671        assert_eq!(
672            IssueKind::parse("private-type-leak"),
673            Some(IssueKind::PrivateTypeLeak)
674        );
675        assert_eq!(
676            IssueKind::parse("unused-dependency"),
677            Some(IssueKind::UnusedDependency)
678        );
679        assert_eq!(
680            IssueKind::parse("unused-dev-dependency"),
681            Some(IssueKind::UnusedDevDependency)
682        );
683        assert_eq!(
684            IssueKind::parse("unused-enum-member"),
685            Some(IssueKind::UnusedEnumMember)
686        );
687        assert_eq!(
688            IssueKind::parse("unused-class-member"),
689            Some(IssueKind::UnusedClassMember)
690        );
691        assert_eq!(
692            IssueKind::parse("unresolved-import"),
693            Some(IssueKind::UnresolvedImport)
694        );
695        assert_eq!(
696            IssueKind::parse("unlisted-dependency"),
697            Some(IssueKind::UnlistedDependency)
698        );
699        assert_eq!(
700            IssueKind::parse("duplicate-export"),
701            Some(IssueKind::DuplicateExport)
702        );
703        assert_eq!(
704            IssueKind::parse("code-duplication"),
705            Some(IssueKind::CodeDuplication)
706        );
707        assert_eq!(
708            IssueKind::parse("circular-dependency"),
709            Some(IssueKind::CircularDependency)
710        );
711        assert_eq!(
712            IssueKind::parse("circular-dependencies"),
713            Some(IssueKind::CircularDependency)
714        );
715        assert_eq!(
716            IssueKind::parse("type-only-dependency"),
717            Some(IssueKind::TypeOnlyDependency)
718        );
719        assert_eq!(
720            IssueKind::parse("test-only-dependency"),
721            Some(IssueKind::TestOnlyDependency)
722        );
723        assert_eq!(
724            IssueKind::parse("boundary-violation"),
725            Some(IssueKind::BoundaryViolation)
726        );
727        // The boundary family token also accepts the rule-id-shaped alias so
728        // users who derive the token from the `boundary-call-violation` rule
729        // id by analogy get a working suppression instead of a silent no-op.
730        assert_eq!(
731            IssueKind::parse("boundary-call-violation"),
732            Some(IssueKind::BoundaryViolation)
733        );
734        assert_eq!(
735            IssueKind::parse("boundary-call-violations"),
736            Some(IssueKind::BoundaryViolation)
737        );
738        assert_eq!(
739            IssueKind::parse("coverage-gaps"),
740            Some(IssueKind::CoverageGaps)
741        );
742        assert_eq!(
743            IssueKind::parse("feature-flag"),
744            Some(IssueKind::FeatureFlag)
745        );
746        assert_eq!(IssueKind::parse("complexity"), Some(IssueKind::Complexity));
747        assert_eq!(
748            IssueKind::parse("stale-suppression"),
749            Some(IssueKind::StaleSuppression)
750        );
751        assert_eq!(
752            IssueKind::parse("unused-catalog-entry"),
753            Some(IssueKind::PnpmCatalogEntry)
754        );
755        assert_eq!(
756            IssueKind::parse("unused-catalog-entries"),
757            Some(IssueKind::PnpmCatalogEntry)
758        );
759        assert_eq!(
760            IssueKind::parse("empty-catalog-group"),
761            Some(IssueKind::EmptyCatalogGroup)
762        );
763        assert_eq!(
764            IssueKind::parse("empty-catalog-groups"),
765            Some(IssueKind::EmptyCatalogGroup)
766        );
767        assert_eq!(
768            IssueKind::parse("unresolved-catalog-reference"),
769            Some(IssueKind::UnresolvedCatalogReference)
770        );
771        assert_eq!(
772            IssueKind::parse("unresolved-catalog-references"),
773            Some(IssueKind::UnresolvedCatalogReference)
774        );
775        assert_eq!(
776            IssueKind::parse("unused-dependency-override"),
777            Some(IssueKind::UnusedDependencyOverride)
778        );
779        assert_eq!(
780            IssueKind::parse("unused-dependency-overrides"),
781            Some(IssueKind::UnusedDependencyOverride)
782        );
783        assert_eq!(
784            IssueKind::parse("misconfigured-dependency-override"),
785            Some(IssueKind::MisconfiguredDependencyOverride)
786        );
787        assert_eq!(
788            IssueKind::parse("misconfigured-dependency-overrides"),
789            Some(IssueKind::MisconfiguredDependencyOverride)
790        );
791        assert_eq!(
792            IssueKind::parse("security-client-server-leak"),
793            Some(IssueKind::SecurityClientServerLeak)
794        );
795        assert_eq!(
796            IssueKind::parse("security-sink"),
797            Some(IssueKind::SecuritySink)
798        );
799        assert_eq!(
800            IssueKind::parse("policy-violation"),
801            Some(IssueKind::PolicyViolation)
802        );
803        assert_eq!(
804            IssueKind::parse("policy-violations"),
805            Some(IssueKind::PolicyViolation)
806        );
807        assert_eq!(
808            IssueKind::parse("invalid-client-export"),
809            Some(IssueKind::InvalidClientExport)
810        );
811        assert_eq!(
812            IssueKind::parse("invalid-client-exports"),
813            Some(IssueKind::InvalidClientExport)
814        );
815        assert_eq!(
816            IssueKind::parse("mixed-client-server-barrel"),
817            Some(IssueKind::MixedClientServerBarrel)
818        );
819        assert_eq!(
820            IssueKind::parse("mixed-client-server-barrels"),
821            Some(IssueKind::MixedClientServerBarrel)
822        );
823        assert_eq!(
824            IssueKind::parse("misplaced-directive"),
825            Some(IssueKind::MisplacedDirective)
826        );
827        assert_eq!(
828            IssueKind::parse("misplaced-directives"),
829            Some(IssueKind::MisplacedDirective)
830        );
831        assert_eq!(
832            IssueKind::parse("route-collision"),
833            Some(IssueKind::RouteCollision)
834        );
835        assert_eq!(
836            IssueKind::parse("route-collisions"),
837            Some(IssueKind::RouteCollision)
838        );
839        assert_eq!(
840            IssueKind::parse("dynamic-segment-name-conflict"),
841            Some(IssueKind::DynamicSegmentNameConflict)
842        );
843        assert_eq!(
844            IssueKind::parse("dynamic-segment-name-conflicts"),
845            Some(IssueKind::DynamicSegmentNameConflict)
846        );
847        assert_eq!(
848            IssueKind::parse("unrendered-component"),
849            Some(IssueKind::UnrenderedComponent)
850        );
851        assert_eq!(
852            IssueKind::parse("unrendered-components"),
853            Some(IssueKind::UnrenderedComponent)
854        );
855        assert_eq!(
856            IssueKind::parse("unused-component-prop"),
857            Some(IssueKind::UnusedComponentProp)
858        );
859        assert_eq!(
860            IssueKind::parse("unused-component-props"),
861            Some(IssueKind::UnusedComponentProp)
862        );
863        assert_eq!(
864            IssueKind::parse("unused-component-emit"),
865            Some(IssueKind::UnusedComponentEmit)
866        );
867        assert_eq!(
868            IssueKind::parse("unused-component-emits"),
869            Some(IssueKind::UnusedComponentEmit)
870        );
871        assert_eq!(
872            IssueKind::parse("unused-component-input"),
873            Some(IssueKind::UnusedComponentInput)
874        );
875        assert_eq!(
876            IssueKind::parse("unused-component-inputs"),
877            Some(IssueKind::UnusedComponentInput)
878        );
879        assert_eq!(
880            IssueKind::parse("unused-component-output"),
881            Some(IssueKind::UnusedComponentOutput)
882        );
883        assert_eq!(
884            IssueKind::parse("unused-component-outputs"),
885            Some(IssueKind::UnusedComponentOutput)
886        );
887        assert_eq!(
888            IssueKind::parse("unused-load-data-key"),
889            Some(IssueKind::UnusedLoadDataKey)
890        );
891        assert_eq!(
892            IssueKind::parse("unused-load-data-keys"),
893            Some(IssueKind::UnusedLoadDataKey)
894        );
895        assert_eq!(
896            IssueKind::parse("prop-drilling"),
897            Some(IssueKind::PropDrilling)
898        );
899        assert_eq!(
900            IssueKind::parse("unused-svelte-event"),
901            Some(IssueKind::UnusedSvelteEvent)
902        );
903        assert_eq!(
904            IssueKind::parse("unused-svelte-events"),
905            Some(IssueKind::UnusedSvelteEvent)
906        );
907    }
908
909    #[test]
910    fn issue_kind_from_str_unknown() {
911        assert_eq!(IssueKind::parse("foo"), None);
912        assert_eq!(IssueKind::parse(""), None);
913    }
914
915    #[test]
916    fn issue_kind_from_str_near_misses() {
917        assert_eq!(IssueKind::parse("Unused-File"), None);
918        assert_eq!(IssueKind::parse("UNUSED-EXPORT"), None);
919        assert_eq!(IssueKind::parse("unused_file"), None);
920        assert_eq!(IssueKind::parse("unused-files"), None);
921    }
922
923    #[test]
924    fn discriminant_out_of_range() {
925        assert_eq!(IssueKind::from_discriminant(0), None);
926        assert_eq!(
927            IssueKind::from_discriminant(29),
928            Some(IssueKind::PolicyViolation)
929        );
930        assert_eq!(
931            IssueKind::from_discriminant(30),
932            Some(IssueKind::InvalidClientExport)
933        );
934        assert_eq!(
935            IssueKind::from_discriminant(31),
936            Some(IssueKind::MixedClientServerBarrel)
937        );
938        assert_eq!(
939            IssueKind::from_discriminant(32),
940            Some(IssueKind::MisplacedDirective)
941        );
942        assert_eq!(
943            IssueKind::from_discriminant(33),
944            Some(IssueKind::UnusedStoreMember)
945        );
946        assert_eq!(
947            IssueKind::from_discriminant(34),
948            Some(IssueKind::UnprovidedInject)
949        );
950        assert_eq!(
951            IssueKind::from_discriminant(35),
952            Some(IssueKind::RouteCollision)
953        );
954        assert_eq!(
955            IssueKind::from_discriminant(36),
956            Some(IssueKind::DynamicSegmentNameConflict)
957        );
958        assert_eq!(
959            IssueKind::from_discriminant(37),
960            Some(IssueKind::UnrenderedComponent)
961        );
962        assert_eq!(
963            IssueKind::from_discriminant(38),
964            Some(IssueKind::UnusedComponentProp)
965        );
966        assert_eq!(
967            IssueKind::from_discriminant(39),
968            Some(IssueKind::UnusedComponentEmit)
969        );
970        assert_eq!(
971            IssueKind::from_discriminant(40),
972            Some(IssueKind::UnusedServerAction)
973        );
974        assert_eq!(
975            IssueKind::from_discriminant(41),
976            Some(IssueKind::UnusedLoadDataKey)
977        );
978        assert_eq!(
979            IssueKind::from_discriminant(42),
980            Some(IssueKind::PropDrilling)
981        );
982        assert_eq!(
983            IssueKind::from_discriminant(43),
984            Some(IssueKind::ThinWrapper)
985        );
986        assert_eq!(
987            IssueKind::from_discriminant(44),
988            Some(IssueKind::DuplicatePropShape)
989        );
990        assert_eq!(
991            IssueKind::from_discriminant(45),
992            Some(IssueKind::UnusedComponentInput)
993        );
994        assert_eq!(
995            IssueKind::from_discriminant(46),
996            Some(IssueKind::UnusedComponentOutput)
997        );
998        assert_eq!(
999            IssueKind::from_discriminant(47),
1000            Some(IssueKind::UnusedSvelteEvent)
1001        );
1002        assert_eq!(IssueKind::from_discriminant(48), None);
1003        assert_eq!(IssueKind::from_discriminant(u8::MAX), None);
1004    }
1005
1006    #[test]
1007    fn discriminant_roundtrip() {
1008        for kind in [
1009            IssueKind::UnusedFile,
1010            IssueKind::UnusedExport,
1011            IssueKind::UnusedType,
1012            IssueKind::PrivateTypeLeak,
1013            IssueKind::UnusedDependency,
1014            IssueKind::UnusedDevDependency,
1015            IssueKind::UnusedEnumMember,
1016            IssueKind::UnusedClassMember,
1017            IssueKind::UnresolvedImport,
1018            IssueKind::UnlistedDependency,
1019            IssueKind::DuplicateExport,
1020            IssueKind::CodeDuplication,
1021            IssueKind::CircularDependency,
1022            IssueKind::ReExportCycle,
1023            IssueKind::TypeOnlyDependency,
1024            IssueKind::TestOnlyDependency,
1025            IssueKind::BoundaryViolation,
1026            IssueKind::CoverageGaps,
1027            IssueKind::FeatureFlag,
1028            IssueKind::Complexity,
1029            IssueKind::StaleSuppression,
1030            IssueKind::PnpmCatalogEntry,
1031            IssueKind::EmptyCatalogGroup,
1032            IssueKind::UnresolvedCatalogReference,
1033            IssueKind::UnusedDependencyOverride,
1034            IssueKind::MisconfiguredDependencyOverride,
1035            IssueKind::SecurityClientServerLeak,
1036            IssueKind::SecuritySink,
1037            IssueKind::PolicyViolation,
1038            IssueKind::InvalidClientExport,
1039            IssueKind::MixedClientServerBarrel,
1040            IssueKind::MisplacedDirective,
1041            IssueKind::UnusedStoreMember,
1042            IssueKind::UnprovidedInject,
1043            IssueKind::RouteCollision,
1044            IssueKind::DynamicSegmentNameConflict,
1045            IssueKind::UnrenderedComponent,
1046            IssueKind::UnusedComponentProp,
1047            IssueKind::UnusedComponentEmit,
1048            IssueKind::UnusedServerAction,
1049            IssueKind::UnusedLoadDataKey,
1050            IssueKind::PropDrilling,
1051            IssueKind::ThinWrapper,
1052            IssueKind::DuplicatePropShape,
1053            IssueKind::UnusedComponentInput,
1054            IssueKind::UnusedComponentOutput,
1055            IssueKind::UnusedSvelteEvent,
1056        ] {
1057            assert_eq!(
1058                IssueKind::from_discriminant(kind.to_discriminant()),
1059                Some(kind)
1060            );
1061        }
1062        assert_eq!(IssueKind::from_discriminant(0), None);
1063        assert_eq!(IssueKind::from_discriminant(48), None);
1064    }
1065
1066    #[test]
1067    fn discriminant_values_are_unique() {
1068        let all_kinds = [
1069            IssueKind::UnusedFile,
1070            IssueKind::UnusedExport,
1071            IssueKind::UnusedType,
1072            IssueKind::PrivateTypeLeak,
1073            IssueKind::UnusedDependency,
1074            IssueKind::UnusedDevDependency,
1075            IssueKind::UnusedEnumMember,
1076            IssueKind::UnusedClassMember,
1077            IssueKind::UnresolvedImport,
1078            IssueKind::UnlistedDependency,
1079            IssueKind::DuplicateExport,
1080            IssueKind::CodeDuplication,
1081            IssueKind::CircularDependency,
1082            IssueKind::ReExportCycle,
1083            IssueKind::TypeOnlyDependency,
1084            IssueKind::TestOnlyDependency,
1085            IssueKind::BoundaryViolation,
1086            IssueKind::CoverageGaps,
1087            IssueKind::FeatureFlag,
1088            IssueKind::Complexity,
1089            IssueKind::StaleSuppression,
1090            IssueKind::PnpmCatalogEntry,
1091            IssueKind::EmptyCatalogGroup,
1092            IssueKind::UnresolvedCatalogReference,
1093            IssueKind::UnusedDependencyOverride,
1094            IssueKind::MisconfiguredDependencyOverride,
1095            IssueKind::SecurityClientServerLeak,
1096            IssueKind::SecuritySink,
1097            IssueKind::PolicyViolation,
1098            IssueKind::InvalidClientExport,
1099            IssueKind::MixedClientServerBarrel,
1100            IssueKind::MisplacedDirective,
1101            IssueKind::UnusedStoreMember,
1102            IssueKind::UnprovidedInject,
1103            IssueKind::RouteCollision,
1104            IssueKind::DynamicSegmentNameConflict,
1105            IssueKind::UnrenderedComponent,
1106            IssueKind::UnusedComponentProp,
1107            IssueKind::UnusedComponentEmit,
1108            IssueKind::UnusedServerAction,
1109            IssueKind::UnusedLoadDataKey,
1110            IssueKind::PropDrilling,
1111            IssueKind::ThinWrapper,
1112            IssueKind::DuplicatePropShape,
1113            IssueKind::UnusedComponentInput,
1114            IssueKind::UnusedComponentOutput,
1115            IssueKind::UnusedSvelteEvent,
1116        ];
1117        let discriminants: Vec<u8> = all_kinds.iter().map(|k| k.to_discriminant()).collect();
1118        let mut sorted = discriminants.clone();
1119        sorted.sort_unstable();
1120        sorted.dedup();
1121        assert_eq!(
1122            discriminants.len(),
1123            sorted.len(),
1124            "discriminant values must be unique"
1125        );
1126    }
1127
1128    #[test]
1129    fn discriminant_starts_at_one() {
1130        assert_eq!(IssueKind::UnusedFile.to_discriminant(), 1);
1131    }
1132
1133    #[test]
1134    fn suppression_line_zero_is_file_wide() {
1135        let s = Suppression::all(0, 1);
1136        assert_eq!(s.line, 0);
1137        assert!(s.issue_kind_target().is_none());
1138    }
1139
1140    #[test]
1141    fn suppression_with_specific_kind_and_line() {
1142        let s = Suppression::issue(42, 41, IssueKind::UnusedExport);
1143        assert_eq!(s.line, 42);
1144        assert_eq!(s.comment_line, 41);
1145        assert_eq!(s.issue_kind_target(), Some(IssueKind::UnusedExport));
1146    }
1147
1148    #[test]
1149    fn parses_scoped_policy_suppression_token() {
1150        let target =
1151            parse_policy_rule_suppression_token("policy-violation:team-policy/no-child-process")
1152                .expect("scoped token should parse");
1153        assert_eq!(target.pack, "team-policy");
1154        assert_eq!(target.rule_id, "no-child-process");
1155        assert_eq!(
1156            target.token(),
1157            "policy-violation:team-policy/no-child-process"
1158        );
1159    }
1160
1161    #[test]
1162    fn rejects_malformed_scoped_policy_suppression_tokens() {
1163        for token in [
1164            "policy-violation:",
1165            "policy-violation:team-policy",
1166            "policy-violation:/no-child-process",
1167            "policy-violation:team-policy/",
1168            "policy-violation:team-policy/no/child-process",
1169            "policy-violation:team policy/no-child-process",
1170            "policy-violation:team-policy/no:child-process",
1171        ] {
1172            assert!(
1173                parse_policy_rule_suppression_token(token).is_none(),
1174                "{token} should be rejected"
1175            );
1176        }
1177    }
1178
1179    #[test]
1180    fn scoped_policy_suppression_matches_exact_policy_rule_only() {
1181        let suppression = Suppression::policy_rule(7, 6, "team-policy", "no-child-process");
1182        assert!(suppression.matches_policy_rule(7, "team-policy", "no-child-process"));
1183        assert!(!suppression.matches_policy_rule(7, "team-policy", "no-fs"));
1184        assert!(!suppression.matches_policy_rule(8, "team-policy", "no-child-process"));
1185        assert!(!suppression.matches_issue_kind(7, IssueKind::PolicyViolation));
1186    }
1187
1188    #[test]
1189    fn known_issue_kind_names_parses_each_entry() {
1190        for &name in KNOWN_ISSUE_KIND_NAMES {
1191            assert!(
1192                IssueKind::parse(name).is_some(),
1193                "KNOWN_ISSUE_KIND_NAMES contains '{name}' but IssueKind::parse rejects it"
1194            );
1195        }
1196    }
1197
1198    #[test]
1199    fn closest_known_kind_name_finds_near_misses() {
1200        assert_eq!(
1201            closest_known_kind_name("unused-exports"),
1202            Some("unused-export")
1203        );
1204        assert_eq!(closest_known_kind_name("unused-files"), Some("unused-file"));
1205        assert_eq!(closest_known_kind_name("complxity"), Some("complexity"));
1206    }
1207
1208    #[test]
1209    fn closest_known_kind_name_rejects_novel_strings() {
1210        assert_eq!(closest_known_kind_name("xyzzy"), None);
1211        assert_eq!(closest_known_kind_name("foo"), None);
1212        assert_eq!(closest_known_kind_name(""), None);
1213    }
1214
1215    #[test]
1216    fn closest_known_kind_name_skips_exact_match() {
1217        assert_eq!(closest_known_kind_name("unused-export"), None);
1218    }
1219}