Skip to content

Commit 24d927b

Browse files
test: verify all keywords of grammar are available in completions (#701)
Adds two tests that make sure: 1. new keywords added to `grammar.js` are included in the keyword-providers 2. all keywords in keyword-providers are included in `grammar.js` I tried generating the tests from the `pg_query.proto` first, but some tokens such as "btree" and "gin" are no "official postgres keywords" — we do want to suggest them as keywords at the right spots, though. Regarding keyword generation: I think generating the keywords from either protobuf or treesitter-grammar doesn't really make sense: We have the `.starts_statement()` and `.require_prefix()` builder pattern, and I figure there'll be more of those options soon; so we'd need some manual code anyways.
1 parent baaaa56 commit 24d927b

2 files changed

Lines changed: 93 additions & 18 deletions

File tree

crates/pgls_completions/src/providers/keywords.rs

Lines changed: 93 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ pub static ALL_KEYWORDS: &[SqlKeyword] = &[
6363
SqlKeyword::new("boolean"),
6464
SqlKeyword::new("brin"),
6565
SqlKeyword::new("btree"),
66+
SqlKeyword::new("buffer_usage_limit"),
67+
SqlKeyword::new("buffers"),
6668
SqlKeyword::new("by"),
6769
SqlKeyword::new("bytea"),
6870
SqlKeyword::new("cache"),
@@ -77,7 +79,6 @@ pub static ALL_KEYWORDS: &[SqlKeyword] = &[
7779
SqlKeyword::new("check"),
7880
SqlKeyword::new("collate"),
7981
SqlKeyword::new("column"),
80-
SqlKeyword::new("columns"),
8182
SqlKeyword::new("comment").starts_statement(),
8283
SqlKeyword::new("commit").starts_statement(),
8384
SqlKeyword::new("committed"),
@@ -89,6 +90,7 @@ pub static ALL_KEYWORDS: &[SqlKeyword] = &[
8990
SqlKeyword::new("constraints"),
9091
SqlKeyword::new("copy").starts_statement(),
9192
SqlKeyword::new("cost"),
93+
SqlKeyword::new("costs"),
9294
SqlKeyword::new("create").starts_statement(),
9395
SqlKeyword::new("cross"),
9496
SqlKeyword::new("csv"),
@@ -109,7 +111,6 @@ pub static ALL_KEYWORDS: &[SqlKeyword] = &[
109111
SqlKeyword::new("delete").starts_statement(),
110112
SqlKeyword::new("delimiter"),
111113
SqlKeyword::new("desc"),
112-
SqlKeyword::new("disable_page_skipping"),
113114
SqlKeyword::new("distinct"),
114115
SqlKeyword::new("do").starts_statement(),
115116
SqlKeyword::new("double"),
@@ -149,6 +150,7 @@ pub static ALL_KEYWORDS: &[SqlKeyword] = &[
149150
SqlKeyword::new("function"),
150151
SqlKeyword::new("functions"),
151152
SqlKeyword::new("generated"),
153+
SqlKeyword::new("generic_plan"),
152154
SqlKeyword::new("gin"),
153155
SqlKeyword::new("gist"),
154156
SqlKeyword::new("grant").starts_statement(),
@@ -159,12 +161,12 @@ pub static ALL_KEYWORDS: &[SqlKeyword] = &[
159161
SqlKeyword::new("having"),
160162
SqlKeyword::new("header"),
161163
SqlKeyword::new("if"),
164+
SqlKeyword::new("ignore"),
162165
SqlKeyword::new("immediate"),
163166
SqlKeyword::new("immutable"),
164167
SqlKeyword::new("in").require_prefix(),
165168
SqlKeyword::new("increment"),
166169
SqlKeyword::new("index"),
167-
SqlKeyword::new("index_cleanup"),
168170
SqlKeyword::new("inet"),
169171
SqlKeyword::new("inherit"),
170172
SqlKeyword::new("initially"),
@@ -194,14 +196,15 @@ pub static ALL_KEYWORDS: &[SqlKeyword] = &[
194196
SqlKeyword::new("limit"),
195197
SqlKeyword::new("list"),
196198
SqlKeyword::new("local"),
197-
SqlKeyword::new("location"),
199+
SqlKeyword::new("log_verbosity"),
198200
SqlKeyword::new("logged"),
199201
SqlKeyword::new("main"),
200202
SqlKeyword::new("maintain"),
201203
SqlKeyword::new("match"),
202204
SqlKeyword::new("matched"),
203205
SqlKeyword::new("materialized"),
204206
SqlKeyword::new("maxvalue"),
207+
SqlKeyword::new("memory"),
205208
SqlKeyword::new("merge").starts_statement(),
206209
SqlKeyword::new("minvalue"),
207210
SqlKeyword::new("money"),
@@ -213,7 +216,6 @@ pub static ALL_KEYWORDS: &[SqlKeyword] = &[
213216
SqlKeyword::new("none"),
214217
SqlKeyword::new("not").require_prefix(),
215218
SqlKeyword::new("nothing"),
216-
SqlKeyword::new("nowait"),
217219
SqlKeyword::new("null").require_prefix(),
218220
SqlKeyword::new("nulls"),
219221
SqlKeyword::new("numeric"),
@@ -224,6 +226,7 @@ pub static ALL_KEYWORDS: &[SqlKeyword] = &[
224226
SqlKeyword::new("oids"),
225227
SqlKeyword::new("old"),
226228
SqlKeyword::new("on"),
229+
SqlKeyword::new("on_error"),
227230
SqlKeyword::new("only").require_prefix(),
228231
SqlKeyword::new("option"),
229232
SqlKeyword::new("or").require_prefix(),
@@ -238,19 +241,16 @@ pub static ALL_KEYWORDS: &[SqlKeyword] = &[
238241
SqlKeyword::new("owner"),
239242
SqlKeyword::new("parallel"),
240243
SqlKeyword::new("partition"),
241-
SqlKeyword::new("partitioned"),
242244
SqlKeyword::new("password"),
243245
SqlKeyword::new("permissive"),
244246
SqlKeyword::new("plain"),
245247
SqlKeyword::new("policy"),
246-
SqlKeyword::new("precedes"),
247248
SqlKeyword::new("preceding"),
248249
SqlKeyword::new("precision"),
249250
SqlKeyword::new("primary"),
250251
SqlKeyword::new("privileges"),
251252
SqlKeyword::new("procedure"),
252253
SqlKeyword::new("procedures"),
253-
SqlKeyword::new("process_toast"),
254254
SqlKeyword::new("program"),
255255
SqlKeyword::new("public"),
256256
SqlKeyword::new("quote"),
@@ -264,10 +264,10 @@ pub static ALL_KEYWORDS: &[SqlKeyword] = &[
264264
SqlKeyword::new("regnamespace"),
265265
SqlKeyword::new("regproc"),
266266
SqlKeyword::new("regtype"),
267+
SqlKeyword::new("reject_limit"),
267268
SqlKeyword::new("rename"),
268269
SqlKeyword::new("repeatable"),
269270
SqlKeyword::new("replace"),
270-
SqlKeyword::new("replication"),
271271
SqlKeyword::new("reset").starts_statement(),
272272
SqlKeyword::new("restart"),
273273
SqlKeyword::new("restrict"),
@@ -277,7 +277,6 @@ pub static ALL_KEYWORDS: &[SqlKeyword] = &[
277277
SqlKeyword::new("returning"),
278278
SqlKeyword::new("returns"),
279279
SqlKeyword::new("revoke").starts_statement(),
280-
SqlKeyword::new("rewrite"),
281280
SqlKeyword::new("right"),
282281
SqlKeyword::new("role"),
283282
SqlKeyword::new("rollback").starts_statement(),
@@ -296,7 +295,10 @@ pub static ALL_KEYWORDS: &[SqlKeyword] = &[
296295
SqlKeyword::new("session_user"),
297296
SqlKeyword::new("set").starts_statement(),
298297
SqlKeyword::new("setof"),
298+
SqlKeyword::new("settings"),
299+
SqlKeyword::new("share"),
299300
SqlKeyword::new("show").starts_statement(),
301+
SqlKeyword::new("silent"),
300302
SqlKeyword::new("similar").require_prefix(),
301303
SqlKeyword::new("skip_locked"),
302304
SqlKeyword::new("smallint"),
@@ -309,9 +311,12 @@ pub static ALL_KEYWORDS: &[SqlKeyword] = &[
309311
SqlKeyword::new("statement"),
310312
SqlKeyword::new("statistics"),
311313
SqlKeyword::new("stdin"),
314+
SqlKeyword::new("stdout"),
315+
SqlKeyword::new("stop"),
312316
SqlKeyword::new("storage"),
313317
SqlKeyword::new("stored"),
314318
SqlKeyword::new("strict"),
319+
SqlKeyword::new("summary"),
315320
SqlKeyword::new("support"),
316321
SqlKeyword::new("system"),
317322
SqlKeyword::new("table"),
@@ -325,6 +330,7 @@ pub static ALL_KEYWORDS: &[SqlKeyword] = &[
325330
SqlKeyword::new("time"),
326331
SqlKeyword::new("timestamp"),
327332
SqlKeyword::new("timestamptz"),
333+
SqlKeyword::new("timing"),
328334
SqlKeyword::new("to"),
329335
SqlKeyword::new("transaction"),
330336
SqlKeyword::new("trigger"),
@@ -353,13 +359,15 @@ pub static ALL_KEYWORDS: &[SqlKeyword] = &[
353359
SqlKeyword::new("version"),
354360
SqlKeyword::new("view"),
355361
SqlKeyword::new("volatile"),
362+
SqlKeyword::new("wal"),
356363
SqlKeyword::new("when"),
357364
SqlKeyword::new("where"),
358365
SqlKeyword::new("window"),
359366
SqlKeyword::new("with").starts_statement(),
360367
SqlKeyword::new("without"),
361368
SqlKeyword::new("write"),
362369
SqlKeyword::new("xml"),
370+
SqlKeyword::new("yaml"),
363371
SqlKeyword::new("zone"),
364372
];
365373

@@ -409,17 +417,92 @@ pub fn complete_keywords<'a>(
409417

410418
#[cfg(test)]
411419
mod tests {
420+
use std::collections::HashSet;
421+
412422
use pgls_test_utils::QueryWithCursorPosition;
423+
use regex::Regex;
413424
use sqlx::PgPool;
425+
use tree_sitter::Language;
414426

415427
use crate::{
416428
CompletionItemKind,
429+
providers::keywords::ALL_KEYWORDS,
417430
test_helper::{
418431
CompletionAssertion, TestCompletionsCase, TestCompletionsSuite,
419432
assert_complete_results, assert_no_complete_results,
420433
},
421434
};
422435

436+
#[test]
437+
fn has_all_keywords_from_treesitter_grammar() {
438+
let expected_keywords = keywords_from_treesitter_grammar();
439+
let actual_keywords = all_keywords_set();
440+
441+
let mut missing_keywords = expected_keywords
442+
.difference(&actual_keywords)
443+
.cloned()
444+
.collect::<Vec<_>>();
445+
missing_keywords.sort_unstable();
446+
447+
assert!(
448+
missing_keywords.is_empty(),
449+
"Found {} keyword(s) from tree-sitter grammar that are missing from ALL_KEYWORDS.\n\
450+
Add missing entries to ALL_KEYWORDS in crates/pgls_completions/src/providers/keywords.rs.\n\
451+
Missing keywords:\n{}",
452+
missing_keywords.len(),
453+
missing_keywords.join("\n")
454+
);
455+
}
456+
457+
#[test]
458+
fn has_no_extra_keywords_outside_treesitter_grammar() {
459+
let expected_keywords = keywords_from_treesitter_grammar();
460+
let actual_keywords = all_keywords_set();
461+
462+
let mut extra_keywords = actual_keywords
463+
.difference(&expected_keywords)
464+
.cloned()
465+
.collect::<Vec<_>>();
466+
extra_keywords.sort_unstable();
467+
468+
assert!(
469+
extra_keywords.is_empty(),
470+
"Found {} keyword(s) in ALL_KEYWORDS that are not tree-sitter grammar keywords.\n\
471+
Remove these from ALL_KEYWORDS or add matching keyword_* symbols to grammar.js.\n\
472+
Extra keywords:\n{}",
473+
extra_keywords.len(),
474+
extra_keywords.join("\n")
475+
);
476+
}
477+
478+
fn all_keywords_set() -> HashSet<String> {
479+
ALL_KEYWORDS
480+
.iter()
481+
.map(|keyword| keyword.name.to_string())
482+
.collect::<HashSet<_>>()
483+
}
484+
485+
fn keywords_from_treesitter_grammar() -> HashSet<String> {
486+
let language: Language = pgls_treesitter_grammar::LANGUAGE.into();
487+
// Tree-sitter generates auxiliary symbol names like `keyword_int_token2` for keyword
488+
// rules that use `choice(...)`. Strip the suffix to compare canonical keyword names.
489+
let generated_token_suffix =
490+
Regex::new(r"_token\d+$").expect("valid regex for generated token suffix");
491+
let mut keywords = HashSet::new();
492+
493+
for id in 0..language.node_kind_count() {
494+
let Some(kind) = language.node_kind_for_id(id as u16) else {
495+
continue;
496+
};
497+
if let Some(keyword) = kind.strip_prefix("keyword_") {
498+
let keyword = generated_token_suffix.replace(keyword, "").to_string();
499+
keywords.insert(keyword);
500+
}
501+
}
502+
503+
keywords
504+
}
505+
423506
#[sqlx::test]
424507
async fn completes_stmt_start_keywords(pool: PgPool) {
425508
let setup = r#"

crates/pgls_treesitter_grammar/grammar.js

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,6 @@ module.exports = grammar({
145145
keyword_tables: (_) => make_keyword("tables"),
146146
keyword_view: (_) => make_keyword("view"),
147147
keyword_column: (_) => make_keyword("column"),
148-
keyword_columns: (_) => make_keyword("columns"),
149148
keyword_materialized: (_) => make_keyword("materialized"),
150149
keyword_tablespace: (_) => make_keyword("tablespace"),
151150
keyword_sequence: (_) => make_keyword("sequence"),
@@ -263,7 +262,6 @@ module.exports = grammar({
263262
keyword_check: (_) => make_keyword("check"),
264263
keyword_option: (_) => make_keyword("option"),
265264
keyword_vacuum: (_) => make_keyword("vacuum"),
266-
keyword_nowait: (_) => make_keyword("nowait"),
267265
keyword_attribute: (_) => make_keyword("attribute"),
268266
keyword_authorization: (_) => make_keyword("authorization"),
269267
keyword_action: (_) => make_keyword("action"),
@@ -300,7 +298,6 @@ module.exports = grammar({
300298
keyword_timing: (_) => make_keyword("timing"),
301299
keyword_summary: (_) => make_keyword("summary"),
302300
keyword_memory: (_) => make_keyword("memory"),
303-
keyword_serialize: (_) => make_keyword("serialize"),
304301
keyword_skip_locked: (_) => make_keyword("skip_locked"),
305302
keyword_buffer_usage_limit: (_) => make_keyword("buffer_usage_limit"),
306303

@@ -373,7 +370,6 @@ module.exports = grammar({
373370
keyword_constraints: (_) => make_keyword("constraints"),
374371
keyword_snapshot: (_) => make_keyword("snapshot"),
375372
keyword_characteristics: (_) => make_keyword("characteristics"),
376-
keyword_precedes: (_) => make_keyword("precedes"),
377373
keyword_each: (_) => make_keyword("each"),
378374
keyword_instead: (_) => make_keyword("instead"),
379375
keyword_of: (_) => make_keyword("of"),
@@ -388,11 +384,7 @@ module.exports = grammar({
388384

389385
keyword_external: (_) => make_keyword("external"),
390386
keyword_stored: (_) => make_keyword("stored"),
391-
keyword_replication: (_) => make_keyword("replication"),
392387
keyword_statistics: (_) => make_keyword("statistics"),
393-
keyword_rewrite: (_) => make_keyword("rewrite"),
394-
keyword_location: (_) => make_keyword("location"),
395-
keyword_partitioned: (_) => make_keyword("partitioned"),
396388
keyword_comment: (_) => make_keyword("comment"),
397389
keyword_format: (_) => make_keyword("format"),
398390
keyword_delimiter: (_) => make_keyword("delimiter"),

0 commit comments

Comments
 (0)