Repository: suo/lintrunner (language: python)

Issue:
Merge branch 'main' into pr60
Merge branch 'main' into pr60

---
Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/suo/lintrunner/pull/60).
* __->__ #60
* #63
* #62
* #61


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml
index bb1e172..2508d2c 100644
--- a/.github/workflows/CI.yml
+++ b/.github/workflows/CI.yml
@@ -38,11 +38,12 @@ jobs:
         uses: nick-fields/retry@v2.8.2
         env:
           TMPDIR: ${{ runner.temp }}
-        timeout_minutes: 10
-        max_attempts: 10
-        retry_wait_seconds: 90
-        command: |
-          cargo test
+        with:
+          timeout_minutes: 10
+          max_attempts: 10
+          retry_wait_seconds: 90
+          command: |
+            cargo test
 
 
   linux:
@@ -115,4 +116,4 @@ jobs:
           MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
         with:
           command: upload
-          args: --skip-existing *
\ No newline at end of file
+          args: --skip-existing *
diff --git a/README.md b/README.md
index 035c76f..f88da7e 100644
--- a/README.md
+++ b/README.md
@@ -85,6 +85,9 @@ lintrunner -m master
 ### `--all-files`
 This will run lint on all files specified in `.lintrunner.toml`.
 
+### `--only-lint-under-config-dir`
+If set, will only lint files under the directory where the configuration file is located and its subdirectories.
+
 ## Linter configuration
 `lintrunner` knows which linters to run and how by looking at a configuration
 file, conventionally named `.lintrunner.toml`.
diff --git a/src/lib.rs b/src/lib.rs
index 5c9904b..f5a6b0e 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -159,6 +159,7 @@ pub fn do_lint(
     enable_spinners: bool,
     revision_opt: RevisionOpt,
     tee_json: Option<String>,
+    only_lint_under_config_dir: bool,
 ) -> Result<i32> {
     debug!(
         "Running linters: {:?}",
@@ -186,6 +187,14 @@ pub fn do_lint(
     files.sort();
     files.dedup();
 
+    if only_lint_under_config_dir {
+        let config_dir = linters[0].get_config_dir();
+        files = files
+            .into_iter()
+            .filter(|path| path.starts_with(config_dir))
+            .collect();
+    }
+
     let files = Arc::new(files);
 
     log_utils::log_files("Linting files: ", &files);
diff --git a/src/lint_config.rs b/src/lint_config.rs
index 7e38d5e..4e4df5e 100644
--- a/src/lint_config.rs
+++ b/src/lint_config.rs
@@ -19,6 +19,11 @@ pub struct LintRunnerConfig {
     /// Recommend setting this is set to your default branch, e.g. `main`
     #[serde()]
     pub merge_base_with: Option<String>,
+
+    /// If set, will only lint files under the directory where the configuration file is located and its subdirectories.
+    /// Supercedes command line argument.
+    #[serde()]
+    pub only_lint_under_config_dir: Option<bool>,
 }
 
 fn is_false(b: &bool) -> bool {
diff --git a/src/linter.rs b/src/linter.rs
index fc4c16f..34ee643 100644
--- a/src/linter.rs
+++ b/src/linter.rs
@@ -37,7 +37,7 @@ fn matches_relative_path(base: &Path, from: &Path, pattern: &Pattern) -> bool {
 }
 
 impl Linter {
-    fn get_config_dir(&self) -> &Path {
+    pub fn get_config_dir(&self) -> &Path {
         // Unwrap is fine here because we know this path is absolute and won't be `/`
         self.primary_config_path.parent().unwrap()
     }
diff --git a/src/main.rs b/src/main.rs
index ec1aba6..d039c29 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -108,6 +108,10 @@ struct Args {
     /// Run lintrunner on all files in the repo. This could take a while!
     #[clap(long, conflicts_with_all=&["paths", "paths-cmd", "paths-from", "revision", "merge-base-with"], global = true)]
     all_files: bool,
+
+    /// If set, will only lint files under the directory where the configuration file is located and its subdirectories.
+    #[clap(long, global = true)]
+    only_lint_under_config_dir: bool,
 }
 
 #[derive(Debug, Parser)]
@@ -254,6 +258,12 @@ fn do_main() -> Result<i32> {
         RevisionOpt::Head
     };
 
+    let only_lint_under_config_dir = if lint_runner_config.only_lint_under_config_dir.is_some() {
+        lint_runner_config.only_lint_under_config_dir.unwrap()
+    } else {
+        args.only_lint_under_config_dir
+    };
+
     let paths_opt = if let Some(paths_file) = args.paths_from {
         let path_file = AbsPath::try_from(&paths_file)
             .with_context(|| format!("Failed to find `--paths-from` file '{}'", paths_file))?;
@@ -284,6 +294,7 @@ fn do_main() -> Result<i32> {
                 enable_spinners,
                 revision_opt,
                 args.tee_json,
+                only_lint_under_config_dir,
             )
         }
         SubCommand::Lint => {
@@ -298,6 +309,7 @@ fn do_main() -> Result<i32> {
                 enable_spinners,
                 revision_opt,
                 args.tee_json,
+                only_lint_under_config_dir,
             )
         }
         SubCommand::Rage { invocation } => do_rage(&persistent_data_store, invocation),


Test patch:
diff --git a/tests/integration_test.rs b/tests/integration_test.rs
index 97ef06f..50be1d8 100644
--- a/tests/integration_test.rs
+++ b/tests/integration_test.rs
@@ -156,6 +156,38 @@ fn simple_linter() -> Result<()> {
     Ok(())
 }
 
+#[test]
+#[cfg_attr(target_os = "windows", ignore)] // STDERR string is different
+fn simple_linter_only_under_dir() -> Result<()> {
+    let data_path = tempfile::tempdir()?;
+    let lint_message = LintMessage {
+        path: Some("tests/fixtures/fake_source_file.rs".to_string()),
+        line: Some(9),
+        char: Some(1),
+        code: "DUMMY".to_string(),
+        name: "dummy failure".to_string(),
+        severity: LintSeverity::Advice,
+        original: None,
+        replacement: None,
+        description: Some("A dummy linter failure".to_string()),
+    };
+    let config = temp_config_returning_msg(lint_message)?;
+
+    let mut cmd = Command::cargo_bin("lintrunner")?;
+    cmd.arg(format!("--config={}", config.path().to_str().unwrap()));
+    cmd.arg(format!(
+        "--data-path={}",
+        data_path.path().to_str().unwrap()
+    ));
+    cmd.arg("--only-lint-under-config-dir");
+    // Run on a file to ensure that the linter is run. It should skip the file
+    cmd.arg("README.md");
+    cmd.assert().success();
+    assert_output_snapshot("simple_linter_only_under_dir", &mut cmd)?;
+
+    Ok(())
+}
+
 #[test]
 #[cfg_attr(target_os = "windows", ignore)] // STDERR string is different
 fn simple_linter_two_configs() -> Result<()> {
diff --git a/tests/snapshots/integration_test__simple_linter_only_under_dir.snap b/tests/snapshots/integration_test__simple_linter_only_under_dir.snap
new file mode 100644
index 0000000..0805b5a
--- /dev/null
+++ b/tests/snapshots/integration_test__simple_linter_only_under_dir.snap
@@ -0,0 +1,11 @@
+---
+source: tests/integration_test.rs
+expression: output_lines
+---
+- "STDOUT:"
+- ok No lint issues.
+- ""
+- ""
+- "STDERR:"
+- "WARNING: No previous init data found. If this is the first time you're running lintrunner, you should run `lintrunner init`."
+