Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ needless_return = "allow"
doc_markdown = "deny"
manual_let_else = "deny"
explicit_iter_loop = "deny"
too_many_arguments = "allow"

[profile.dev]
debug = 0
Expand Down
9 changes: 2 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,6 @@ FLAGS:
-h, --help
Prints help information

--list-rules
List all available rules

-V, --version
Prints version information

Expand All @@ -88,8 +85,8 @@ OPTIONS:
-c, --config <config-path>
Path to the squawk config file (.squawk.toml)

--dump-ast <ast-format>
Output AST in JSON [possible values: Raw, Parsed, Debug]
--debug <format>
Output debug info [possible values: Lex, Parse]

--exclude-path <excluded-path>...
Paths to exclude
Expand All @@ -102,8 +99,6 @@ OPTIONS:
Exclude specific warnings

For example: --exclude=require-concurrent-index-creation,ban-drop-database
--explain <rule>
Provide documentation on the given rule

--pg-version <pg-version>
Specify postgres version
Expand Down
20 changes: 15 additions & 5 deletions crates/cli/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
[package]
name = "squawk"
version = "1.6.1"
authors = ["Steve Dignam <steve@dignam.xyz>"]
edition = "2018"
license = "GPL-3.0"
default-run = "squawk"

authors.workspace = true
edition.workspace = true
license.workspace = true
description = "Linter for Postgresql focused on database migrations."
repository = "https://github.com/sbdchd/squawk"
documentation = "https://github.com/sbdchd/squawk/blob/master/README.md"
Expand All @@ -23,11 +25,19 @@ atty.workspace = true
base64.workspace = true
simplelog.workspace = true
log.workspace = true
squawk-parser.workspace = true
squawk-linter.workspace = true
enum-iterator.workspace = true
squawk_syntax.workspace = true
squawk_linter.workspace = true
squawk_lexer.workspace = true
squawk-github.workspace = true
toml.workspace = true
glob.workspace = true
anyhow.workspace = true
annotate-snippets.workspace = true
line-index.workspace = true

[dev-dependencies]
insta.workspace = true

[lints]
workspace = true
45 changes: 8 additions & 37 deletions crates/cli/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,41 +1,11 @@
use anyhow::{Context, Result};
use log::info;
use serde::Deserialize;
use squawk_linter::{versions::Version, violations::RuleViolationKind};
use std::{env, io, path::Path, path::PathBuf};
use squawk_linter::{Rule, Version};
use std::{env, path::Path, path::PathBuf};

const FILE_NAME: &str = ".squawk.toml";

#[derive(Debug)]
pub enum ConfigError {
LookupError(io::Error),
ReadError(io::Error),
ParseError(toml::de::Error),
}

impl std::convert::From<io::Error> for ConfigError {
fn from(e: io::Error) -> Self {
Self::ReadError(e)
}
}

impl std::convert::From<toml::de::Error> for ConfigError {
fn from(e: toml::de::Error) -> Self {
Self::ParseError(e)
}
}

impl std::fmt::Display for ConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match *self {
Self::LookupError(ref err) => {
write!(f, "Error when finding configuration file: {err}")
}
Self::ReadError(ref err) => write!(f, "Failed to read configuration file: {err}"),
Self::ParseError(ref err) => write!(f, "Failed to parse configuration file: {err}"),
}
}
}

#[derive(Debug, Default, Deserialize)]
pub struct UploadToGitHubConfig {
#[serde(default)]
Expand All @@ -47,7 +17,7 @@ pub struct Config {
#[serde(default)]
pub excluded_paths: Vec<String>,
#[serde(default)]
pub excluded_rules: Vec<RuleViolationKind>,
pub excluded_rules: Vec<Rule>,
#[serde(default)]
pub pg_version: Option<Version>,
#[serde(default)]
Expand All @@ -57,7 +27,7 @@ pub struct Config {
}

impl Config {
pub fn parse(custom_path: Option<PathBuf>) -> Result<Option<Self>, ConfigError> {
pub fn parse(custom_path: Option<PathBuf>) -> Result<Option<Self>> {
let path = if let Some(path) = custom_path {
Some(path)
} else {
Expand Down Expand Up @@ -90,8 +60,9 @@ fn recurse_directory(directory: &Path, file_name: &str) -> Result<Option<PathBuf
}
}

fn find_by_traversing_back() -> Result<Option<PathBuf>, ConfigError> {
recurse_directory(&env::current_dir()?, FILE_NAME).map_err(ConfigError::LookupError)
fn find_by_traversing_back() -> Result<Option<PathBuf>> {
recurse_directory(&env::current_dir()?, FILE_NAME)
.context("Error when finding configuration file")
}

#[cfg(test)]
Expand Down
98 changes: 98 additions & 0 deletions crates/cli/src/debug.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
use std::{io, path::PathBuf};

use annotate_snippets::{Level, Message, Renderer, Snippet};
use anyhow::Result;
use squawk_syntax::syntax_error::SyntaxError;

use crate::{
file::{sql_from_path, sql_from_stdin},
DebugOption,
};

pub(crate) fn debug<W: io::Write>(
f: &mut W,
paths: &[PathBuf],
read_stdin: bool,
dump_ast: &DebugOption,
verbose: bool,
) -> Result<()> {
let process_dump_ast = |sql: &str, filename: &str, f: &mut W| -> Result<()> {
match dump_ast {
DebugOption::Lex => {
let tokens = squawk_lexer::tokenize(sql);
let mut start = 0;
for token in tokens {
if verbose {
let content = &sql[start as usize..(start + token.len) as usize];
start += token.len;
writeln!(f, "{:?} @ {:?}", content, token.kind)?;
} else {
writeln!(f, "{:?}", token)?;
}
}
}
DebugOption::Parse => {
let parse = squawk_syntax::SourceFile::parse(sql);
if verbose {
writeln!(f, "{}\n---", parse.syntax_node())?;
}
writeln!(f, "{:#?}", parse.syntax_node())?;
let errors = parse.errors();
if !errors.is_empty() {
let mut snap = "---".to_string();
for syntax_error in &errors {
let range = syntax_error.range();
let text = syntax_error.message();
// split into there own lines so that we can just grep
// for error without hitting this part
snap += "\n";
snap += "ERROR";
if range.start() == range.end() {
snap += &format!("@{:?} {:?}", range.start(), text);
} else {
snap += &format!("@{:?}:{:?} {:?}", range.start(), range.end(), text);
}
}
writeln!(f, "{}", snap)?;
let renderer = Renderer::styled();
render_syntax_errors(&errors, filename, sql, |message| {
writeln!(f, "{}", renderer.render(message))?;
Ok(())
})?;
}
}
}
Ok(())
};
if read_stdin {
let sql = sql_from_stdin()?;
process_dump_ast(&sql, "stdin", f)?;
return Ok(());
}

for path in paths {
let sql = sql_from_path(path)?;
process_dump_ast(&sql, &path.to_string_lossy(), f)?;
}
Ok(())
}

fn render_syntax_errors(
errors: &[SyntaxError],
filename: &str,
sql: &str,
mut render: impl FnMut(Message<'_>) -> Result<()>,
) -> Result<()> {
for err in errors {
let text = err.message();
let span = err.range().into();
let message = Level::Warning.title(text).id("syntax-error").snippet(
Snippet::source(sql)
.origin(filename)
.fold(true)
.annotation(Level::Error.span(span)),
);
render(message)?;
}
Ok(())
}
22 changes: 22 additions & 0 deletions crates/cli/src/file.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
use std::{
fs::File,
io::{self, Read},
path::PathBuf,
};

use anyhow::Result;

pub(crate) fn sql_from_stdin() -> Result<String> {
let mut buffer = String::new();
let stdin = io::stdin();
let mut handle = stdin.lock();
handle.read_to_string(&mut buffer)?;
Ok(buffer)
}

pub(crate) fn sql_from_path(path: &PathBuf) -> Result<String> {
let mut file = File::open(path)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}
36 changes: 2 additions & 34 deletions crates/cli/src/file_finding.rs
Original file line number Diff line number Diff line change
@@ -1,42 +1,10 @@
use anyhow::Result;
use std::path::PathBuf;

use log::info;

#[derive(Debug)]
pub enum FindFilesError {
PatternError(glob::PatternError),
GlobError(glob::GlobError),
}

impl std::fmt::Display for FindFilesError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match *self {
Self::PatternError(ref err) => {
write!(f, "Failed to build pattern: {err}")
}
Self::GlobError(ref err) => {
write!(f, "Failed to read file: {err}")
}
}
}
}

impl std::convert::From<glob::PatternError> for FindFilesError {
fn from(e: glob::PatternError) -> Self {
Self::PatternError(e)
}
}
impl std::convert::From<glob::GlobError> for FindFilesError {
fn from(e: glob::GlobError) -> Self {
Self::GlobError(e)
}
}

/// Given a list of patterns or paths, along with exclusion patterns, find matching files.
pub fn find_paths(
path_patterns: &[String],
exclude_patterns: &[String],
) -> Result<Vec<PathBuf>, FindFilesError> {
pub fn find_paths(path_patterns: &[String], exclude_patterns: &[String]) -> Result<Vec<PathBuf>> {
let mut matched_paths = vec![];
let exclude_paths: Vec<_> = exclude_patterns
.iter()
Expand Down
Loading
Loading