Skip to content

Commit 90d75c5

Browse files
committed
feat: no-wildcard
1 parent 849162d commit 90d75c5

File tree

3 files changed

+192
-1
lines changed

3 files changed

+192
-1
lines changed

crates/uroborosql-lint/src/linter.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use crate::{
22
context::LintContext,
33
diagnostic::{Diagnostic, Severity},
44
rule::Rule,
5-
rules::{NoDistinct, NoNotIn, NoUnionDistinct, TooLargeInList},
5+
rules::{NoDistinct, NoNotIn, NoUnionDistinct, NoWildcardProjection, TooLargeInList},
66
tree::collect_preorder,
77
};
88
use postgresql_cst_parser::tree_sitter;
@@ -107,6 +107,7 @@ fn default_rules() -> Vec<Box<dyn Rule>> {
107107
Box::new(NoDistinct),
108108
Box::new(NoNotIn),
109109
Box::new(NoUnionDistinct),
110+
Box::new(NoWildcardProjection),
110111
Box::new(TooLargeInList),
111112
]
112113
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
mod no_distinct;
22
mod no_not_in;
33
mod no_union_distinct;
4+
mod no_wildcard_projection;
45
mod too_large_in_list;
56

67
pub use no_distinct::NoDistinct;
78
pub use no_not_in::NoNotIn;
89
pub use no_union_distinct::NoUnionDistinct;
10+
pub use no_wildcard_projection::NoWildcardProjection;
911
pub use too_large_in_list::TooLargeInList;
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
use crate::{
2+
context::LintContext,
3+
diagnostic::{Diagnostic, Severity},
4+
rule::Rule,
5+
};
6+
use postgresql_cst_parser::{
7+
syntax_kind::SyntaxKind,
8+
tree_sitter::{Node, Range},
9+
};
10+
11+
/// Detects wildcard projections. (e.g. `SELECT *`, `SELECT u.*`, `RETURNING *`)
12+
/// Rule source: https://future-architect.github.io/coding-standards/documents/forSQL/SQL%E3%82%B3%E3%83%BC%E3%83%87%E3%82%A3%E3%83%B3%E3%82%B0%E8%A6%8F%E7%B4%84%EF%BC%88PostgreSQL%EF%BC%89.html
13+
pub struct NoWildcardProjection;
14+
15+
impl Rule for NoWildcardProjection {
16+
fn name(&self) -> &'static str {
17+
"no-wildcard-projection"
18+
}
19+
20+
fn default_severity(&self) -> Severity {
21+
Severity::Warning
22+
}
23+
24+
fn target_kinds(&self) -> &'static [SyntaxKind] {
25+
&[SyntaxKind::target_el]
26+
}
27+
28+
fn run_on_node<'tree>(&self, node: &Node<'tree>, ctx: &mut LintContext, severity: Severity) {
29+
let Some(range) = detect_wildcard(node) else {
30+
return;
31+
};
32+
33+
let diagnostic = Diagnostic::new(
34+
self.name(),
35+
severity,
36+
"Wildcard projections are not allowed; list the columns explicitly.",
37+
&range,
38+
);
39+
ctx.report(diagnostic);
40+
}
41+
}
42+
43+
fn detect_wildcard(target_el_node: &Node<'_>) -> Option<Range> {
44+
assert_eq!(target_el_node.kind(), SyntaxKind::target_el);
45+
// target_el:
46+
// - '*'
47+
// ^^^ Wildcard
48+
// - a_expr AS ColLabel
49+
// - a_expr BareColLabel
50+
// - a_expr
51+
// ^^^^^^ columnref が含まれる場合、 wildcard が出現する可能性がある
52+
//
53+
// source: https://github.com/postgres/postgres/blob/65f4976189b6cbe9aa93fc5f4b1eb7a2040b6301/src/backend/parser/gram.y#L17364-L17401
54+
55+
let mut cursor = target_el_node.walk();
56+
cursor.goto_first_child();
57+
58+
match cursor.node().kind() {
59+
SyntaxKind::Star => Some(cursor.node().range()),
60+
SyntaxKind::a_expr => {
61+
let a_expr = cursor.node();
62+
let columnref = get_columnref_from_a_expr(&a_expr)?;
63+
64+
// columnref:
65+
// - ColId
66+
// - ColId indirection
67+
// - e.g.: `a.field`, `a.field[1]`
68+
//
69+
// source: https://github.com/postgres/postgres/blob/65f4976189b6cbe9aa93fc5f4b1eb7a2040b6301/src/backend/parser/gram.y#L17041-L17049
70+
71+
let indirection = columnref
72+
.children()
73+
.iter()
74+
.find(|child| child.kind() == SyntaxKind::indirection)
75+
.cloned()?;
76+
77+
// indirection: list of indirection_el
78+
let last_indirection_el = indirection.children().last()?.clone();
79+
80+
// indirection_el:
81+
// - '.' attr_name
82+
// - '.' '*'
83+
// - '[' a_expr ']'
84+
// - '[' opt_slice_bound ':' opt_slice_bound ']'
85+
//
86+
// source: https://github.com/postgres/postgres/blob/65f4976189b6cbe9aa93fc5f4b1eb7a2040b6301/src/backend/parser/gram.y#L17051-L17078
87+
let last_of_indirection_el = last_indirection_el.children().last().cloned()?;
88+
89+
// possible: attr_name, '*', ']'
90+
if last_of_indirection_el.kind() == SyntaxKind::Star {
91+
Some(last_indirection_el.range())
92+
} else {
93+
None
94+
}
95+
}
96+
_ => None,
97+
}
98+
}
99+
100+
/// Retrieves the `columnref` node from an `a_expr` node if it exists.
101+
fn get_columnref_from_a_expr<'a>(a_expr: &'a Node<'a>) -> Option<Node<'a>> {
102+
// current: a_expr
103+
//
104+
// possible structure:
105+
// - a_expr
106+
// - c_expr
107+
// - columnref
108+
109+
let mut cursor = a_expr.walk();
110+
cursor.goto_first_child();
111+
112+
match cursor.node().kind() {
113+
SyntaxKind::c_expr => {
114+
cursor.goto_first_child();
115+
if cursor.node().kind() == SyntaxKind::columnref {
116+
return Some(cursor.node());
117+
}
118+
119+
None
120+
}
121+
_ => None,
122+
}
123+
}
124+
125+
#[cfg(test)]
126+
mod tests {
127+
use super::*;
128+
use crate::{linter::tests::run_with_rules, SqlSpan};
129+
130+
fn run(sql: &str) -> Vec<Diagnostic> {
131+
run_with_rules(sql, vec![Box::new(NoWildcardProjection)])
132+
}
133+
134+
#[test]
135+
fn detects_select_star() {
136+
let sql = "SELECT * FROM users;";
137+
let diagnostics = run(sql);
138+
139+
let diagnostic = diagnostics
140+
.iter()
141+
.find(|diag| diag.rule_id == "no-wildcard-projection")
142+
.expect("should detect SELECT *");
143+
144+
let SqlSpan { start, end } = diagnostic.span;
145+
assert_eq!(&sql[start.byte..end.byte], "*");
146+
}
147+
148+
#[test]
149+
fn detects_returning_star() {
150+
let sql = "INSERT INTO users(id) VALUES (1) RETURNING *;";
151+
let diagnostics = run(sql);
152+
153+
let diagnostic = diagnostics
154+
.iter()
155+
.find(|diag| diag.rule_id == "no-wildcard-projection")
156+
.expect("should detect RETURNING *");
157+
158+
let SqlSpan { start, end } = diagnostic.span;
159+
assert_eq!(&sql[start.byte..end.byte], "*");
160+
}
161+
162+
#[test]
163+
fn detects_table_star() {
164+
let sql = "SELECT u.* FROM users u;";
165+
let diagnostics = run(sql);
166+
let diagnostic = diagnostics
167+
.iter()
168+
.find(|diag| diag.rule_id == "no-wildcard-projection")
169+
.expect("should detect .*");
170+
171+
let SqlSpan { start, end } = diagnostic.span;
172+
assert_eq!(&sql[start.byte..end.byte], ".*");
173+
}
174+
175+
#[test]
176+
fn allows_explicit_columns() {
177+
let sql = "SELECT id, name FROM users;";
178+
let diagnostics = run(sql);
179+
assert!(diagnostics.is_empty());
180+
}
181+
182+
#[test]
183+
fn allows_count_star() {
184+
let sql = "SELECT count(*) FROM users;";
185+
let diagnostics = run(sql);
186+
assert!(diagnostics.is_empty(), "count(*) should be allowed");
187+
}
188+
}

0 commit comments

Comments
 (0)