flowscope_core/linter/rules/
al_006.rs1use crate::linter::config::LintConfig;
7use crate::linter::rule::{LintContext, LintRule};
8use crate::types::{issue_codes, Issue};
9use sqlparser::ast::{Select, Statement, TableFactor, TableWithJoins};
10
11use super::semantic_helpers::{table_factor_alias_name, visit_selects_in_statement};
12
13const DEFAULT_MIN_ALIAS_LENGTH: usize = 0;
14const DEFAULT_MAX_ALIAS_LENGTH: Option<usize> = None;
15
16pub struct AliasingLength {
17 min_alias_length: usize,
18 max_alias_length: Option<usize>,
19}
20
21impl AliasingLength {
22 pub fn from_config(config: &LintConfig) -> Self {
23 Self {
24 min_alias_length: config
25 .rule_option_usize(issue_codes::LINT_AL_006, "min_alias_length")
26 .unwrap_or(DEFAULT_MIN_ALIAS_LENGTH),
27 max_alias_length: config
28 .rule_option_usize(issue_codes::LINT_AL_006, "max_alias_length")
29 .or(DEFAULT_MAX_ALIAS_LENGTH),
30 }
31 }
32}
33
34impl Default for AliasingLength {
35 fn default() -> Self {
36 Self {
37 min_alias_length: DEFAULT_MIN_ALIAS_LENGTH,
38 max_alias_length: DEFAULT_MAX_ALIAS_LENGTH,
39 }
40 }
41}
42
43impl LintRule for AliasingLength {
44 fn code(&self) -> &'static str {
45 issue_codes::LINT_AL_006
46 }
47
48 fn name(&self) -> &'static str {
49 "Alias length"
50 }
51
52 fn description(&self) -> &'static str {
53 "Enforce table alias lengths in from clauses and join conditions."
54 }
55
56 fn check(&self, statement: &Statement, ctx: &LintContext) -> Vec<Issue> {
57 let mut violations = 0usize;
58
59 visit_selects_in_statement(statement, &mut |select| {
60 violations += alias_length_violation_count_in_select(
61 select,
62 self.min_alias_length,
63 self.max_alias_length,
64 );
65 });
66
67 (0..violations)
68 .map(|_| {
69 Issue::info(
70 issue_codes::LINT_AL_006,
71 "Alias length violates configured bounds.",
72 )
73 .with_statement(ctx.statement_index)
74 })
75 .collect()
76 }
77}
78
79fn alias_length_violation_count_in_select(
80 select: &Select,
81 min_alias_length: usize,
82 max_alias_length: Option<usize>,
83) -> usize {
84 let mut count = 0usize;
85
86 for table in &select.from {
87 count += alias_length_violation_count_in_table_with_joins(
88 table,
89 min_alias_length,
90 max_alias_length,
91 );
92 }
93
94 count
95}
96
97fn alias_length_violation_count_in_table_with_joins(
98 table_with_joins: &TableWithJoins,
99 min_alias_length: usize,
100 max_alias_length: Option<usize>,
101) -> usize {
102 let mut count = alias_length_violation_count_in_table_factor(
103 &table_with_joins.relation,
104 min_alias_length,
105 max_alias_length,
106 );
107 for join in &table_with_joins.joins {
108 count += alias_length_violation_count_in_table_factor(
109 &join.relation,
110 min_alias_length,
111 max_alias_length,
112 );
113 }
114 count
115}
116
117fn alias_length_violation_count_in_table_factor(
118 table_factor: &TableFactor,
119 min_alias_length: usize,
120 max_alias_length: Option<usize>,
121) -> usize {
122 let mut count = 0usize;
123
124 if table_factor_alias_name(table_factor)
125 .is_some_and(|alias| alias_length_violates(alias, min_alias_length, max_alias_length))
126 {
127 count += 1;
128 }
129
130 match table_factor {
131 TableFactor::NestedJoin {
132 table_with_joins, ..
133 } => {
134 count += alias_length_violation_count_in_table_with_joins(
135 table_with_joins,
136 min_alias_length,
137 max_alias_length,
138 )
139 }
140 TableFactor::Pivot { table, .. }
141 | TableFactor::Unpivot { table, .. }
142 | TableFactor::MatchRecognize { table, .. } => {
143 count += alias_length_violation_count_in_table_factor(
144 table,
145 min_alias_length,
146 max_alias_length,
147 )
148 }
149 _ => {}
150 }
151
152 count
153}
154
155fn alias_length_violates(
156 alias: &str,
157 min_alias_length: usize,
158 max_alias_length: Option<usize>,
159) -> bool {
160 let length = alias.len();
161 if length < min_alias_length {
162 return true;
163 }
164
165 if let Some(max_alias_length) = max_alias_length {
166 return length > max_alias_length;
167 }
168
169 false
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175 use crate::parser::parse_sql;
176
177 fn run(sql: &str) -> Vec<Issue> {
178 let statements = parse_sql(sql).expect("parse");
179 let rule = AliasingLength::default();
180 statements
181 .iter()
182 .enumerate()
183 .flat_map(|(index, statement)| {
184 rule.check(
185 statement,
186 &LintContext {
187 sql,
188 statement_range: 0..sql.len(),
189 statement_index: index,
190 },
191 )
192 })
193 .collect()
194 }
195
196 #[test]
197 fn default_does_not_flag_overlong_table_alias() {
198 let issues = run("SELECT * FROM users this_alias_name_is_longer_than_thirty_chars");
199 assert!(issues.is_empty());
200 }
201
202 #[test]
203 fn does_not_flag_short_alias() {
204 let issues = run("SELECT * FROM users u");
205 assert!(issues.is_empty());
206 }
207
208 #[test]
209 fn does_not_flag_alias_at_length_limit() {
210 let issues = run("SELECT * FROM users aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
211 assert!(issues.is_empty());
212 }
213
214 #[test]
215 fn default_does_not_flag_nested_select_alias() {
216 let issues = run(
217 "SELECT * FROM (SELECT * FROM users this_alias_name_is_longer_than_thirty_chars) sub",
218 );
219 assert!(issues.is_empty());
220 }
221
222 #[test]
223 fn flags_overlong_table_alias_with_max_length_config() {
224 let statements =
225 parse_sql("SELECT * FROM users this_alias_name_is_longer_than_thirty_chars")
226 .expect("parse");
227 let config = LintConfig {
228 enabled: true,
229 disabled_rules: vec![],
230 rule_configs: std::collections::BTreeMap::from([(
231 "aliasing.length".to_string(),
232 serde_json::json!({"max_alias_length": 30}),
233 )]),
234 };
235 let rule = AliasingLength::from_config(&config);
236
237 let issues = statements
238 .iter()
239 .enumerate()
240 .flat_map(|(index, statement)| {
241 rule.check(
242 statement,
243 &LintContext {
244 sql: "SELECT * FROM users this_alias_name_is_longer_than_thirty_chars",
245 statement_range: 0
246 .."SELECT * FROM users this_alias_name_is_longer_than_thirty_chars"
247 .len(),
248 statement_index: index,
249 },
250 )
251 })
252 .collect::<Vec<_>>();
253
254 assert_eq!(issues.len(), 1);
255 assert_eq!(issues[0].code, issue_codes::LINT_AL_006);
256 }
257
258 #[test]
259 fn applies_max_alias_length_from_config() {
260 let statements = parse_sql("SELECT * FROM users eleven_chars").expect("parse");
261 let config = LintConfig {
262 enabled: true,
263 disabled_rules: vec![],
264 rule_configs: std::collections::BTreeMap::from([(
265 "LINT_AL_006".to_string(),
266 serde_json::json!({"max_alias_length": 10}),
267 )]),
268 };
269 let rule = AliasingLength::from_config(&config);
270
271 let issues = statements
272 .iter()
273 .enumerate()
274 .flat_map(|(index, statement)| {
275 rule.check(
276 statement,
277 &LintContext {
278 sql: "SELECT * FROM users eleven_chars",
279 statement_range: 0.."SELECT * FROM users eleven_chars".len(),
280 statement_index: index,
281 },
282 )
283 })
284 .collect::<Vec<_>>();
285
286 assert_eq!(issues.len(), 1);
287 }
288
289 #[test]
290 fn applies_min_alias_length_from_config() {
291 let statements = parse_sql("SELECT * FROM users a").expect("parse");
292 let config = LintConfig {
293 enabled: true,
294 disabled_rules: vec![],
295 rule_configs: std::collections::BTreeMap::from([(
296 "aliasing.length".to_string(),
297 serde_json::json!({"min_alias_length": 2}),
298 )]),
299 };
300 let rule = AliasingLength::from_config(&config);
301
302 let issues = statements
303 .iter()
304 .enumerate()
305 .flat_map(|(index, statement)| {
306 rule.check(
307 statement,
308 &LintContext {
309 sql: "SELECT * FROM users a",
310 statement_range: 0.."SELECT * FROM users a".len(),
311 statement_index: index,
312 },
313 )
314 })
315 .collect::<Vec<_>>();
316
317 assert_eq!(issues.len(), 1);
318 }
319}