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

Issue:
add strictness in PyO3



Discussion / hints:

Fix patch:
diff --git a/crates/cli/src/run.rs b/crates/cli/src/run.rs
index 586745ba26..e4b4934f74 100644
--- a/crates/cli/src/run.rs
+++ b/crates/cli/src/run.rs
@@ -44,7 +44,7 @@ impl ValueEnum for Strictness {
       Strictness(M::Cst),
       Strictness(M::Smart),
       Strictness(M::Ast),
-      Strictness(M::Lenient),
+      Strictness(M::Relaxed),
       Strictness(M::Signature),
     ]
   }
@@ -54,7 +54,7 @@ impl ValueEnum for Strictness {
       M::Cst => PossibleValue::new("cst").help("Match exact all node"),
       M::Smart => PossibleValue::new("smart").help("Match all node except source trivial nodes"),
       M::Ast => PossibleValue::new("ast").help("Match only ast nodes"),
-      M::Lenient => PossibleValue::new("lenient").help("Match ast node except comments"),
+      M::Relaxed => PossibleValue::new("lenient").help("Match ast node except comments"),
       M::Signature => {
         PossibleValue::new("signature").help("Match ast node except comments, without text")
       }
diff --git a/crates/config/src/rule/mod.rs b/crates/config/src/rule/mod.rs
index b5fb46899c..04262fba16 100644
--- a/crates/config/src/rule/mod.rs
+++ b/crates/config/src/rule/mod.rs
@@ -136,7 +136,7 @@ pub enum Strictness {
   /// only ast nodes are matched
   Ast,
   /// ast-nodes excluding comments are matched
-  Lenient,
+  Relaxed,
   /// ast-nodes excluding comments, without text
   Signature,
 }
@@ -149,7 +149,7 @@ impl From<MatchStrictness> for Strictness {
       M::Cst => S::Cst,
       M::Smart => S::Smart,
       M::Ast => S::Ast,
-      M::Lenient => S::Lenient,
+      M::Relaxed => S::Relaxed,
       M::Signature => S::Signature,
     }
   }
@@ -163,7 +163,7 @@ impl From<Strictness> for MatchStrictness {
       S::Cst => M::Cst,
       S::Smart => M::Smart,
       S::Ast => M::Ast,
-      S::Lenient => M::Lenient,
+      S::Relaxed => M::Relaxed,
       S::Signature => M::Signature,
     }
   }
diff --git a/crates/core/src/match_tree/match_node.rs b/crates/core/src/match_tree/match_node.rs
index 0f45bf9de5..82ad320e25 100644
--- a/crates/core/src/match_tree/match_node.rs
+++ b/crates/core/src/match_tree/match_node.rs
@@ -263,9 +263,9 @@ mod test {
   }
 
   #[test]
-  fn test_lenient_match() {
-    matched("import $A from 'lib'", "import A from \"lib\"", M::Lenient);
-    matched("$A(bar)", "foo(/* A*/bar)", M::Lenient);
+  fn test_relaxed_match() {
+    matched("import $A from 'lib'", "import A from \"lib\"", M::Relaxed);
+    matched("$A(bar)", "foo(/* A*/bar)", M::Relaxed);
   }
 
   #[test]
diff --git a/crates/core/src/match_tree/strictness.rs b/crates/core/src/match_tree/strictness.rs
index df0c3c97d7..835e3af582 100644
--- a/crates/core/src/match_tree/strictness.rs
+++ b/crates/core/src/match_tree/strictness.rs
@@ -6,7 +6,7 @@ pub enum MatchStrictness {
   Cst,       // all nodes are matched
   Smart,     // all nodes except source trivial nodes are matched.
   Ast,       // only ast nodes are matched
-  Lenient,   // ast-nodes excluding comments are matched
+  Relaxed,   // ast-nodes excluding comments are matched
   Signature, // ast-nodes excluding comments, without text
 }
 
@@ -43,7 +43,7 @@ impl MatchStrictness {
       M::Cst => (false, false),
       M::Smart => (false, !candidate.is_named()),
       M::Ast => (!is_named, !candidate.is_named()),
-      M::Lenient => (!is_named, skip_comment_or_unnamed(candidate)),
+      M::Relaxed => (!is_named, skip_comment_or_unnamed(candidate)),
       M::Signature => {
         if k == kind {
           return MatchOneNode::MatchedBoth;
@@ -66,7 +66,7 @@ impl MatchStrictness {
       M::Cst => false,
       M::Smart => true,
       M::Ast => false,
-      M::Lenient => skip_comment_or_unnamed(candidate),
+      M::Relaxed => skip_comment_or_unnamed(candidate),
       M::Signature => skip_comment_or_unnamed(candidate),
     }
   }
@@ -79,9 +79,9 @@ impl FromStr for MatchStrictness {
       "cst" => Ok(MatchStrictness::Cst),
       "smart" => Ok(MatchStrictness::Smart),
       "ast" => Ok(MatchStrictness::Ast),
-      "lenient" => Ok(MatchStrictness::Lenient),
+      "relaxed" => Ok(MatchStrictness::Relaxed),
       "signature" => Ok(MatchStrictness::Signature),
-      _ => Err("invalid strictness, valid options are: cst, smart, ast, lenient, signature"),
+      _ => Err("invalid strictness, valid options are: cst, smart, ast, relaxed, signature"),
     }
   }
 }
diff --git a/crates/napi/index.d.ts b/crates/napi/index.d.ts
index ee470dfc92..66983a4498 100644
--- a/crates/napi/index.d.ts
+++ b/crates/napi/index.d.ts
@@ -62,10 +62,10 @@ export const enum Lang {
   Swift = 'Swift'
 }
 export interface Edit {
-  /** The position of the edit */
-  position: number
-  /** The length of the text to be deleted */
-  deletedLength: number
+  /** The start position of the edit */
+  startPos: number
+  /** The end position of the edit */
+  endPos: number
   /** The text to be inserted */
   insertedText: string
 }
diff --git a/crates/napi/src/sg_node.rs b/crates/napi/src/sg_node.rs
index 5e59764b68..6aac35151b 100644
--- a/crates/napi/src/sg_node.rs
+++ b/crates/napi/src/sg_node.rs
@@ -8,10 +8,10 @@ use ast_grep_core::source::Content;
 
 #[napi(object)]
 pub struct Edit {
-  /// The position of the edit
-  pub position: u32,
-  /// The length of the text to be deleted
-  pub deleted_length: u32,
+  /// The start position of the edit
+  pub start_pos: u32,
+  /// The end position of the edit
+  pub end_pos: u32,
   /// The text to be inserted
   pub inserted_text: String,
 }
@@ -313,22 +313,22 @@ impl SgNode {
     let byte_range = self.inner.range();
     // the text is u16, need to convert to JS str length
     Edit {
-      position: (byte_range.start / 2) as u32,
-      deleted_length: (byte_range.len() / 2) as u32,
+      start_pos: (byte_range.start / 2) as u32,
+      end_pos: (byte_range.end / 2) as u32,
       inserted_text: text,
     }
   }
 
   #[napi]
   pub fn commit_edits(&self, mut edits: Vec<Edit>) -> String {
-    edits.sort_by_key(|edit| edit.position);
+    edits.sort_by_key(|edit| edit.start_pos);
     let mut new_content = Vec::new();
     let text = self.text();
     let old_content = Wrapper::decode_str(&text);
     let offset = self.inner.range().start / 2;
     let mut start = 0;
     for diff in edits {
-      let pos = diff.position as usize - offset;
+      let pos = diff.start_pos as usize - offset;
       // skip overlapping edits
       if start > pos {
         continue;
@@ -336,7 +336,7 @@ impl SgNode {
       new_content.extend(&old_content[start..pos]);
       let bytes = Wrapper::decode_str(&diff.inserted_text);
       new_content.extend(&*bytes);
-      start = pos + diff.deleted_length as usize;
+      start = diff.end_pos as usize - offset;
     }
     // add trailing statements
     new_content.extend(&old_content[start..]);
diff --git a/crates/pyo3/ast_grep_py/__init__.py b/crates/pyo3/ast_grep_py/__init__.py
index b7d4e638a0..95278b91e3 100644
--- a/crates/pyo3/ast_grep_py/__init__.py
+++ b/crates/pyo3/ast_grep_py/__init__.py
@@ -3,8 +3,11 @@
 from typing import List, TypedDict,  Literal, Dict, Union, Mapping
 from .ast_grep_py import SgNode, SgRoot, Pos, Range, Edit
 
+Strictness = Union[Literal["cst"], Literal["smart"], Literal["ast"], Literal["relaxed"], Literal["signature"]]
+
 class Pattern(TypedDict):
-    selector: str
+    selector: Optional[str]
+    strictness: Optional[Strictness]
     context: str
 
 class NthChild(TypedDict):
diff --git a/crates/pyo3/ast_grep_py/ast_grep_py.pyi b/crates/pyo3/ast_grep_py/ast_grep_py.pyi
index c7c1d09171..db3e73ba30 100644
--- a/crates/pyo3/ast_grep_py/ast_grep_py.pyi
+++ b/crates/pyo3/ast_grep_py/ast_grep_py.pyi
@@ -12,8 +12,8 @@ class Range:
     end: Pos
 
 class Edit:
-    position: int
-    deleted_length: int
+    start_pos: int
+    end_pos: int
     inserted_text: str
 
 class SgRoot:
diff --git a/crates/pyo3/src/py_node.rs b/crates/pyo3/src/py_node.rs
index b4e31089a0..838db41a5f 100644
--- a/crates/pyo3/src/py_node.rs
+++ b/crates/pyo3/src/py_node.rs
@@ -246,18 +246,18 @@ impl SgNode {
     Python::with_gil(|py| {
       let root = self.root.bind(py);
       let root = root.borrow();
-      let position = root.position.byte_to_char(byte_range.start);
-      let end = root.position.byte_to_char(byte_range.end);
+      let start_pos = root.position.byte_to_char(byte_range.start);
+      let end_pos = root.position.byte_to_char(byte_range.end);
       Edit {
-        position,
-        deleted_length: end - position,
+        start_pos,
+        end_pos,
         inserted_text: text.to_string(),
       }
     })
   }
 
   fn commit_edits(&self, mut edits: Vec<Edit>) -> String {
-    edits.sort_by_key(|edit| edit.position);
+    edits.sort_by_key(|edit| edit.start_pos);
     let mut new_content = String::new();
     let old_content = self.text();
     let converted: Vec<_> = Python::with_gil(move |py| {
@@ -267,10 +267,8 @@ impl SgNode {
       edits
         .into_iter()
         .map(|mut e| {
-          let char_offset = e.position + e.deleted_length;
-          let byte_offset = conv.char_to_byte(char_offset);
-          e.position = conv.char_to_byte(e.position);
-          e.deleted_length = byte_offset - e.position;
+          e.start_pos = conv.char_to_byte(e.start_pos);
+          e.end_pos = conv.char_to_byte(e.end_pos);
           e
         })
         .collect()
@@ -278,14 +276,14 @@ impl SgNode {
     let offset = self.inner.range().start;
     let mut start = 0;
     for diff in converted {
-      let pos = diff.position - offset;
+      let pos = diff.start_pos - offset;
       // skip overlapping edits
       if start > pos {
         continue;
       }
       new_content.push_str(&old_content[start..pos]);
       new_content.push_str(&diff.inserted_text);
-      start = pos + diff.deleted_length;
+      start = diff.end_pos - offset;
     }
     // add trailing statements
     new_content.push_str(&old_content[start..]);
@@ -374,10 +372,10 @@ fn get_matcher_from_rule(
 #[pyclass(get_all, set_all)]
 #[derive(Clone, PartialEq, Eq, Hash)]
 pub struct Edit {
-  /// The char position of the edit
-  pub position: usize,
-  /// The char length of the text to be deleted
-  pub deleted_length: usize,
+  /// The start position of the edit in character
+  pub start_pos: usize,
+  /// The end position of the edit in character
+  pub end_pos: usize,
   /// The text to be inserted
   pub inserted_text: String,
 }


Test patch:
diff --git a/crates/napi/__test__/index.spec.ts b/crates/napi/__test__/index.spec.ts
index 63d4c2889c..1e3fc67fe9 100644
--- a/crates/napi/__test__/index.spec.ts
+++ b/crates/napi/__test__/index.spec.ts
@@ -94,8 +94,8 @@ test('test code fix', t => {
   const match = sg.root().find('console.log')!
   const fix = match.replace('console.error')
   t.deepEqual(fix.insertedText, 'console.error')
-  t.deepEqual(fix.position, 4)
-  t.deepEqual(fix.deletedLength, 11) // length of console.log
+  t.deepEqual(fix.startPos, 4)
+  t.deepEqual(fix.endPos, 15) // length of console.log
   t.deepEqual(match.commitEdits([fix]), 'console.error')
   const newCode = sg.root().commitEdits([fix])
   t.deepEqual(newCode, 'a = console.error(123)')
@@ -107,7 +107,7 @@ test('test code with multiple fixes', t => {
   const matches = sg.root().findAll(js.kind('number'))
   const fixes = matches.map(match => match.replace('114514'))
   // make it in reverse order to test rust behavior
-  fixes.sort((a, b) => b.position - a.position)
+  fixes.sort((a, b) => b.startPos - a.startPos)
   const newCode = sg.root().commitEdits(fixes)
   t.deepEqual(newCode, 'いいよ = log(114514) + log(114514)')
 })
@@ -116,8 +116,8 @@ test('test code fix with user defined range', t => {
   const sg = parse('いいよ = log(123)')
   const match = sg.root().find(js.kind('number'))!
   const edit = match.replace('514')
-  edit.position -= 1
-  edit.deletedLength += 2
+  edit.startPos -= 1
+  edit.endPos += 1
   const newCode = sg.root().commitEdits([edit])
   t.deepEqual(newCode, 'いいよ = log514')
 })
diff --git a/crates/pyo3/tests/test_fix.py b/crates/pyo3/tests/test_fix.py
index e4ff649e4b..643230b393 100644
--- a/crates/pyo3/tests/test_fix.py
+++ b/crates/pyo3/tests/test_fix.py
@@ -16,8 +16,8 @@
 def test_one_fix():
     edit = node1.replace("let b = 456")
     r = node1.range()
-    assert edit.position == r.start.index
-    assert edit.deleted_length == r.end.index - edit.position
+    assert edit.start_pos == r.start.index
+    assert edit.end_pos == r.end.index
     s = node1.commit_edits([edit])
     assert s == "let b = 456"
     s = root.commit_edits([edit])
@@ -28,7 +28,7 @@ def test_multiple_fix():
     root = sg.root()
     nodes = root.find_all(kind="number")
     edits = [node.replace('114514') for node in nodes]
-    edits = sorted(edits, key=lambda x: x.position, reverse=True)
+    edits = sorted(edits, key=lambda x: x.start_pos, reverse=True)
     s = root.commit_edits(edits)
     assert s == "いいよ = log(114514) + log(114514)"
 
@@ -46,7 +46,7 @@ def test_modified_fix():
     node = root.find(kind="number")
     assert node
     edit = node.replace('こいよ')
-    edit.position -= 1
-    edit.deleted_length += 2
+    edit.start_pos -= 1
+    edit.end_pos += 1
     s = root.commit_edits([edit])
     assert s == "いいよ = logこいよ"
\ No newline at end of file
diff --git a/crates/pyo3/tests/test_rule.py b/crates/pyo3/tests/test_rule.py
index 95c2d28a97..ea9df385d6 100644
--- a/crates/pyo3/tests/test_rule.py
+++ b/crates/pyo3/tests/test_rule.py
@@ -149,4 +149,10 @@ def test_pattern():
         context="let a = 123",
         selector="variable_declarator",
     ))
-    assert node == node2
\ No newline at end of file
+    assert node == node2
+
+def test_strictness():
+    node = root.find(pattern={
+        "context": "let b = 456",
+        "strictness": "signature",
+    })
\ No newline at end of file