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
6 changes: 5 additions & 1 deletion crates/sqlbuild-rules/src/dsl_headers/main/format_headers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ pub fn format_headers<P: HeaderParser>(
let headers: Vec<HeaderSpan> = scan_headers(text, false);
let trimmed: Vec<char> = without_trailing_whitespace(text, &headers);
let (relocated, faults) = relocate_leading_comment(&trimmed, config, &headers, parser)?;
let relocated_headers: Vec<HeaderSpan> = scan_headers(&relocated, false);
let relocated_headers: Vec<HeaderSpan> = if relocated.as_slice() == text {
headers
} else {
scan_headers(&relocated, false)
};
let wrapped: Vec<char> =
wrap_descriptions(&relocated, &relocated_headers, config.line_width, parser)?;
Ok((wrap_audits(&wrapped, config.line_width, parser)?, faults))
Expand Down
38 changes: 27 additions & 11 deletions crates/sqlbuild-rules/src/sql_lint/_helpers/formatter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,24 @@ use polyglot_sql::tokens::{Token, TokenType};
use polyglot_sql::{ComplexityGuardOptions, Dialect, DialectType, ParseOptions};
use rayon::iter::{IntoParallelIterator, ParallelIterator};

use crate::lint_text::constants::LINT_SENTINEL_PREFIX;
use crate::sql_lint::_helpers::formatter_syntax::{cte_macro_indices, protect_syntax};
use crate::sql_lint::_helpers::printed_layout::{PrintedSql, WrapOptions, wrap_lines};
use crate::sql_lint::_helpers::token_layout::{
AuthoredSql, KeywordRecase, OracleLayout, comments_in, neutralize_comments, oracle_layout,
print_authored_tokens, tokenize, verify_token_invariant,
AuthoredSql, KeywordRecase, OracleLayout, TokenTexts, comments_in, neutralize_comments,
oracle_layout, print_authored_tokens, tokenize, verify_printed_tokens,
};
use crate::sql_lint::constants::{
CAST_TYPE_SEPARATOR_KEYWORD, CLOSE_PARENTHESIS, LINT_API_VERSION, OPEN_PARENTHESIS,
};
use crate::sql_lint::models::{FormatRequest, FormatResponse};

#[cfg(test)]
thread_local! {
/// Idempotency re-prints on this thread, for tests that assert which bodies are re-printed.
pub(crate) static RELAYOUTS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}

const NOT_IDEMPOTENT_FAILURE: &str = "native formatter output is not idempotent";
const UNSUPPORTED_SQL_FAILURE: &str =
"native formatter could not safely restore literal or cast spelling";
Expand Down Expand Up @@ -92,10 +99,11 @@ fn try_format_sql(request: FormatRequest) -> Result<FormatResponse, String> {
wrap: wrap.as_ref(),
};
let (formatted, formatted_tokens, oracle) = format_layout(&original, &layout, &parse_options)?;
if relayout((&formatted, &formatted_tokens), oracle.as_ref(), &layout)? != formatted {
let changed = formatted != original;
if changed && relayout((&formatted, &formatted_tokens), oracle.as_ref(), &layout)? != formatted
{
return Err(NOT_IDEMPOTENT_FAILURE.to_string());
}
let changed = formatted != original;
Ok(FormatResponse {
version: LINT_API_VERSION,
changed,
Expand Down Expand Up @@ -137,10 +145,13 @@ fn format_layout(
tokens: &tokens,
comments: &comments,
};
let (printed, recases) = layout.print(&authored, &oracle)?;
let (printed, recases, authored_texts) = layout.print(&authored, &oracle)?;
if printed == sql {
return Ok((printed, tokens, Some(oracle)));
}
let printed_tokens = tokenize(&printed, dialect)?;
verify_token_invariant(
(sql, &tokens),
verify_printed_tokens(
(sql, &tokens, &authored_texts),
(&printed, &printed_tokens),
dialect,
&recases,
Expand All @@ -154,6 +165,8 @@ fn relayout(
oracle: Option<&OracleLayout>,
layout: &Layout<'_>,
) -> Result<String, String> {
#[cfg(test)]
RELAYOUTS.with(|relayouts| relayouts.set(relayouts.get() + 1));
let Some(oracle) = oracle.filter(|_| !tokens.is_empty()) else {
return Ok(formatted.to_string());
};
Expand All @@ -177,19 +190,19 @@ impl Layout<'_> {
&self,
authored: &AuthoredSql<'_>,
oracle: &OracleLayout,
) -> Result<(String, BTreeMap<usize, KeywordRecase>), String> {
let (printed, recases) = print_authored_tokens(authored, oracle, self.dialect)?;
) -> Result<(String, BTreeMap<usize, KeywordRecase>, TokenTexts), String> {
let (printed, recases, texts) = print_authored_tokens(authored, oracle, self.dialect)?;
let no_widths: HashMap<String, usize> = HashMap::new();
let token_widths = self.wrap.map_or(&no_widths, |wrap| wrap.token_widths);
let Some(mut sql) = PrintedSql::parse(&printed, self.dialect, token_widths)? else {
return Ok((printed, recases));
return Ok((printed, recases, texts));
};
sql.arrange_clauses(self.wrap.map_or(usize::MAX, |wrap| wrap.line_width));
if let Some(wrap) = self.wrap {
wrap_lines(&mut sql, wrap.line_width);
}
sql.lead_operators_of_multiline_operands();
Ok((sql.render(), recases))
Ok((sql.render(), recases, texts))
}
}

Expand Down Expand Up @@ -222,6 +235,9 @@ fn layout_oracle(
}

fn layout_cte_macros(mut formatted: String, dialect: &Dialect) -> Result<String, String> {
if !formatted.contains(LINT_SENTINEL_PREFIX) {
return Ok(formatted);
}
let tokens = dialect
.tokenize(&formatted)
.map_err(|error| error.to_string())?;
Expand Down
41 changes: 30 additions & 11 deletions crates/sqlbuild-rules/src/sql_lint/_helpers/formatter_syntax.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::lint_text::constants::LINT_SENTINEL_PREFIX;
use crate::sql_lint::_helpers::engine::token_depths;
use crate::sql_lint::constants::{
CTE_MACRO_PLACEHOLDER_LITERAL, POSITION_ARGUMENT_SEPARATOR, POSTFIX_CAST_PREFIX_TOKEN_COUNT,
Expand All @@ -8,6 +9,8 @@ use std::collections::{HashMap, HashSet};

pub(super) struct ProtectedSql {
pub sql: String,
/// The lower-case prefix every sentinel name starts with.
prefix: String,
fragments: Vec<(String, String)>,
in_functions: HashSet<String>,
}
Expand All @@ -19,6 +22,14 @@ impl ProtectedSql {
.map_err(|error| error.to_string())?;
let mut positions: HashMap<String, Vec<usize>> = HashMap::new();
for (index, token) in tokens.iter().enumerate() {
let prefixed = token
.text
.as_bytes()
.get(..self.prefix.len())
.is_some_and(|start| start.eq_ignore_ascii_case(self.prefix.as_bytes()));
if !prefixed {
continue;
}
positions
.entry(token.text.to_ascii_lowercase())
.or_default()
Expand Down Expand Up @@ -101,9 +112,14 @@ pub(super) fn protect_syntax(
expressions: bool,
) -> Result<ProtectedSql, String> {
let mut prefix = "__sqlbuild_format_".to_string();
while sql.to_ascii_lowercase().contains(&prefix) {
let lowercase: String = sql.to_ascii_lowercase();
while lowercase.contains(&prefix) {
prefix.push('_');
}
let mut cast_type_ends: HashMap<usize, usize> = HashMap::new();
for (start, end) in cast_types {
let _ = cast_type_ends.entry(*start).or_insert(*end);
}
let mut ranges: Vec<(usize, usize, String)> = Vec::new();
let cte_macros = cte_macro_indices(tokens);
let depths = token_depths(tokens);
Expand All @@ -112,23 +128,23 @@ pub(super) fn protect_syntax(
let mut index = 0;
while index < tokens.len() {
let token = &tokens[index];
let sentinel = format!("{prefix}{}__", ranges.len());
let sentinel = || format!("{prefix}{}__", ranges.len());
if separators.contains(&index) {
ranges.push((token.span.start, token.span.end, ",".to_string()));
index += 1;
continue;
}
let (end, replacement) = if cte_macros.contains(&index) {
(index, format!("{sentinel} AS (SELECT 1)"))
(index, format!("{} AS (SELECT 1)", sentinel()))
} else if !expressions {
index += 1;
continue;
} else if let Some((_, end)) = cast_types.iter().find(|(start, _)| *start == index) {
(*end, sentinel)
} else if let Some(end) = cast_type_ends.get(&index) {
(*end, sentinel())
} else if token.token_type == TokenType::DColon {
(
postfix_type_end(tokens, index + 1)?,
format!("::{sentinel}"),
format!("::{}", sentinel()),
)
} else if token.token_type == TokenType::Colon
&& index > 0
Expand All @@ -144,7 +160,7 @@ pub(super) fn protect_syntax(
)
})
{
(index + 1, format!(".{sentinel}"))
(index + 1, format!(".{}", sentinel()))
} else if is_typed_literal(tokens, index)
|| (matches!(token.token_type, TokenType::Var | TokenType::Identifier)
&& tokens.get(index + 1).is_some_and(|next| {
Expand All @@ -154,12 +170,14 @@ pub(super) fn protect_syntax(
.get(index + 2)
.is_some_and(|next| next.token_type == TokenType::Arrow))
{
(index + 1, sentinel)
} else if matches!(token.text.to_ascii_uppercase().as_str(), "MOD" | "POSITION")
(index + 1, sentinel())
} else if (token.text.eq_ignore_ascii_case("MOD")
|| token.text.eq_ignore_ascii_case("POSITION"))
&& tokens
.get(index + 1)
.is_some_and(|next| next.token_type == TokenType::LParen)
{
let sentinel: String = sentinel();
if token.text.eq_ignore_ascii_case("POSITION") {
let separator = (index + 2..tokens.len())
.take_while(|&position| {
Expand All @@ -180,7 +198,7 @@ pub(super) fn protect_syntax(
token.token_type,
TokenType::String | TokenType::DollarString
) {
(index, format!("'{sentinel}'"))
(index, format!("'{}'", sentinel()))
} else {
index += 1;
continue;
Expand Down Expand Up @@ -215,6 +233,7 @@ pub(super) fn protect_syntax(
prepared.extend(&chars[cursor..]);
Ok(ProtectedSql {
sql: prepared,
prefix,
fragments,
in_functions,
})
Expand Down Expand Up @@ -263,7 +282,7 @@ pub(super) fn cte_macro_indices(tokens: &[Token]) -> HashSet<usize> {
scopes.insert(depths[index]);
} else if token.token_type == TokenType::Select {
scopes.remove(&depths[index]);
} else if token.text.starts_with("__sqb_lint_")
} else if token.text.starts_with(LINT_SENTINEL_PREFIX)
&& scopes.contains(&depths[index])
&& index > 0
&& matches!(
Expand Down
Loading
Loading