Repository: o0Ignition0o/cargo-scout (language: rust)

Issue:
Format changed code?
Do you think it would be possible to add a flag/mode to format the changed code? This would be really helpful alongside clippy warnings to keep PRs high quality.

(A lot of projects would pick up a lot of changes that would be irrelevant to the PR if I run `cargo fmt`, so formatting just my diff would be great.)


Discussion / hints:
Hey @davidhewitt :)

I think it would be a great idea, it has raised concerns in the fmt project https://github.com/rust-lang/rustfmt/issues/1324 , and it seems like it would be a bit hard to do.

Other trade offs would include running fmt only on the files that are part of a diff, or maybe try to work on a way to get the shortest part of the AST and apply rust fmt on it.

I wonder if that would be possible, but it would definitely be a great thing to try to figure out!

I would gladly try to work on the ast idea if someone could mentor me, I’m not really sure who I could refer to.
Maybe that would be something @nrc could mentor me on. I’ll try to send the team a message :)
> Maybe that would be something @nrc could mentor me on. I’ll try to send the team a message :)

Sorry, I am not working on Rustfmt anymore. If you comment on an issue on their repo, there should be somebody who can mentor you. Good luck!
Hey and thanks for your reply, I’ll send a message over :)
[There might be a way to use an unstable feature to do it](github.com/nrc/rustfmt#running):

```
If you want to restrict reformatting to specific sets of lines, you can use the --file-lines option. 
Its argument is a JSON array of objects with file and range properties, where file is a file name, and range is an array representing a range of lines like [7,13].
Ranges are 1-based and inclusive of both end points.

rustfmt --file-lines '[
    {"file":"src/lib.rs","range":[7,13]},
    {"file":"src/lib.rs","range":[21,29]},
    {"file":"src/foo.rs","range":[10,11]},
    {"file":"src/foo.rs","range":[15,15]}]'

```

Let's try to play around with that :D 

Fix patch:
diff --git a/.travis.yml b/.travis.yml
index 04bc181..3355c5e 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,31 +1,35 @@
+os: linux
 language: rust
 rust:
   - stable
   - nightly
-sudo: required
-matrix:
+jobs:
   allow_failures:
     - rust: nightly
 env:
-  - RUST_BACKTRACE=1
+  - RUST_BACKTRACE=full
 addons:
   apt:
     packages:
       - libssl-dev
+cache: cargo
 before_script:
   - |
     if [[ "$TRAVIS_RUST_VERSION" == stable ]]; then
-      cargo install cargo-tarpaulin -f
+      cargo install cargo-tarpaulin  
       rustup component add clippy
+      rustup component add rustfmt
+    else
+      rustup component add clippy --toolchain=nightly || cargo install --git https://github.com/rust-lang/rust-clippy/ --force clippy
+      rustup component add rustfmt --toolchain nightly || cargo install --git https://github.com/rust-lang/rustfmt/ --force fmt
     fi
-  - rustup component add rustfmt
 script:
   - cargo fmt -- --check
-  - |
+  - cargo test --all
+  - cargo clippy --all-targets --tests -- -D clippy::pedantic
+after_success: |
     if [[ "$TRAVIS_RUST_VERSION" == stable ]]; then
-      cargo clippy -- -D clippy::pedantic
       USE_SKEPTIC=1 cargo tarpaulin --out Xml
       bash <(curl -s https://codecov.io/bash)
       echo "Uploaded code coverage"
     fi
-  - cargo test --all
diff --git a/README.md b/README.md
index 9e3e931..e10deb7 100644
--- a/README.md
+++ b/README.md
@@ -6,6 +6,9 @@
     <a href="https://travis-ci.com/o0Ignition0o/cargo-scout">
       <img src="https://travis-ci.com/o0Ignition0o/cargo-scout.svg?branch=master" alt="Travis Build Status">
   </a>
+  <a href="https://codecov.io/gh/o0Ignition0o/cargo-scout">
+    <img src="https://codecov.io/gh/o0Ignition0o/cargo-scout/branch/master/graph/badge.svg" />
+  </a>
   <a href="LICENSE-APACHE">
     <img
     src="https://img.shields.io/badge/license-apache2-green.svg" alt="MPL 2.0 License">
@@ -19,8 +22,27 @@
 
 # Leave this world a little better than you found it
 
-A cargo subcommand to get [clippy::pedantic](https://github.com/rust-lang/rust-clippy#clippy) lints for the changes you have made in a codebase.
+A cargo subcommand to get [clippy::pedantic](https://github.com/rust-lang/rust-clippy#clippy) lints or [rustfmt](https://github.com/rust-lang/rustfmt) lints for the changes you have made in a codebase.
+
+Commands and their bash pseudocode would probably look like this:
+
+```bash
+# run clippy pedantic on a diff
+$ cargo-scout lint # git diff | cargo clippy -- -D clippy::pedantic
+
+# run rustfmt on a diff
+$ cargo-scout fmt # git diff | cargo fmt --check
+```
+There's more to it (the commented code wouldn't work), such as workspace management and quite a lot of flags that will hopefully match your usecase. You can find them by running the commands with -h or --help.
+
+If cargo-scout is missing a feature for you to use it, consider filing an issue!
+
 
+```bash
+$ cargo-scout -h
+$ cargo-scout lint -h
+$ cargo-scout fmt -h
+```
 
 ## Current Status
 
@@ -31,7 +53,12 @@ The current minimum Rust version supported is 1.37 stable.
 ## Prerequisites
 Git: In order to compute a set of changes, it requires a project running git.
 
-cargo-clippy as a subcommand: Instructions can be found [in the clippy repository](https://github.com/rust-lang/rust-clippy#as-a-cargo-subcommand-cargo-clippy).
+The linter uses [clippy](https://github.com/rust-lang/rust-clippy) and the formatter uses [rustfmt](https://github.com/rust-lang/rustfmt). Head over to the respective links to figure out how to install it.
+
+Rust nightly: Some commands require a nightly edition of rust, because the features we use aren't available in stable yet ([rustfmt --emit json](https://github.com/rust-lang/rustfmt/issues/3947) and some [cargo clippy features in a workspace setting](https://github.com/rust-lang/cargo/issues/4942)).
+
+We try to keep a close eye to the relevant tracking issues and hope we can switch it to stable soon. If the issues evolved and we didn't notice, please file an issue and let us know !
+
 
 ## How to install
 ```bash
@@ -39,11 +66,15 @@ $ cargo install cargo-scout
 ```
 
 ## How to run it
+
 Open a shell, go to the project you would like to run the command in, and run cargo-scout, with an optional target branch:
 ```bash
-$ cargo-scout # Diff with master
-$ cargo-scout -b <branch_name> # Diff with the target branch you chose.
+$ cargo-scout lint # clippy::pedantic lints on a diff with master
+$ cargo-scout fmt # rustfmt lints on a diff with master
 ```
+
+Each command and subcommand supports -h and --help:
+
 A git diff will be queried and clippy will be run as well, searching for lints that may apply to your diff.
 
 If some lints can apply, the command execution will error out. This design decision has been made so you can put it in your CI pipeline at some point (but please wait for 1.0 release ^^').
diff --git a/cargo-scout-lib/src/lib.rs b/cargo-scout-lib/src/lib.rs
index 6db35e3..f148f1e 100644
--- a/cargo-scout-lib/src/lib.rs
+++ b/cargo-scout-lib/src/lib.rs
@@ -5,3 +5,5 @@ pub mod scout;
 pub mod vcs;
 
 pub use error::Error;
+
+mod utils;
diff --git a/cargo-scout-lib/src/linter/clippy.rs b/cargo-scout-lib/src/linter/clippy.rs
index a89d3e5..cca4b58 100644
--- a/cargo-scout-lib/src/linter/clippy.rs
+++ b/cargo-scout-lib/src/linter/clippy.rs
@@ -1,7 +1,8 @@
-use crate::linter::{Lint, Linter};
+use crate::linter;
+use crate::utils::get_absolute_file_path;
+use serde::Deserialize;
 use std::io::{self, Write};
-use std::path::Path;
-use std::path::PathBuf;
+use std::path::{Path, PathBuf};
 use std::process::Command;
 
 #[derive(Default)]
@@ -13,8 +14,44 @@ pub struct Clippy {
     preview: bool,
 }
 
-impl Linter for Clippy {
-    fn lints(&self, working_dir: PathBuf) -> Result<Vec<Lint>, crate::error::Error> {
+#[derive(Deserialize, Clone)]
+/// A `Linter`s output is a `Vec<Lint>`
+struct Lint {
+    /// The package id
+    /// Example:
+    /// "cargo-scout-lib".to_string()
+    package_id: String,
+    /// The file the lint was reported on
+    /// Example:
+    /// Some("src/lib.rs".to_string())
+    src_path: Option<String>,
+    /// The message structure
+    message: Option<Message>,
+}
+
+#[derive(Deserialize, Clone)]
+/// This struct contains the message output,
+/// and a `Vec<Span>` with the message location
+struct Message {
+    /// The message string
+    /// Example:
+    /// unused variable `count`
+    rendered: String,
+    /// The file names and lines the lint
+    /// was reported on
+    spans: Vec<Span>,
+}
+
+#[derive(Deserialize, Clone)]
+/// A `Span` has a file name, a start and an end line
+struct Span {
+    file_name: String,
+    line_start: u32,
+    line_end: u32,
+}
+
+impl linter::Linter for Clippy {
+    fn lints(&self, working_dir: PathBuf) -> Result<Vec<linter::Lint>, crate::error::Error> {
         println!(
             "[Clippy] - getting lints for directory {}",
             &working_dir.to_str().unwrap_or("<no directory>")
@@ -75,7 +112,14 @@ impl Clippy {
         if let Some(features) = &self.features {
             params.append(&mut vec!["--features", features]);
         }
-        params.append(&mut vec!["--", "-W", "clippy::pedantic"]);
+        params.append(&mut vec![
+            "--tests",
+            "--",
+            "-W",
+            "clippy::all",
+            "-W",
+            "clippy::pedantic",
+        ]);
         params
     }
 
@@ -87,6 +131,10 @@ impl Clippy {
         envs
     }
 
+    // Skipped from code coverage
+    // because an external command
+    // cannot be easily unit tested
+    #[cfg_attr(tarpaulin, skip)]
     fn clippy(&self, path: impl AsRef<Path>) -> Result<String, crate::error::Error> {
         let clippy_pedantic_output = Command::new("cargo")
             .current_dir(path)
@@ -134,24 +182,42 @@ impl Clippy {
     }
 }
 #[must_use]
-fn lints(clippy_output: &str) -> Vec<Lint> {
-    clippy_output
+fn lints(clippy_output: &str) -> Vec<linter::Lint> {
+    let mut lints = Vec::new();
+
+    let clippy_messages: Vec<Message> = clippy_output
         .lines()
         .filter(|l| l.starts_with('{'))
-        .filter_map(|line| serde_json::from_str(line).ok())
-        .filter(|lint: &Lint| {
-            if let Some(m) = &lint.message {
-                !m.spans.is_empty()
+        .filter_map(|line| {
+            if let Ok(lint) = serde_json::from_str::<Lint>(line) {
+                lint.message
             } else {
-                false
+                None
             }
         })
-        .collect::<Vec<Lint>>()
+        .filter(|message: &Message| !message.spans.is_empty())
+        .collect();
+
+    for c in clippy_messages {
+        for s in c.spans {
+            if let Ok(path) = get_absolute_file_path(s.file_name) {
+                lints.push(linter::Lint {
+                    message: c.rendered.clone(),
+                    location: linter::Location {
+                        path,
+                        lines: [s.line_start, s.line_end],
+                    },
+                })
+            }
+        }
+    }
+    lints
 }
 
 #[cfg(test)]
 mod tests {
     use super::*;
+    use crate::error::Error;
     #[test]
     fn test_set_verbose() {
         let mut linter = Clippy::default();
@@ -180,193 +246,92 @@ mod tests {
             "clippy",
             "--message-format",
             "json",
+            "--tests",
             "--",
             "-W",
+            "clippy::all",
+            "-W",
             "clippy::pedantic",
         ];
 
         assert_eq!(expected_command_parameters, linter.command_parameters());
 
         let verbose_linter = linter.set_verbose(true);
-        let verbose_expected_command_parameters = vec![
-            "clippy",
-            "--message-format",
-            "json",
-            "--verbose",
-            "--",
-            "-W",
-            "clippy::pedantic",
-        ];
-        assert_eq!(
-            verbose_expected_command_parameters,
-            verbose_linter.command_parameters()
-        );
+        assert!(verbose_linter.command_parameters().contains(&"--verbose"));
 
         let no_default_features_linter = linter.set_verbose(false).set_no_default_features(true);
-        let no_default_features_expected_command_parameters = vec![
-            "clippy",
-            "--message-format",
-            "json",
-            "--no-default-features",
-            "--",
-            "-W",
-            "clippy::pedantic",
-        ];
-        assert_eq!(
-            no_default_features_expected_command_parameters,
-            no_default_features_linter.command_parameters()
-        );
+        assert!(no_default_features_linter
+            .command_parameters()
+            .contains(&"--no-default-features"));
 
         let all_features_linter = linter
             .set_verbose(false)
             .set_no_default_features(false)
             .set_all_features(true);
-        let all_features_expected_command_parameters = vec![
-            "clippy",
-            "--message-format",
-            "json",
-            "--all-features",
-            "--",
-            "-W",
-            "clippy::pedantic",
-        ];
-        assert_eq!(
-            all_features_expected_command_parameters,
-            all_features_linter.command_parameters()
-        );
+        assert!(all_features_linter
+            .command_parameters()
+            .contains(&"--all-features"));
 
         let features_linter = linter
             .set_all_features(false)
             .set_features(Some(String::from("foo bar baz")));
-        let features_expected_command_parameters = vec![
-            "clippy",
-            "--message-format",
-            "json",
-            "--features",
-            "foo bar baz",
-            "--",
-            "-W",
-            "clippy::pedantic",
-        ];
-        assert_eq!(
-            features_expected_command_parameters,
-            features_linter.command_parameters()
-        );
 
+        assert!(features_linter.command_parameters().contains(&"--features"));
+        assert!(features_linter
+            .command_parameters()
+            .contains(&"foo bar baz"));
+    }
+
+    #[test]
+    fn test_get_command_parameters_nightly() {
         let mut nightly_linter = Clippy::default();
         let nightly_linter = nightly_linter.set_preview(true);
-        let expected_command_parameters = vec![
-            "+nightly",
-            "clippy-preview",
-            "-Z",
-            "unstable-options",
-            "--message-format",
-            "json",
-            "--",
-            "-W",
-            "clippy::pedantic",
-        ];
-        assert_eq!(
-            expected_command_parameters,
-            nightly_linter.command_parameters()
-        );
+        assert!(nightly_linter
+            .command_parameters()
+            .contains(&"clippy-preview"));
 
         let nightly_verbose_linter = nightly_linter.set_verbose(true);
-        let verbose_expected_command_nightly_parameters = vec![
-            "+nightly",
-            "clippy-preview",
-            "-Z",
-            "unstable-options",
-            "--message-format",
-            "json",
-            "--verbose",
-            "--",
-            "-W",
-            "clippy::pedantic",
-        ];
-        assert_eq!(
-            verbose_expected_command_nightly_parameters,
-            nightly_verbose_linter.command_parameters()
-        );
+        assert!(nightly_verbose_linter
+            .command_parameters()
+            .contains(&"--verbose"));
 
         let nightly_all_features_linter = nightly_linter.set_verbose(false).set_all_features(true);
-        let all_features_expected_command_nightly_parameters = vec![
-            "+nightly",
-            "clippy-preview",
-            "-Z",
-            "unstable-options",
-            "--message-format",
-            "json",
-            "--all-features",
-            "--",
-            "-W",
-            "clippy::pedantic",
-        ];
-        assert_eq!(
-            all_features_expected_command_nightly_parameters,
-            nightly_all_features_linter.command_parameters()
-        );
+        assert!(nightly_all_features_linter
+            .command_parameters()
+            .contains(&"--all-features"));
 
         let nightly_no_default_features_linter = nightly_linter
             .set_verbose(false)
             .set_all_features(false)
             .set_no_default_features(true);
-        let no_default_features_expected_command_nightly_parameters = vec![
-            "+nightly",
-            "clippy-preview",
-            "-Z",
-            "unstable-options",
-            "--message-format",
-            "json",
-            "--no-default-features",
-            "--",
-            "-W",
-            "clippy::pedantic",
-        ];
-        assert_eq!(
-            no_default_features_expected_command_nightly_parameters,
-            nightly_no_default_features_linter.command_parameters()
-        );
+        assert!(nightly_no_default_features_linter
+            .command_parameters()
+            .contains(&"--no-default-features"));
 
         let nightly_features_linter = nightly_linter
             .set_no_default_features(false)
             .set_features(Some(String::from("foo bar baz")));
-        let features_expected_command_nightly_parameters = vec![
-            "+nightly",
-            "clippy-preview",
-            "-Z",
-            "unstable-options",
-            "--message-format",
-            "json",
-            "--features",
-            "foo bar baz",
-            "--",
-            "-W",
-            "clippy::pedantic",
-        ];
-        assert_eq!(
-            features_expected_command_nightly_parameters,
-            nightly_features_linter.command_parameters()
-        );
+        assert!(nightly_features_linter
+            .command_parameters()
+            .contains(&"--features"));
+        assert!(nightly_features_linter
+            .command_parameters()
+            .contains(&"foo bar baz"));
     }
     #[test]
-    fn test_lints() {
-        use crate::linter::{Message, Span};
-        let expected_lints = vec![Lint {
-            package_id: "cargo-scout".to_string(),
-            src_path: Some("test/foo/bar.rs".to_string()),
-            message: Some(Message {
-                rendered: "this is a test lint".to_string(),
-                spans: vec![Span {
-                    file_name: "test/foo/baz.rs".to_string(),
-                    line_start: 10,
-                    line_end: 12,
-                }],
-            }),
+    fn test_lints() -> Result<(), Error> {
+        use crate::linter;
+        let expected_lints = vec![linter::Lint {
+            message: "this is a test lint".to_string(),
+            location: linter::Location {
+                path: get_absolute_file_path("test/foo/baz.rs")?,
+                lines: [10, 12],
+            },
         }];
 
         let clippy_output = r#"{"package_id": "cargo-scout","src_path": "test/foo/bar.rs","message": { "rendered": "this is a test lint","spans": [{"file_name": "test/foo/baz.rs","line_start": 10,"line_end": 12}]}}"#;
 
         assert_eq!(expected_lints, lints(clippy_output));
+        Ok(())
     }
 }
diff --git a/cargo-scout-lib/src/linter/mod.rs b/cargo-scout-lib/src/linter/mod.rs
index 98a988d..adf59c9 100644
--- a/cargo-scout-lib/src/linter/mod.rs
+++ b/cargo-scout-lib/src/linter/mod.rs
@@ -1,44 +1,28 @@
-use serde::Deserialize;
 use std::path::PathBuf;
 
 pub mod clippy;
+pub mod rustfmt;
 
 pub trait Linter {
     fn lints(&self, working_dir: PathBuf) -> Result<Vec<Lint>, crate::error::Error>;
 }
 
-#[derive(Deserialize, PartialEq, Debug, Clone)]
-/// A `Linter`s output is a `Vec<Lint>`
+/// This struct contains the lint,
+/// It may contain a message, and a location.
+#[derive(PartialEq, Clone, Debug)]
 pub struct Lint {
-    /// The package id
-    /// Example:
-    /// "cargo-scout-lib".to_string()
-    pub package_id: String,
-    /// The file the lint was reported on
-    /// Example:
-    /// Some("src/lib.rs".to_string())
-    pub src_path: Option<String>,
-    /// The message structure
-    pub message: Option<Message>,
-}
-
-#[derive(Deserialize, PartialEq, Debug, Clone)]
-/// This struct contains the message output,
-/// and a `Vec<Span>` with the message location
-pub struct Message {
     /// The message string
     /// Example:
     /// unused variable `count`
-    pub rendered: String,
+    pub message: String,
     /// The file names and lines the lint
     /// was reported on
-    pub spans: Vec<Span>,
+    pub location: Location,
 }
 
-#[derive(Deserialize, PartialEq, Debug, Clone)]
-/// A `Span` has a file name, a start and an end line
-pub struct Span {
-    pub file_name: String,
-    pub line_start: u32,
-    pub line_end: u32,
+/// A `Location` has a file name, a start and an end line
+#[derive(PartialEq, Clone, Debug)]
+pub struct Location {
+    pub path: String,
+    pub lines: [u32; 2],
 }
diff --git a/cargo-scout-lib/src/linter/rustfmt.rs b/cargo-scout-lib/src/linter/rustfmt.rs
new file mode 100644
index 0000000..e88268c
--- /dev/null
+++ b/cargo-scout-lib/src/linter/rustfmt.rs
@@ -0,0 +1,187 @@
+use crate::error::Error;
+use crate::linter::{Lint, Linter, Location};
+use crate::utils::get_absolute_file_path;
+use serde::Deserialize;
+use serde_json;
+use std::path::{Path, PathBuf};
+use std::process::Command;
+
+#[derive(Default)]
+pub struct RustFmt {}
+
+impl Linter for RustFmt {
+    fn lints(&self, working_dir: PathBuf) -> Result<Vec<Lint>, Error> {
+        println!(
+            "[RustFmt] - checking format for directory {}",
+            &working_dir.to_str().unwrap_or("<no directory>")
+        );
+        let rustfmt_output = Self::fmt(working_dir)?;
+        lints(&rustfmt_output)
+    }
+}
+
+impl RustFmt {
+    fn command_parameters() -> Vec<&'static str> {
+        vec!["+nightly", "fmt", "--", "--emit", "json"]
+    }
+
+    // Skipped from code coverage
+    // because an external command
+    // cannot be easily unit tested
+    #[cfg_attr(tarpaulin, skip)]
+    fn fmt(path: impl AsRef<Path>) -> Result<String, Error> {
+        let fmt_output = Command::new("cargo")
+            .current_dir(path)
+            .args(Self::command_parameters())
+            .output()
+            .expect("failed to run cargo fmt");
+
+        if fmt_output.status.success() {
+            Ok(String::from_utf8(fmt_output.stdout)?)
+        } else {
+            Err(Error::Command(String::from_utf8(fmt_output.stderr)?))
+        }
+    }
+}
+
+#[derive(Deserialize, Debug)]
+struct FmtLint {
+    name: String,
+    mismatches: Vec<FmtMismatch>,
+}
+
+#[derive(Deserialize, Debug)]
+struct FmtMismatch {
+    original_begin_line: u32,
+    original_end_line: u32,
+    original: String,
+    expected: String,
+}
+
+fn lints(fmt_output: &str) -> Result<Vec<Lint>, Error> {
+    let mut lints = Vec::new();
+    let fmt_lints: Vec<FmtLint> = serde_json::from_str(fmt_output)?;
+    for fmt_lint in fmt_lints {
+        lints.append(
+            &mut fmt_lint
+                .mismatches
+                .iter()
+                .filter_map(|mismatch| {
+                    if let Ok(path) = get_absolute_file_path(fmt_lint.name.clone()) {
+                        Some(Lint {
+                            message: display_mismatch(mismatch, &path),
+                            location: Location {
+                                path,
+                                lines: [mismatch.original_begin_line, mismatch.original_end_line],
+                            },
+                        })
+                    } else {
+                        None
+                    }
+                })
+                .collect::<Vec<Lint>>(),
+        );
+    }
+    Ok(lints)
+}
+
+fn display_mismatch(mismatch: &FmtMismatch, path: &str) -> String {
+    if mismatch.original_begin_line == mismatch.original_end_line {
+        format!(
+            "Diff in {} at line {}:\n-{}\n+{}\n",
+            path, mismatch.original_begin_line, mismatch.original, mismatch.expected
+        )
+    } else {
+        format!(
+            "Diff in {} between lines {} and {}:\n{}\n{}\n",
+            path,
+            mismatch.original_begin_line,
+            mismatch.original_end_line,
+            mismatch
+                .original
+                .lines()
+                .map(|line| format!("-{}", line))
+                .collect::<Vec<String>>()
+                .join("\n"),
+            mismatch
+                .expected
+                .lines()
+                .map(|line| format!("+{}", line))
+                .collect::<Vec<String>>()
+                .join("\n")
+        )
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_command_parameters() {
+        assert_eq!(
+            vec!["+nightly", "fmt", "--", "--emit", "json"],
+            RustFmt::command_parameters()
+        );
+    }
+
+    #[test]
+    fn test_display_mismatch_one_line() -> Result<(), Error> {
+        let mismatch = FmtMismatch {
+            original_begin_line: 1,
+            original_end_line: 1,
+            original: "    this is a test mismatch".to_string(),
+            expected: "this is a test mismatch".to_string(),
+        };
+
+        let path = get_absolute_file_path("src/foo/bar.txt")?;
+
+        let expected_display = format!(
+            "Diff in {} at line 1:\n-    this is a test mismatch\n+this is a test mismatch\n",
+            path
+        );
+        let actual_display = display_mismatch(&mismatch, &path);
+        assert_eq!(expected_display, actual_display);
+        Ok(())
+    }
+
+    #[test]
+    fn test_display_mismatch_several_lines() -> Result<(), Error> {
+        let mismatch = FmtMismatch {
+            original_begin_line: 1,
+            original_end_line: 2,
+            original: "    this is a test mismatch\n  the indent is wrong".to_string(),
+            expected: "this is a test mismatch\nthe indent is wrong".to_string(),
+        };
+
+        let path = get_absolute_file_path("src/foo/bar.txt")?;
+
+        let expected_display = format!("Diff in {} between lines 1 and 2:\n-    this is a test mismatch\n-  the indent is wrong\n+this is a test mismatch\n+the indent is wrong\n", path);
+        let actual_display = display_mismatch(&mismatch, &path);
+        assert_eq!(expected_display, actual_display);
+        Ok(())
+    }
+
+    #[test]
+    fn test_lints() -> Result<(), crate::error::Error> {
+        let fmt_output = r#"[{"name":"cargo-scout/cargo-scout-lib/src/lib.rs","mismatches":[{"original_begin_line":1,"original_end_line":1,"expected_begin_line":1,"expected_end_line":1,"original":"    pub mod config;","expected":"pub mod config;"}]}]"#;
+
+        let path = get_absolute_file_path("cargo-scout/cargo-scout-lib/src/lib.rs")?;
+        let expected_lints = vec![Lint {
+            location: Location {
+                lines: [1, 1],
+                path: path.clone(),
+            },
+            message: format!(
+                "Diff in {} at line 1:\n-    pub mod config;\n+pub mod config;\n",
+                path
+            ),
+        }];
+
+        let actual_lints = lints(fmt_output).unwrap();
+
+        assert_eq!(expected_lints, actual_lints);
+
+        Ok(())
+    }
+}
diff --git a/cargo-scout-lib/src/scout/mod.rs b/cargo-scout-lib/src/scout/mod.rs
index e9c2ab4..927ed80 100644
--- a/cargo-scout-lib/src/scout/mod.rs
+++ b/cargo-scout-lib/src/scout/mod.rs
@@ -1,6 +1,7 @@
 use crate::config::*;
-use crate::linter::*;
+use crate::linter::{Lint, Linter};
 use crate::vcs::*;
+use std::path::PathBuf;
 
 pub struct Scout<V, C, L>
 where
@@ -27,57 +28,55 @@ where
         }
     }
     pub fn run(&self) -> Result<Vec<Lint>, crate::error::Error> {
-        let diff_sections = self.vcs.sections(".")?;
-        let current_dir = std::fs::canonicalize(".")?;
+        let current_dir = std::fs::canonicalize(std::env::current_dir()?)?;
+        let diff_sections = self.vcs.sections(current_dir.clone())?;
         let mut lints = Vec::new();
-        let members = self.config.members();
+        let config_members = self.config.members();
+        let members = config_members.iter().map(|m| {
+            let mut member = current_dir.clone();
+            member.push(m);
+            member
+        });
         // There's no need to run the linter on members where no changes have been made
-        let relevant_members = members.iter().filter(|m| diff_in_member(m, &diff_sections));
+        let relevant_members = members.filter(|m| diff_in_member(m, &diff_sections));
         for m in relevant_members {
-            lints.extend(self.linter.lints(current_dir.join(m))?);
+            lints.extend(self.linter.lints(current_dir.clone().join(m))?);
         }
         println!("[Scout] - checking for intersections");
         Ok(lints_from_diff(&lints, &diff_sections))
     }
 }
 
-fn diff_in_member(member: &str, sections: &[Section]) -> bool {
-    for s in sections {
-        if s.file_name.starts_with(&member) {
-            return true;
+fn diff_in_member(member: &PathBuf, sections: &[Section]) -> bool {
+    if let Some(m) = member.to_str() {
+        for s in sections {
+            if s.file_name.starts_with(&m) {
+                return true;
+            }
         }
     }
     false
 }
 
-// Check if clippy_lint and git_section have overlapped lines
-fn lines_in_range(clippy_lint: &Span, git_section: &Section) -> bool {
-    // If git_section.line_start is included in the clippy_lint span
-    clippy_lint.line_start <= git_section.line_start && git_section.line_start <= clippy_lint.line_end ||
-    // If clippy_lint.line_start is included in the git_section span
-    git_section.line_start <= clippy_lint.line_start && clippy_lint.line_start <= git_section.line_end
+// Check if lint and git_section have overlapped lines
+fn lines_in_range(lint: &Lint, git_section: &Section) -> bool {
+    // If git_section.line_start is included in the lint span
+    lint.location.lines[0] <= git_section.line_start && git_section.line_start <= lint.location.lines[1] ||
+    // If lint.line_start is included in the git_section span
+    git_section.line_start <= lint.location.lines[0] && lint.location.lines[0] <= git_section.line_end
 }
 
-fn files_match(clippy_lint: &Span, git_section: &Section) -> bool {
+fn files_match(lint: &Lint, git_section: &Section) -> bool {
     // Git diff paths and clippy paths don't get along too well on Windows...
-    clippy_lint.file_name.replace("\\", "/") == git_section.file_name.replace("\\", "/")
+    lint.location.path.replace("\\", "/") == git_section.file_name.replace("\\", "/")
 }
 
 fn lints_from_diff(lints: &[Lint], diffs: &[Section]) -> Vec<Lint> {
     let mut lints_in_diff = Vec::new();
     for diff in diffs {
-        let diff_lints = lints.iter().filter(|lint| {
-            if let Some(m) = &lint.message {
-                for s in &m.spans {
-                    if files_match(&s, &diff) && lines_in_range(&s, &diff) {
-                        return true;
-                    };
-                }
-                false
-            } else {
-                false
-            }
-        });
+        let diff_lints = lints
+            .iter()
+            .filter(|lint| files_match(&lint, &diff) && lines_in_range(&lint, &diff));
         for l in diff_lints {
             lints_in_diff.push(l.clone());
         }
@@ -90,7 +89,8 @@ mod scout_tests {
     use super::*;
     use crate::config::Config;
     use crate::error::Error;
-    use crate::linter::{Lint, Linter};
+    use crate::linter::{Lint, Linter, Location};
+    use crate::utils::get_absolute_file_path;
     use std::cell::RefCell;
     use std::clone::Clone;
     use std::path::{Path, PathBuf};
@@ -114,22 +114,31 @@ mod scout_tests {
         }
     }
     struct TestLinter {
-        // Using a RefCell here becauselints
+        // Using a RefCell here because lints
         // takes &self and not &mut self.
         // We use usize here because we will compare it to a Vec::len()
         lints_times_called: Rc<RefCell<usize>>,
+        lints: Vec<Lint>,
     }
     impl TestLinter {
         pub fn new() -> Self {
             Self {
                 lints_times_called: Rc::new(RefCell::new(0)),
+                lints: Vec::new(),
+            }
+        }
+
+        pub fn with_lints(lints: Vec<Lint>) -> Self {
+            Self {
+                lints_times_called: Rc::new(RefCell::new(0)),
+                lints,
             }
         }
     }
     impl Linter for TestLinter {
         fn lints(&self, _working_dir: PathBuf) -> Result<Vec<Lint>, crate::error::Error> {
             *self.lints_times_called.borrow_mut() += 1;
-            Ok(Vec::new())
+            Ok(self.lints.clone())
         }
     }
     struct TestConfig {
@@ -137,7 +146,7 @@ mod scout_tests {
     }
     impl TestConfig {
         pub fn new(members: Vec<String>) -> Self {
-            TestConfig { members }
+            Self { members }
         }
     }
     impl Config for TestConfig {
@@ -166,11 +175,37 @@ mod scout_tests {
     #[test]
     fn test_scout_no_workspace_one_diff() -> Result<(), crate::error::Error> {
         let diff = vec![Section {
-            file_name: "foo/bar.rs".to_string(),
+            file_name: get_absolute_file_path("foo/bar.rs")?,
             line_start: 0,
             line_end: 10,
         }];
-        let linter = TestLinter::new();
+
+        let lints = vec![
+            Lint {
+                location: Location {
+                    lines: [2, 2],
+                    path: get_absolute_file_path("foo/bar.rs")?,
+                },
+                message: "Test lint".to_string(),
+            },
+            Lint {
+                location: Location {
+                    lines: [12, 22],
+                    path: get_absolute_file_path("foo/bar.rs")?,
+                },
+                message: "This lint is not in diff".to_string(),
+            },
+        ];
+
+        let expected_lints_from_diff = vec![Lint {
+            location: Location {
+                lines: [2, 2],
+                path: get_absolute_file_path("foo/bar.rs")?,
+            },
+            message: "Test lint".to_string(),
+        }];
+
+        let linter = TestLinter::with_lints(lints);
         let vcs = TestVCS::new(diff);
         // The member matches the file name
         let config = TestConfig::new(vec!["foo".to_string()]);
@@ -180,15 +215,16 @@ mod scout_tests {
         // We don't check for the lints result here.
         // It is already tested in the linter tests
         // and in intersection tests
-        let _ = scout.run()?;
+        let actual_lints_from_diff = scout.run()?;
         assert_eq!(expected_times_called, *actual_times_called.borrow());
+        assert_eq!(expected_lints_from_diff, actual_lints_from_diff);
         Ok(())
     }
 
     #[test]
     fn test_scout_no_workspace_one_diff_not_relevant_member() -> Result<(), crate::error::Error> {
         let diff = vec![Section {
-            file_name: "baz/bar.rs".to_string(),
+            file_name: get_absolute_file_path("baz/bar.rs")?,
             line_start: 0,
             line_end: 10,
         }];
@@ -211,12 +247,12 @@ mod scout_tests {
     fn test_scout_in_workspace() -> Result<(), crate::error::Error> {
         let diff = vec![
             Section {
-                file_name: "member1/bar.rs".to_string(),
+                file_name: get_absolute_file_path("member1/bar.rs")?,
                 line_start: 0,
                 line_end: 10,
             },
             Section {
-                file_name: "member2/baz.rs".to_string(),
+                file_name: get_absolute_file_path("member2/bar.rs")?,
                 line_start: 0,
                 line_end: 10,
             },
@@ -245,7 +281,7 @@ mod scout_tests {
 
 #[cfg(test)]
 mod intersections_tests {
-    use crate::linter::Span;
+    use crate::linter::{Lint, Location};
     use crate::vcs::Section;
 
     type TestSection = (&'static str, u32, u32);
@@ -302,21 +338,26 @@ mod intersections_tests {
     fn assert_all_files_match(ranges: Vec<(TestSection, TestSection)>) {
         use crate::scout::files_match;
         for range in ranges {
-            let lint = range.0;
-            let section = range.1;
-            let clippy_lint = Span {
-                file_name: String::from(lint.0),
-                line_start: lint.1,
-                line_end: lint.2,
+            let lint_section = range.0;
+            let git_section = range.1;
+            let lint = Lint {
+                message: String::new(),
+                location: Location {
+                    path: String::from(lint_section.0),
+                    lines: [lint_section.1, lint_section.2],
+                },
             };
-            let git_section = Section {
-                file_name: String::from(section.0),
-                line_start: section.1,
-                line_end: section.2,
+            let git = Section {
+                file_name: String::from(git_section.0),
+                line_start: git_section.1,
+                line_end: git_section.2,
             };
             assert!(
-                files_match(&clippy_lint, &git_section),
-                print!("Expected files match for {} and {}", lint.0, section.0)
+                files_match(&lint, &git),
+                print!(
+                    "Expected files match for {} and {}",
+                    lint_section.0, git_section.0
+                )
             );
         }
     }
@@ -324,21 +365,26 @@ mod intersections_tests {
     fn assert_no_files_match(ranges: Vec<(TestSection, TestSection)>) {
         use crate::scout::files_match;
         for range in ranges {
-            let lint = range.0;
-            let section = range.1;
-            let clippy_lint = Span {
-                file_name: String::from(lint.0),
-                line_start: lint.1,
-                line_end: lint.2,
+            let lint_section = range.0;
+            let git_section = range.1;
+            let lint = Lint {
+                message: String::new(),
+                location: Location {
+                    path: String::from(lint_section.0),
+                    lines: [lint_section.1, lint_section.2],
+                },
             };
-            let git_section = Section {
-                file_name: String::from(section.0),
-                line_start: section.1,
-                line_end: section.2,
+            let git = Section {
+                file_name: String::from(git_section.0),
+                line_start: git_section.1,
+                line_end: git_section.2,
             };
             assert!(
-                !files_match(&clippy_lint, &git_section),
-                print!("Expected files match for {} and {}", lint.0, section.0)
+                !files_match(&lint, &git),
+                print!(
+                    "Expected files not to match for {} and {}",
+                    lint_section.0, git_section.0
+                )
             );
         }
     }
@@ -371,19 +417,21 @@ mod intersections_tests {
         }
     }
 
-    fn in_range(lint: (&str, u32, u32), section: (&str, u32, u32)) -> bool {
+    fn in_range(lint_section: (&str, u32, u32), git_section: (&str, u32, u32)) -> bool {
         use crate::scout::lines_in_range;
-        let clippy_lint = Span {
-            file_name: String::from(lint.0),
-            line_start: lint.1,
-            line_end: lint.2,
+        let lint = Lint {
+            message: String::new(),
+            location: Location {
+                path: String::from(lint_section.0),
+                lines: [lint_section.1, lint_section.2],
+            },
         };
 
         let git_section = Section {
-            file_name: String::from(section.0),
-            line_start: section.1,
-            line_end: section.2,
+            file_name: String::from(git_section.0),
+            line_start: git_section.1,
+            line_end: git_section.2,
         };
-        lines_in_range(&clippy_lint, &git_section)
+        lines_in_range(&lint, &git_section)
     }
 }
diff --git a/cargo-scout-lib/src/utils.rs b/cargo-scout-lib/src/utils.rs
new file mode 100644
index 0000000..5dfca29
--- /dev/null
+++ b/cargo-scout-lib/src/utils.rs
@@ -0,0 +1,8 @@
+use crate::error::Error;
+use std::path::Path;
+
+pub fn get_absolute_file_path(file_path: impl AsRef<Path>) -> Result<String, Error> {
+    let mut absolute_path = std::env::current_dir()?;
+    absolute_path.push(file_path);
+    Ok(absolute_path.to_string_lossy().to_string())
+}
diff --git a/cargo-scout-lib/src/vcs/git.rs b/cargo-scout-lib/src/vcs/git.rs
index 39bfd0c..348a3f5 100644
--- a/cargo-scout-lib/src/vcs/git.rs
+++ b/cargo-scout-lib/src/vcs/git.rs
@@ -1,5 +1,6 @@
 use super::*;
 use crate::error::Error;
+use crate::utils::get_absolute_file_path;
 use git2::{Delta, DiffOptions, Repository};
 use std::path::Path;
 
@@ -45,13 +46,16 @@ impl VCS for Git {
                 match delta.status() {
                     Delta::Modified | Delta::Added | Delta::Untracked => {
                         if let Some(file_path) = delta.new_file().path() {
-                            let file_path = file_path.to_string_lossy().to_string();
-                            if file_path.ends_with(".rs") {
-                                sections.push(Section {
-                                    file_name: file_path,
-                                    line_start: hunk.new_start(),
-                                    line_end: hunk.new_start() + hunk.new_lines(),
-                                });
+                            // Path returns the path of the entry relative to the working directory.
+                            // We can get the absolute path
+                            if let Ok(file_name) = get_absolute_file_path(&file_path) {
+                                if file_name.ends_with(".rs") {
+                                    sections.push(Section {
+                                        file_name,
+                                        line_start: hunk.new_start(),
+                                        line_end: hunk.new_start() + hunk.new_lines(),
+                                    });
+                                }
                             }
                         }
                     }
@@ -92,12 +96,12 @@ mod tests {
 
         let expected = vec![
             Section {
-                file_name: "bar.rs".into(),
+                file_name: get_absolute_file_path(&"bar.rs")?,
                 line_start: 1,
                 line_end: 5,
             },
             Section {
-                file_name: "foo.rs".into(),
+                file_name: get_absolute_file_path(&"foo.rs")?,
                 line_start: 1,
                 line_end: 7,
             },
@@ -117,12 +121,12 @@ mod tests {
 
         let expected = vec![
             Section {
-                file_name: "foo.rs".into(),
+                file_name: get_absolute_file_path(&"foo.rs")?,
                 line_start: 1,
                 line_end: 7,
             },
             Section {
-                file_name: "inside/some/dir/bar.rs".into(),
+                file_name: get_absolute_file_path(&"inside/some/dir/bar.rs")?,
                 line_start: 1,
                 line_end: 5,
             },
@@ -147,22 +151,22 @@ mod tests {
 
         let expected = vec![
             Section {
-                file_name: "bar.rs".into(),
+                file_name: get_absolute_file_path(&"bar.rs")?,
                 line_start: 1,
                 line_end: 2,
             },
             Section {
-                file_name: "bar.rs".into(),
+                file_name: get_absolute_file_path(&"bar.rs")?,
                 line_start: 5,
                 line_end: 9,
             },
             Section {
-                file_name: "foo.rs".into(),
+                file_name: get_absolute_file_path(&"foo.rs")?,
                 line_start: 3,
                 line_end: 4,
             },
             Section {
-                file_name: "foo.rs".into(),
+                file_name: get_absolute_file_path(&"foo.rs")?,
                 line_start: 6,
                 line_end: 7,
             },
@@ -182,7 +186,7 @@ mod tests {
             .stage(&["foo.rs", "bar.txt"])?;
 
         let expected = vec![Section {
-            file_name: "foo.rs".into(),
+            file_name: get_absolute_file_path(&"foo.rs")?,
             line_start: 1,
             line_end: 7,
         }];
@@ -204,12 +208,12 @@ mod tests {
 
         let expected = vec![
             Section {
-                file_name: "foo.rs".into(),
+                file_name: get_absolute_file_path(&"foo.rs")?,
                 line_start: 3,
                 line_end: 4,
             },
             Section {
-                file_name: "foo.rs".into(),
+                file_name: get_absolute_file_path(&"foo.rs")?,
                 line_start: 6,
                 line_end: 7,
             },
@@ -242,7 +246,7 @@ mod tests {
                 let sig = repo.signature()?;
                 repo.commit(Some("HEAD"), &sig, &sig, "initial", &tree, &[])?;
             }
-            Ok(RepoFixture { dir, repo })
+            Ok(Self { dir, repo })
         }
 
         pub fn write<P: AsRef<Path>>(self, path: P, test_file: &str) -> Result<Self> {
diff --git a/cargo-scout/src/main.rs b/cargo-scout/src/main.rs
index 266f820..59b6802 100644
--- a/cargo-scout/src/main.rs
+++ b/cargo-scout/src/main.rs
@@ -1,18 +1,43 @@
 use cargo_scout_lib::config::rust::CargoConfig;
 use cargo_scout_lib::linter::clippy::Clippy;
+use cargo_scout_lib::linter::rustfmt::RustFmt;
 use cargo_scout_lib::linter::Lint;
 use cargo_scout_lib::scout::Scout;
 use cargo_scout_lib::vcs::git::Git;
 use cargo_scout_lib::Error;
 use structopt::StructOpt;
 
-#[derive(Debug, StructOpt)]
+#[derive(StructOpt)]
 #[structopt(
     name = "cargo-scout",
     author,
     about = "Leave the codebase better than when you found it."
 )]
-struct Options {
+enum Command {
+    Fmt(FmtOptions),
+    Lint(LintOptions),
+}
+
+#[derive(Debug, StructOpt)]
+struct FmtOptions {
+    #[structopt(
+        short = "b",
+        long = "branch",
+        value_name = "branch",
+        default_value = "master"
+    )]
+    /// Set the target branch
+    branch: String,
+    #[structopt(short = "t", long = "cargo-toml", default_value = "./Cargo.toml")]
+    /// Pass the path of the `Cargo.toml` file
+    cargo_toml: String,
+    #[structopt(short = "w", long = "without-error")]
+    /// Set to display the warnings without actually returning an error
+    without_error: bool,
+}
+
+#[derive(Debug, StructOpt)]
+struct LintOptions {
     #[structopt(short = "v", long = "verbose")]
     /// Set the verbosity level
     verbose: bool,
@@ -45,8 +70,17 @@ struct Options {
     preview: bool,
 }
 
+// There is no logic to test
+#[cfg_attr(tarpaulin, skip)]
 fn main() -> Result<(), Error> {
-    let opts = Options::from_args();
+    match Command::from_args() {
+        Command::Fmt(opts) => run_fmt(opts),
+        Command::Lint(opts) => run_lint(opts),
+    }
+}
+
+#[cfg_attr(tarpaulin, skip)]
+fn run_lint(opts: LintOptions) -> Result<(), Error> {
     let fail_if_errors = opts.without_error;
 
     let vcs = Git::with_target(opts.branch);
@@ -58,6 +92,18 @@ fn main() -> Result<(), Error> {
         .set_all_features(opts.all_features)
         .set_features(opts.features)
         .set_preview(opts.preview);
+    let scout = Scout::new(vcs, config, linter);
+    let relevant_lints = scout.run()?;
+    return_warnings(&relevant_lints, fail_if_errors)
+}
+
+#[cfg_attr(tarpaulin, skip)]
+fn run_fmt(opts: FmtOptions) -> Result<(), Error> {
+    let fail_if_errors = opts.without_error;
+
+    let vcs = Git::with_target(opts.branch);
+    let config = CargoConfig::from_manifest_path(opts.cargo_toml)?;
+    let linter = RustFmt::default();
 
     let scout = Scout::new(vcs, config, linter);
     let relevant_lints = scout.run()?;
@@ -66,7 +112,7 @@ fn main() -> Result<(), Error> {
 
 fn return_warnings(lints: &[Lint], without_error: bool) -> Result<(), Error> {
     if lints.is_empty() {
-        println!("No warnings raised by clippy::pedantic in your diff, you're good to go!");
+        println!("No issues in your diff, you're good to go!");
         Ok(())
     } else {
         display_warnings(&lints);
@@ -80,24 +126,29 @@ fn return_warnings(lints: &[Lint], without_error: bool) -> Result<(), Error> {
 
 fn display_warnings(warnings: &[Lint]) {
     for w in warnings {
-        if let Some(m) = &w.message {
-            for l in m.rendered.split('\n') {
-                println!("{}", l);
-            }
+        for l in w.message.split('\n') {
+            println!("{}", l);
         }
     }
-    println!("Clippy::pedantic found {} warnings", warnings.len());
+    if warnings.len() == 1 {
+        println!("Cargo scout found a warning");
+    } else {
+        println!("Cargo scout found {} warnings", warnings.len());
+    }
 }
 
 #[cfg(test)]
 mod tests {
     use super::*;
+    use cargo_scout_lib::linter::Location;
     #[test]
     fn test_return_status_with_lints() {
         let lints = vec![Lint {
-            package_id: "cargo-scout".to_string(),
-            src_path: None,
-            message: None,
+            message: String::new(),
+            location: Location {
+                path: String::new(),
+                lines: [0, 0],
+            },
         }];
 
         assert!(return_warnings(&lints, true).is_ok());


Test patch: