svelte-compiler 0.1.4

Core compiler API for the Rust Svelte toolchain
Documentation
use crate::names::OrderedNames;
use std::sync::Arc;

pub(crate) fn parse_svelte_ignores(comment_data: &str) -> Box<[Arc<str>]> {
    let trimmed = comment_data.trim_start();
    let Some(rest) = trimmed.strip_prefix("svelte-ignore") else {
        return Box::default();
    };

    let mut ignores = OrderedNames::default();
    let mut token = String::new();
    for ch in rest.chars() {
        if ch.is_ascii_alphanumeric() || ch == '_' || ch == '-' || ch == '$' {
            token.push(ch);
            continue;
        }
        if !token.is_empty() {
            push_ignore_code_variants(&token, &mut ignores);
            token.clear();
        }
    }
    if !token.is_empty() {
        push_ignore_code_variants(&token, &mut ignores);
    }

    ignores.into_boxed_slice()
}

pub(crate) fn migrate_svelte_ignore(text: &str) -> Option<String> {
    let prefix_len = svelte_ignore_directive_prefix_len(text)?;
    let payload = &text[prefix_len..];

    let mut output = String::with_capacity(text.len());
    output.push_str(&text[..prefix_len]);

    let mut cursor = 0usize;
    let mut changed = false;

    while let Some((start, end)) = find_next_hyphenated_ignore_code(payload, cursor) {
        output.push_str(&payload[cursor..start]);

        let code = &payload[start..end];
        let mut replacement = legacy_ignore_replacement(code)
            .map(str::to_string)
            .unwrap_or_else(|| code.replace('-', "_"));

        if find_next_hyphenated_ignore_code(payload, end).is_some() {
            replacement.push(',');
        }

        changed |= replacement != code;
        output.push_str(&replacement);
        cursor = end;
    }

    if !changed {
        return None;
    }

    output.push_str(&payload[cursor..]);
    Some(output)
}

fn push_ignore_code_variants(code: &str, ignores: &mut OrderedNames) {
    push_unique_ignore(code, ignores);

    if let Some(replacement) = legacy_ignore_replacement(code) {
        push_unique_ignore(replacement, ignores);
        return;
    }

    if code.contains('-') {
        let normalized = code.replace('-', "_");
        push_unique_ignore(&normalized, ignores);
    }
}

fn legacy_ignore_replacement(code: &str) -> Option<&'static str> {
    match code {
        "non-top-level-reactive-declaration" => Some("reactive_declaration_invalid_placement"),
        "module-script-reactive-declaration" => Some("reactive_declaration_module_script"),
        "empty-block" => Some("block_empty"),
        "avoid-is" => Some("attribute_avoid_is"),
        "invalid-html-attribute" => Some("attribute_invalid_property_name"),
        "a11y-structure" => Some("a11y_figcaption_parent"),
        "illegal-attribute-character" => Some("attribute_illegal_colon"),
        "invalid-rest-eachblock-binding" => Some("bind_invalid_each_rest"),
        "unused-export-let" => Some("export_let_unused"),
        _ => None,
    }
}

fn push_unique_ignore(code: &str, ignores: &mut OrderedNames) {
    ignores.extend([Arc::from(code)]);
}

fn svelte_ignore_directive_prefix_len(text: &str) -> Option<usize> {
    let trimmed = text.trim_start_matches(char::is_whitespace);
    let rest = trimmed.strip_prefix("svelte-ignore")?;
    if !rest.chars().next().is_some_and(char::is_whitespace) {
        return None;
    }
    Some(text.len() - rest.len() + 1)
}

fn find_next_hyphenated_ignore_code(text: &str, mut cursor: usize) -> Option<(usize, usize)> {
    let bytes = text.as_bytes();

    while cursor < bytes.len() {
        if !is_ignore_word_char(bytes[cursor]) {
            cursor += 1;
            continue;
        }

        let start = cursor;
        cursor += 1;
        while cursor < bytes.len() && (is_ignore_word_char(bytes[cursor]) || bytes[cursor] == b'-')
        {
            cursor += 1;
        }

        if is_hyphenated_ignore_code(&text[start..cursor]) {
            return Some((start, cursor));
        }
    }

    None
}

fn is_hyphenated_ignore_code(token: &str) -> bool {
    let mut saw_hyphen = false;
    let mut segment_len = 0usize;

    for byte in token.bytes() {
        if byte == b'-' {
            if segment_len == 0 {
                return false;
            }
            saw_hyphen = true;
            segment_len = 0;
            continue;
        }

        if !is_ignore_word_char(byte) {
            return false;
        }
        segment_len += 1;
    }

    saw_hyphen && segment_len > 0
}

fn is_ignore_word_char(byte: u8) -> bool {
    byte.is_ascii_alphanumeric() || byte == b'_'
}

pub(crate) fn find_matching_paren(source: &str, open_index: usize) -> Option<usize> {
    let bytes = source.as_bytes();
    if bytes.get(open_index).copied() != Some(b'(') {
        return None;
    }
    let mut depth = 0usize;
    let mut cursor = open_index + 1;
    let mut in_single = false;
    let mut in_double = false;
    let mut escaped = false;

    while cursor < bytes.len() {
        let ch = bytes[cursor] as char;
        if escaped {
            escaped = false;
            cursor += 1;
            continue;
        }
        if ch == '\\' {
            escaped = true;
            cursor += 1;
            continue;
        }
        if ch == '\'' && !in_double {
            in_single = !in_single;
            cursor += 1;
            continue;
        }
        if ch == '"' && !in_single {
            in_double = !in_double;
            cursor += 1;
            continue;
        }
        if in_single || in_double {
            cursor += 1;
            continue;
        }
        match ch {
            '(' => depth += 1,
            ')' => {
                if depth == 0 {
                    return Some(cursor);
                }
                depth = depth.saturating_sub(1);
            }
            _ => {}
        }
        cursor += 1;
    }

    None
}

pub(crate) fn next_char_boundary(source: &str, index: usize) -> usize {
    if index >= source.len() {
        return source.len();
    }
    let mut next = index + 1;
    while next < source.len() && !source.is_char_boundary(next) {
        next += 1;
    }
    next
}

#[cfg(test)]
mod tests {
    use super::{migrate_svelte_ignore, parse_svelte_ignores};

    #[test]
    fn parse_svelte_ignores_keeps_legacy_and_normalized_codes() {
        let ignores = parse_svelte_ignores(
            " svelte-ignore non-top-level-reactive-declaration a11y-something-something ",
        );

        let codes = ignores.iter().map(|code| code.as_ref()).collect::<Vec<_>>();
        assert_eq!(
            codes,
            vec![
                "non-top-level-reactive-declaration",
                "reactive_declaration_invalid_placement",
                "a11y-something-something",
                "a11y_something_something",
            ]
        );
    }

    #[test]
    fn migrate_svelte_ignore_rewrites_legacy_codes() {
        assert_eq!(
            migrate_svelte_ignore(
                " svelte-ignore non-top-level-reactive-declaration a11y-something-something a11y-something-something2 ",
            ),
            Some(
                " svelte-ignore reactive_declaration_invalid_placement, a11y_something_something, a11y_something_something2 "
                    .to_string(),
            )
        );
    }

    #[test]
    fn migrate_svelte_ignore_ignores_non_directives() {
        assert_eq!(migrate_svelte_ignore(" not-a-directive a-b "), None);
        assert_eq!(migrate_svelte_ignore(" svelte-ignore already_valid "), None);
    }
}