Skip to content

Commit

Permalink
feat: implement json type returner
Browse files Browse the repository at this point in the history
  • Loading branch information
benfdking committed Dec 11, 2024
1 parent 26cb11d commit a210d65
Show file tree
Hide file tree
Showing 22 changed files with 245 additions and 9 deletions.
4 changes: 4 additions & 0 deletions crates/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ harness = false
name = "ui_github"
harness = false

[[test]]
name = "ui_json"
harness = false

[features]
python = ["sqruff-lib/python"]
codegen-docs = ["clap-markdown", "minijinja", "serde", "python"]
Expand Down
1 change: 1 addition & 0 deletions crates/cli/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ pub(crate) struct FixArgs {
pub(crate) enum Format {
Human,
GithubAnnotationNative,
Json,
}

impl Default for Format {
Expand Down
5 changes: 5 additions & 0 deletions crates/cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use clap::Parser as _;
use commands::Format;
use sqruff_lib::cli::formatters::Formatter;
use sqruff_lib::cli::json::JsonFormatter;
use sqruff_lib::cli::{
formatters::OutputStreamFormatter,
github_annotation_native_formatter::GithubAnnotationNativeFormatter,
Expand Down Expand Up @@ -103,6 +104,10 @@ pub(crate) fn linter(config: FluffConfig, format: Format) -> Linter {
let formatter = GithubAnnotationNativeFormatter::new(output_stream);
Arc::new(formatter)
}
Format::Json => {
let formatter = JsonFormatter::default();
Arc::new(formatter)
}
};

Linter::new(config, Some(formatter), None)
Expand Down
1 change: 1 addition & 0 deletions crates/cli/tests/json/LT01_noqa.exitcode
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0
1 change: 1 addition & 0 deletions crates/cli/tests/json/LT01_noqa.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SELECT 1; --noqa: LT01
Empty file.
1 change: 1 addition & 0 deletions crates/cli/tests/json/LT01_noqa.stdout
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"tests/lint/LT01_noqa.sql":[]}
1 change: 1 addition & 0 deletions crates/cli/tests/json/hql_file.exitcode
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1
1 change: 1 addition & 0 deletions crates/cli/tests/json/hql_file.hql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SELECT 1;
Empty file.
1 change: 1 addition & 0 deletions crates/cli/tests/json/hql_file.stdout
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"tests/lint/hql_file.hql":[{"range":{"start":{"line":1,"character":7},"end":{"line":1,"character":7}},"message":"Expected only single space before \"1\". Found \" \".","severity":"Error","source":"sqruff"},{"range":{"start":{"line":1,"character":11},"end":{"line":1,"character":11}},"message":"Files must end with a single trailing newline.","severity":"Error","source":"sqruff"}]}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SELECT 1 ,4
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"tests/lint/test_fail_whitespace_before_comma.sql":[{"range":{"start":{"line":1,"character":8},"end":{"line":1,"character":8}},"message":"Column expression without alias. Use explicit `AS` clause.","severity":"Error","source":"sqruff"},{"range":{"start":{"line":1,"character":11},"end":{"line":1,"character":11}},"message":"Column expression without alias. Use explicit `AS` clause.","severity":"Error","source":"sqruff"},{"range":{"start":{"line":1,"character":9},"end":{"line":1,"character":9}},"message":"Unexpected whitespace before comma.","severity":"Error","source":"sqruff"},{"range":{"start":{"line":1,"character":11},"end":{"line":1,"character":11}},"message":"Expected single whitespace between \",\" and \"4\".","severity":"Error","source":"sqruff"},{"range":{"start":{"line":1,"character":12},"end":{"line":1,"character":12}},"message":"Files must end with a single trailing newline.","severity":"Error","source":"sqruff"}]}
67 changes: 67 additions & 0 deletions crates/cli/tests/ui_json.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
use std::fs;
use std::path::PathBuf;

use assert_cmd::Command;
use expect_test::expect_file;

fn main() {
let profile = if cfg!(debug_assertions) {
"debug"
} else {
"release"
};
let mut lint_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
lint_dir.push("tests/json");

// Iterate over each test file in the directory
for entry in fs::read_dir(&lint_dir).unwrap() {
let entry = entry.unwrap();
let path = entry.path();

// Check if the file has a .sql or .hql extension
if path
.extension()
.and_then(|e| e.to_str())
.map_or(false, |ext| ext == "sql" || ext == "hql")
{
// Construct the path to the sqruff binary
let mut sqruff_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
sqruff_path.push(format!("../../target/{}/sqruff", profile));

// Set up the command with arguments
let mut cmd = Command::new(sqruff_path);

cmd.arg("lint");
cmd.arg(path.to_str().unwrap());
cmd.arg("-f");
cmd.arg("json");
// Set the HOME environment variable to the fake home directory
cmd.env("HOME", PathBuf::from(env!("CARGO_MANIFEST_DIR")));

// Run the command and capture the output
let assert = cmd.assert();

// Construct the expected output file paths
let mut expected_output_path_stderr = path.clone();
expected_output_path_stderr.set_extension("stderr");
let mut expected_output_path_stdout = path.clone();
expected_output_path_stdout.set_extension("stdout");
let mut expected_output_path_exitcode = path.clone();
expected_output_path_exitcode.set_extension("exitcode");

// Read the expected output
let output = assert.get_output();
let stderr_str = std::str::from_utf8(&output.stderr).unwrap();
let stdout_str = std::str::from_utf8(&output.stdout).unwrap();
let exit_code_str = output.status.code().unwrap().to_string();

let test_dir_str = lint_dir.to_string_lossy().to_string();
let stderr_normalized: String = stderr_str.replace(&test_dir_str, "tests/lint");
let stdout_normalized: String = stdout_str.replace(&test_dir_str, "tests/lint");

expect_file![expected_output_path_stderr].assert_eq(&stderr_normalized);
expect_file![expected_output_path_stdout].assert_eq(&stdout_normalized);
expect_file![expected_output_path_exitcode].assert_eq(&exit_code_str);
}
}
}
7 changes: 3 additions & 4 deletions crates/lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,6 @@ name = "depth_map"
harness = false

[features]
serde = ["dep:serde"]
stringify = ["dep:serde_yaml", "serde"]
python = ["pyo3"]

[dependencies]
Expand Down Expand Up @@ -64,8 +62,9 @@ nohash-hasher = "0.2.0"
rustc-hash = "2.1.0"
strum_macros = "0.26.4"
strum = "0.26.3"
serde = { version = "1.0", features = ["derive"], optional = true }
serde = { version = "1.0", features = ["derive"] }
serde_yaml = { version = "0.9.34", optional = true }
serde_json = "1"
append-only-vec = "0.1.5"

# Only activated on python
Expand All @@ -81,7 +80,7 @@ jemallocator = { version = "0.6.0", package = "tikv-jemallocator" }
pprof = { version = "0.14", features = ["flamegraph", "criterion"] }

[dev-dependencies]
sqruff-lib = { path = ".", features = ["serde"] }
sqruff-lib = { path = "." }
serde_yaml = "0.9.34"
criterion = "0.5"
expect-test = "1.5"
Expand Down
2 changes: 2 additions & 0 deletions crates/lib/src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
pub mod formatters;
pub mod github_annotation_native_formatter;
pub mod json;
pub mod json_types;
50 changes: 50 additions & 0 deletions crates/lib/src/cli/json.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
use std::sync::Mutex;

use crate::core::{config::FluffConfig, linter::linted_file::LintedFile};

use super::{
formatters::Formatter,
json_types::{Diagnostic, DiagnosticCollection, DiagnosticSeverity},
};

#[derive(Default)]
pub struct JsonFormatter {
violations: Mutex<DiagnosticCollection>,
}

impl Formatter for JsonFormatter {
fn dispatch_file_violations(&self, linted_file: &LintedFile, only_fixable: bool) {
let violations = linted_file.get_violations(only_fixable.then_some(true));
let mut lock = self.violations.lock().unwrap();
lock.entry(linted_file.path.clone()).or_default().extend(
violations
.iter()
.map(|err| Diagnostic::from(err.clone()))
.collect::<Vec<_>>(),
);
}

fn has_fail(&self) -> bool {
let lock = self.violations.lock().unwrap();
lock.values().any(|v| {
v.iter()
.any(|d| matches!(&d.severity, DiagnosticSeverity::Error))
})
}

fn completion_message(&self) {
let lock = self.violations.lock().unwrap();
let json = serde_json::to_string(&*lock).unwrap();
println!("{}", json);
}

fn dispatch_template_header(
&self,
_f_name: String,
_linter_config: FluffConfig,
_file_config: FluffConfig,
) {
}

fn dispatch_parse_header(&self, _f_name: String) {}
}
100 changes: 100 additions & 0 deletions crates/lib/src/cli/json_types.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
use std::collections::BTreeMap;

use serde::Serialize;
use sqruff_lib_core::errors::SQLBaseError;

impl From<SQLBaseError> for Diagnostic {
fn from(value: SQLBaseError) -> Self {
Diagnostic {
range: Range {
start: Position::new(value.line_no as u32, value.line_pos as u32),
end: Position::new(value.line_no as u32, value.line_pos as u32),
},
message: value.description,
severity: if value.warning {
DiagnosticSeverity::Warning
} else {
DiagnosticSeverity::Error
},
source: Some("sqruff".to_string()),
// code: todo!(),
// source: Some(value.get_source().to_string()),
// code: Some(DiagnosticCode {
// value: value.rule_code().to_string(),
// target: Uri::new("".to_string()),
// }),
// related_information: Vec::new(),
// tags: Vec::new(),
}
}
}

/// Represents a line and character position, such as the position of the cursor.
#[derive(Serialize)]
struct Position {
/// The zero-based line value.
line: u32,
/// The zero-based character value.
character: u32,
}

impl Position {
/// Creates a new `Position` instance.
fn new(line: u32, character: u32) -> Self {
Self { line, character }
}
}

/// A range represents an ordered pair of two positions. It is guaranteed that `start` is before or equal to `end`.
#[derive(Serialize)]
struct Range {
/// The start position. It is before or equal to `end`.
start: Position,
/// The end position. It is after or equal to `start`.
end: Position,
}

/// Represents a diagnostic, such as a compiler error or warning. Diagnostic objects are only valid in the scope of a file.
#[derive(Serialize)]
pub struct Diagnostic {
/// The range to which this diagnostic applies.
range: Range,
/// The human-readable message.
message: String,
/// The severity, default is {@link DiagnosticSeverity::Error error}.
pub severity: DiagnosticSeverity,
/// A human-readable string describing the source of this diagnostic, e.g. 'typescript' or 'super lint'.
source: Option<String>,
// A code or identifier for this diagnostic. Should be used for later processing, e.g. when providing {@link CodeActionContext code actions}.
// code: Option<DiagnosticCode>,
// TODO Maybe implement
// An array of related diagnostic information, e.g. when symbol-names within a scope collide all definitions can be marked via this property.
// related_information: Vec<DiagnosticRelatedInformation>,
// Additional metadata about the diagnostic.
// tags: Vec<DiagnosticTag>,
}

// /// Represents a related message and source code location for a diagnostic. This should be used to point to code locations that cause or are related to a diagnostics, e.g when duplicating a symbol in a scope.
// #[derive(Serialize)]
// struct DiagnosticCode {
// /// A code or identifier for this diagnostic.
// value: String,
// // TODO Maybe implement
// // A target URI to open with more information about the diagnostic error.
// // target: Uri,
// }

/// Represents the severity of diagnostics.
#[derive(Serialize)]
pub enum DiagnosticSeverity {
/// Something not allowed by the rules of a language or other means.
Error = 0,
/// Something suspicious but allowed.
Warning = 1,
/// Something to inform about but not a problem.
Information = 2,
/// Something to hint to a better way of doing it, like proposing a refactoring.
Hint = 3,
}

pub type DiagnosticCollection = BTreeMap<String, Vec<Diagnostic>>;
5 changes: 2 additions & 3 deletions crates/lib/src/core/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -458,9 +458,8 @@ impl ConfigLoader {
}
}

#[derive(Debug, Clone, PartialEq, Default)]
#[cfg_attr(any(test, feature = "serde"), derive(serde::Deserialize))]
#[cfg_attr(any(test, feature = "serde"), serde(untagged))]
#[derive(Debug, Clone, PartialEq, Default, serde::Deserialize)]
#[serde(untagged)]
pub enum Value {
Int(i32),
Bool(bool),
Expand Down
4 changes: 2 additions & 2 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ lint files

Default value: `human`

Possible values: `human`, `github-annotation-native`
Possible values: `human`, `github-annotation-native`, `json`



Expand All @@ -65,7 +65,7 @@ fix files

Default value: `human`

Possible values: `human`, `github-annotation-native`
Possible values: `human`, `github-annotation-native`, `json`



Expand Down

0 comments on commit a210d65

Please sign in to comment.