Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ anyhow = "1.0.98"
# Safe to migrate back to the official ast-grep release once PR #2359 is merged and a new version is published.
ast-grep-config = { git = "https://github.com/fengmk2/ast-grep.git", rev = "2f4c6924438a72e136485f0e14cd136b2e17d8d3" }
ast-grep-core = { git = "https://github.com/fengmk2/ast-grep.git", rev = "2f4c6924438a72e136485f0e14cd136b2e17d8d3" }
ast-grep-language = { git = "https://github.com/fengmk2/ast-grep.git", rev = "2f4c6924438a72e136485f0e14cd136b2e17d8d3", default-features = false, features = ["lang-bash"] }
ast-grep-language = { git = "https://github.com/fengmk2/ast-grep.git", rev = "2f4c6924438a72e136485f0e14cd136b2e17d8d3", default-features = false, features = ["lang-bash", "lang-typescript"] }
backon = "1.3.0"
bincode = "2.0.1"
bstr = { version = "1.12.0", default-features = false, features = ["alloc", "std"] }
Expand Down
3 changes: 3 additions & 0 deletions crates/vite_migration/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,8 @@ ast-grep-language = { workspace = true }
serde_json = { workspace = true, features = ["preserve_order"] }
vite_error = { workspace = true }

[dev-dependencies]
tempfile = { workspace = true }

[lints]
workspace = true
82 changes: 82 additions & 0 deletions crates/vite_migration/src/ast_grep.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
use ast_grep_config::{GlobalRules, RuleConfig, from_yaml_string};
use ast_grep_core::replacer::Replacer;
use ast_grep_language::{LanguageExt, SupportLang};
use vite_error::Error;

/// Apply ast-grep rules to content and return the transformed content
///
/// This is the core transformation function that:
/// 1. Parses the rule YAML
/// 2. Applies each rule to find matches
/// 3. Replaces matches from back to front to maintain correct positions
///
/// # Arguments
///
/// * `content` - The source content to transform
/// * `rule_yaml` - The ast-grep rules in YAML format
///
/// # Returns
///
/// A tuple of (transformed_content, was_updated)
pub(crate) fn apply_rules(content: &str, rule_yaml: &str) -> Result<(String, bool), Error> {
let rules = load_rules(rule_yaml)?;
let result = apply_loaded_rules(content, &rules);
let updated = result != content;
Ok((result, updated))
}

/// Load ast-grep rules from YAML string
pub(crate) fn load_rules(yaml: &str) -> Result<Vec<RuleConfig<SupportLang>>, Error> {
let globals = GlobalRules::default();
let rules: Vec<RuleConfig<SupportLang>> = from_yaml_string::<SupportLang>(yaml, &globals)?;
Ok(rules)
}

/// Apply pre-loaded ast-grep rules to content
///
/// This is useful when you need to apply the same rules multiple times
/// (e.g., processing multiple scripts in a loop).
///
/// # Arguments
///
/// * `content` - The source content to transform
/// * `rules` - Pre-loaded ast-grep rules
///
/// # Returns
///
/// The transformed content (always returns a new string, even if unchanged)
pub(crate) fn apply_loaded_rules(content: &str, rules: &[RuleConfig<SupportLang>]) -> String {
let mut current = content.to_string();

for rule in rules {
// Parse current content with the rule's language
let grep = rule.language.ast_grep(&current);
let root = grep.root();

let matcher = &rule.matcher;

// Get the fixer if available (rules without fix are pure lint, skip them)
let fixers = match rule.get_fixer() {
Ok(f) if !f.is_empty() => f,
_ => continue,
};

// Collect all matches and their replacements
let mut replacements = Vec::new();
for node in root.find_all(matcher) {
let range = node.range();
let replacement_bytes = fixers[0].generate_replacement(&node);
let replacement_str = String::from_utf8_lossy(&replacement_bytes).to_string();
replacements.push((range.start, range.end, replacement_str));
}

// Replace from back to front to maintain correct positions
replacements.sort_by_key(|(start, _, _)| std::cmp::Reverse(*start));

for (start, end, replacement) in replacements {
current.replace_range(start..end, &replacement);
}
}

current
}
3 changes: 3 additions & 0 deletions crates/vite_migration/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
mod ast_grep;
mod package;
mod vite_config;

pub use package::rewrite_scripts;
pub use vite_config::{MergeResult, RewriteResult, merge_json_config, rewrite_import};
58 changes: 8 additions & 50 deletions crates/vite_migration/src/package.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
use ast_grep_config::{GlobalRules, RuleConfig, from_yaml_string};
use ast_grep_core::replacer::Replacer;
use ast_grep_language::{LanguageExt, SupportLang};
use ast_grep_config::RuleConfig;
use ast_grep_language::SupportLang;
use serde_json::{Map, Value};
use vite_error::Error;

/// load script rules from yaml file
fn load_ast_grep_rules(yaml: &str) -> Result<Vec<RuleConfig<SupportLang>>, Error> {
let globals = GlobalRules::default();
let rules: Vec<RuleConfig<SupportLang>> = from_yaml_string::<SupportLang>(&yaml, &globals)?;
Ok(rules)
}
use crate::ast_grep;

// Marker to replace "cross-env " before ast-grep processing
// Using a fake env var assignment that won't match our rules
Expand All @@ -22,57 +16,23 @@ fn rewrite_script(script: &str, rules: &[RuleConfig<SupportLang>]) -> String {
let has_cross_env = script.contains(CROSS_ENV_REPLACEMENT);

// Step 1: Replace "cross-env " with marker so ast-grep can see the actual commands
let mut current = if has_cross_env {
let preprocessed = if has_cross_env {
script.replace(CROSS_ENV_REPLACEMENT, CROSS_ENV_MARKER)
} else {
script.to_string()
};

// Step 2: Process with ast-grep
for rule in rules {
// only handle bash rules
if rule.language != SupportLang::Bash {
continue;
}

// parse current script with corresponding language
let grep = rule.language.ast_grep(&current);
let root = grep.root();

// this matcher is the AST matcher generated by deserializing the YAML rule
let matcher = &rule.matcher;

// rules may not have fix (pure lint), skip here
let fixers = match rule.get_fixer() {
Ok(f) if !f.is_empty() => f,
_ => continue,
};

// collect all matches and their replacements
let mut replacements = Vec::new();
for node in root.find_all(matcher) {
let range = node.range();
let replacement_bytes = fixers[0].generate_replacement(&node);
let replacement_str = String::from_utf8_lossy(&replacement_bytes).to_string();
replacements.push((range.start, range.end, replacement_str));
}

// Replace from back to front
replacements.sort_by_key(|(start, _, _)| std::cmp::Reverse(*start));

for (start, end, replacement) in replacements {
current.replace_range(start..end, &replacement);
}
}
let result = ast_grep::apply_loaded_rules(&preprocessed, rules);

// Step 3: Replace marker back with "cross-env " (only if we replaced it)
if has_cross_env { current.replace(CROSS_ENV_MARKER, CROSS_ENV_REPLACEMENT) } else { current }
if has_cross_env { result.replace(CROSS_ENV_MARKER, CROSS_ENV_REPLACEMENT) } else { result }
}

/// rewrite scripts json content using rules from rules_yaml
pub fn rewrite_scripts(scripts_json: &str, rules_yaml: &str) -> Result<Option<String>, Error> {
let mut scripts: Map<String, Value> = serde_json::from_str(scripts_json)?;
let rules = load_ast_grep_rules(rules_yaml)?;
let rules = ast_grep::load_rules(rules_yaml)?;

let mut updated = false;
// get scripts field (object)
Expand Down Expand Up @@ -156,9 +116,7 @@ fix: vite test

#[test]
fn test_rewrite_script() {
let globals = GlobalRules::default();
let rules: Vec<RuleConfig<SupportLang>> =
from_yaml_string::<SupportLang>(&RULES_YAML, &globals).unwrap();
let rules = ast_grep::load_rules(RULES_YAML).unwrap();
// vite commands
assert_eq!(rewrite_script("vite", &rules), "vite dev");
assert_eq!(rewrite_script("vite dev", &rules), "vite dev");
Expand Down
Loading
Loading