Repository: ast-grep/ast-grep (language: rust)

Issue:
support report injected language issue in `scan`



Discussion / hints:

Fix patch:
diff --git a/crates/cli/src/run.rs b/crates/cli/src/run.rs
index a9e832322e..2fac472ebf 100644
--- a/crates/cli/src/run.rs
+++ b/crates/cli/src/run.rs
@@ -383,6 +383,16 @@ mod test {
     assert!(run_with_pattern(arg).is_ok())
   }
 
+  #[test]
+  fn test_run_with_strictness() {
+    let arg = RunArg {
+      pattern: "console.log".to_string(),
+      strictness: Some(Strictness(MatchStrictness::Ast)),
+      ..default_run_arg()
+    };
+    assert!(run_with_pattern(arg).is_ok())
+  }
+
   #[test]
   fn test_run_with_specific_lang() {
     let arg = RunArg {
diff --git a/crates/cli/src/scan.rs b/crates/cli/src/scan.rs
index 7932e78d82..5b3853574c 100644
--- a/crates/cli/src/scan.rs
+++ b/crates/cli/src/scan.rs
@@ -128,7 +128,7 @@ impl<P: Printer> Worker for ScanWithConfig<P> {
     for (path, grep, hit_set) in items {
       let file_content = grep.source().to_string();
       let path = &path;
-      let rules = self.configs.for_path(path);
+      let rules = self.configs.get_rule_from_lang(path, *grep.lang());
       let combined = CombinedScan::new(rules);
       let interactive = self.arg.output.needs_interactive();
       // exclude_fix rule because we already have diff inspection before
@@ -166,18 +166,7 @@ impl<P: Printer> PathWorker for ScanWithConfig<P> {
     self.arg.input.walk()
   }
   fn produce_item(&self, path: &Path) -> Option<Vec<Self::Item>> {
-    let rules = self.configs.for_path(path);
-    if rules.is_empty() {
-      return None;
-    }
-    let lang = rules[0].language;
-    let combined = CombinedScan::new(rules);
-    let unit = filter_file_interactive(path, lang, ast_grep_core::matcher::MatchAll)?;
-    let hit_set = combined.find(&unit.grep);
-    if !hit_set.is_empty() {
-      return Some(vec![(unit.path, unit.grep, hit_set)]);
-    }
-    None
+    filter_file_interactive(path, &self.configs)
   }
 }
 
diff --git a/crates/cli/src/utils.rs b/crates/cli/src/utils.rs
index 69da0ab324..20066f327c 100644
--- a/crates/cli/src/utils.rs
+++ b/crates/cli/src/utils.rs
@@ -2,6 +2,7 @@ use crate::lang::SgLang;
 use crate::print::{ColorArg, JsonStyle};
 
 use anyhow::{anyhow, Context, Result};
+use bit_set::BitSet;
 use clap::{Args, ValueEnum};
 use crossterm::{
   event::{self, Event, KeyCode},
@@ -11,8 +12,9 @@ use crossterm::{
 use ignore::{DirEntry, WalkBuilder, WalkParallel, WalkState};
 use serde::{Deserialize, Serialize};
 
+use ast_grep_config::{CombinedScan, RuleCollection};
 use ast_grep_core::Pattern;
-use ast_grep_core::{AstGrep, Matcher, StrDoc};
+use ast_grep_core::{Matcher, StrDoc};
 use ast_grep_language::Language;
 
 use std::fs::read_to_string;
@@ -22,6 +24,8 @@ use std::path::{Path, PathBuf};
 use std::str::FromStr;
 use std::sync::{mpsc, Arc};
 
+type AstGrep = ast_grep_core::AstGrep<StrDoc<SgLang>>;
+
 fn read_char() -> Result<char> {
   loop {
     if let Event::Key(evt) = event::read()? {
@@ -202,19 +206,44 @@ fn read_file(path: &Path) -> Option<String> {
   }
 }
 
-pub fn filter_file_interactive<M: Matcher<SgLang>>(
+fn filter(
+  grep: &AstGrep,
   path: &Path,
   lang: SgLang,
-  matcher: M,
-) -> Option<MatchUnit<M>> {
+  configs: &RuleCollection<SgLang>,
+) -> Option<BitSet> {
+  let rules = configs.get_rule_from_lang(path, lang);
+  let combined = CombinedScan::new(rules);
+  let hit_set = combined.find(grep);
+  if hit_set.is_empty() {
+    None
+  } else {
+    Some(hit_set)
+  }
+}
+
+pub fn filter_file_interactive(
+  path: &Path,
+  configs: &RuleCollection<SgLang>,
+) -> Option<Vec<(PathBuf, AstGrep, BitSet)>> {
+  let lang = SgLang::from_path(path)?;
   let file_content = read_file(path)?;
   let grep = lang.ast_grep(file_content);
-  let has_match = grep.root().find(&matcher).is_some();
-  has_match.then(|| MatchUnit {
-    grep,
-    path: path.to_path_buf(),
-    matcher,
-  })
+  let mut ret = vec![];
+  let root =
+    filter(&grep, path, lang, configs).map(|hit_set| (path.to_path_buf(), grep.clone(), hit_set));
+  ret.extend(root);
+  if let Some(injected) = lang.injectable_sg_langs() {
+    let docs = grep.inner.get_injections(|s| SgLang::from_str(s).ok());
+    let inj = injected.filter_map(|l| {
+      let doc = docs.iter().find(|d| *d.lang() == l)?;
+      let grep = AstGrep { inner: doc.clone() };
+      let hit_set = filter(&grep, path, l, configs)?;
+      Some((path.to_path_buf(), grep, hit_set))
+    });
+    ret.extend(inj)
+  }
+  Some(ret)
 }
 
 pub fn filter_file_pattern(
@@ -225,7 +254,7 @@ pub fn filter_file_pattern(
 ) -> Option<Vec<(MatchUnit<Pattern<SgLang>>, SgLang)>> {
   let file_content = read_file(path)?;
   let grep = lang.ast_grep(&file_content);
-  let do_match = |ast_grep: AstGrep<_>, matcher: Pattern<SgLang>, lang: SgLang| {
+  let do_match = |ast_grep: AstGrep, matcher: Pattern<SgLang>, lang: SgLang| {
     let fixed = matcher.fixed_string();
     if !fixed.is_empty() && !file_content.contains(&*fixed) {
       return None;
@@ -271,7 +300,7 @@ fn file_too_large(file_content: &str) -> bool {
 /// An analogy to compilation unit in C programming language.
 pub struct MatchUnit<M: Matcher<SgLang>> {
   pub path: PathBuf,
-  pub grep: AstGrep<StrDoc<SgLang>>,
+  pub grep: AstGrep,
   pub matcher: M,
 }
 
diff --git a/crates/config/src/rule_collection.rs b/crates/config/src/rule_collection.rs
index 04661c9d33..a56dec4769 100644
--- a/crates/config/src/rule_collection.rs
+++ b/crates/config/src/rule_collection.rs
@@ -96,11 +96,8 @@ impl<L: Language + Eq> RuleCollection<L> {
     })
   }
 
-  pub fn for_path<P: AsRef<Path>>(&self, path: P) -> Vec<&RuleConfig<L>> {
+  pub fn get_rule_from_lang(&self, path: &Path, lang: L) -> Vec<&RuleConfig<L>> {
     let mut all_rules = vec![];
-    let Some(lang) = L::from_path(path.as_ref()) else {
-      return vec![];
-    };
     for rule in &self.tenured {
       if rule.lang == lang {
         all_rules = rule.rules.iter().collect();
@@ -108,7 +105,7 @@ impl<L: Language + Eq> RuleCollection<L> {
       }
     }
     all_rules.extend(self.contingent.iter().filter_map(|cont| {
-      if cont.rule.language == lang && cont.matches_path(path.as_ref()) {
+      if cont.rule.language == lang && cont.matches_path(path) {
         Some(&cont.rule)
       } else {
         None
@@ -117,6 +114,14 @@ impl<L: Language + Eq> RuleCollection<L> {
     all_rules
   }
 
+  pub fn for_path<P: AsRef<Path>>(&self, path: P) -> Vec<&RuleConfig<L>> {
+    let path = path.as_ref();
+    let Some(lang) = L::from_path(path) else {
+      return vec![];
+    };
+    self.get_rule_from_lang(path, lang)
+  }
+
   pub fn get_rule(&self, id: &str) -> Option<&RuleConfig<L>> {
     for rule in &self.tenured {
       for r in &rule.rules {


Test patch:
diff --git a/crates/cli/tests/scan_test.rs b/crates/cli/tests/scan_test.rs
index 33c14461f9..b1b725e46c 100644
--- a/crates/cli/tests/scan_test.rs
+++ b/crates/cli/tests/scan_test.rs
@@ -119,3 +119,20 @@ fn test_sg_scan_py_empty_text() -> Result<()> {
     .stdout(contains("STDIN:6:1"));
   Ok(())
 }
+
+#[test]
+fn test_sg_scan_html() -> Result<()> {
+  let dir = create_test_files([
+    ("rule.yml", RULE1),
+    ("test.html", "<script lang=ts>Some(123)</script>"),
+  ])?;
+  Command::cargo_bin("sg")?
+    .current_dir(dir.path())
+    .args(["scan", "-r", "rule.yml"])
+    .assert()
+    .success()
+    .stdout(contains("on-rule"))
+    .stdout(contains("script"))
+    .stdout(contains("rule-3").not());
+  Ok(())
+}