use crate::ast::ScriptFile;
use crate::diagnostic::Diagnostic;
use crate::token::{Token, TokenKind};
const RESERVED_WORDS: &[&str] = &[
"and",
"as",
"assert",
"await",
"break",
"breakpoint",
"class",
"class_name",
"const",
"continue",
"elif",
"else",
"enum",
"extends",
"false",
"for",
"func",
"if",
"in",
"is",
"namespace",
"not",
"null",
"or",
"pass",
"preload",
"return",
"self",
"signal",
"static",
"super",
"trait",
"true",
"var",
"void",
"while",
"yield",
];
pub fn is_reserved_word(word: &str) -> bool {
RESERVED_WORDS.contains(&word)
}
pub fn check_reserved_identifier(
tokens: &[Token],
file: &ScriptFile,
diagnostics: &mut Vec<Diagnostic>,
) {
let significant: Vec<&Token> = tokens
.iter()
.filter(|t| {
!matches!(
t.kind,
TokenKind::Newline
| TokenKind::Indent
| TokenKind::Dedent
| TokenKind::Comment(_)
| TokenKind::DocComment(_)
)
})
.collect();
let mut report = |token: &Token, role: &str| {
if is_reserved_word(&token.text) {
diagnostics.push(Diagnostic::error(
"syntax/reserved-identifier",
format!(
"'{}' is a reserved word in GDScript and cannot be used as {} name",
token.text,
with_article(role)
),
token.span,
&file.path,
));
}
};
for (index, token) in significant.iter().enumerate() {
if index > 0
&& matches!(
significant[index - 1].kind,
TokenKind::Dot | TokenKind::Dollar | TokenKind::UniqueNodeMarker | TokenKind::Slash
)
{
continue;
}
let Some(next) = significant.get(index + 1) else {
break;
};
if next.span.line != token.span.line {
continue;
}
match token.kind {
TokenKind::Func => {
if next.kind == TokenKind::LeftParen {
report_list_entries(&significant, index + 1, "parameter", &mut report);
} else {
report(next, "function");
if significant.get(index + 2).map(|t| &t.kind) == Some(&TokenKind::LeftParen) {
report_list_entries(&significant, index + 2, "parameter", &mut report);
}
}
}
TokenKind::Signal => {
report(next, "signal");
if significant.get(index + 2).map(|t| &t.kind) == Some(&TokenKind::LeftParen) {
report_list_entries(&significant, index + 2, "parameter", &mut report);
}
}
TokenKind::Enum => {
if next.kind == TokenKind::LeftBrace {
report_list_entries(&significant, index + 1, "enum member", &mut report);
} else {
report(next, "enum");
if significant.get(index + 2).map(|t| &t.kind) == Some(&TokenKind::LeftBrace) {
report_list_entries(&significant, index + 2, "enum member", &mut report);
}
}
}
TokenKind::Var => report(next, "variable"),
TokenKind::Const => report(next, "constant"),
TokenKind::Class | TokenKind::ClassName => report(next, "class"),
TokenKind::For => report(next, "loop variable"),
TokenKind::Identifier(ref name)
if name == "set"
&& next.kind == TokenKind::LeftParen
&& closing_index(&significant, index + 1)
.and_then(|close| significant.get(close + 1))
.is_some_and(|t| t.kind == TokenKind::Colon) =>
{
report_list_entries(&significant, index + 1, "parameter", &mut report);
}
_ => {}
}
}
}
fn with_article(role: &str) -> String {
let article = if role.starts_with(['a', 'e', 'i', 'o', 'u']) {
"an"
} else {
"a"
};
format!("{} {}", article, role)
}
fn closing_index(significant: &[&Token], open_index: usize) -> Option<usize> {
let mut depth = 0usize;
for (index, token) in significant.iter().enumerate().skip(open_index) {
match token.kind {
TokenKind::LeftParen | TokenKind::LeftBracket | TokenKind::LeftBrace => depth += 1,
TokenKind::RightParen | TokenKind::RightBracket | TokenKind::RightBrace => {
depth = depth.saturating_sub(1);
if depth == 0 {
return Some(index);
}
}
_ => {}
}
}
None
}
fn report_list_entries(
significant: &[&Token],
open_index: usize,
role: &str,
report: &mut impl FnMut(&Token, &str),
) {
let mut depth = 0usize;
let mut expects_name = false;
for token in &significant[open_index..] {
match token.kind {
TokenKind::LeftParen | TokenKind::LeftBracket | TokenKind::LeftBrace => {
depth += 1;
expects_name = depth == 1;
continue;
}
TokenKind::RightParen | TokenKind::RightBracket | TokenKind::RightBrace => {
depth = depth.saturating_sub(1);
if depth == 0 {
return;
}
continue;
}
TokenKind::Comma if depth == 1 => {
expects_name = true;
continue;
}
TokenKind::Ellipsis if expects_name => continue,
_ => {}
}
if expects_name {
report(token, role);
expects_name = false;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn reserved_diagnostics(source: &str) -> Vec<Diagnostic> {
let tokens = crate::lexer::Lexer::new(source).tokenize();
let file = ScriptFile {
path: "test.gd".to_string(),
members: vec![],
lines: source.split('\n').map(str::to_string).collect(),
};
let mut diagnostics = Vec::new();
check_reserved_identifier(&tokens, &file, &mut diagnostics);
diagnostics
}
fn flagged_words(source: &str) -> Vec<String> {
reserved_diagnostics(source)
.iter()
.map(|d| d.message.split('\'').nth(1).unwrap_or_default().to_string())
.collect()
}
#[test]
fn issue_32_parameter_and_function_name() {
let source = "extends Node\n\n\nfunc _prose(namespace: String) -> Dictionary:\n\treturn { \"namespace\": namespace }\n\n\nfunc trait(id: String) -> float:\n\treturn 0.0\n";
let diagnostics = reserved_diagnostics(source);
assert_eq!(diagnostics.len(), 2, "got {:?}", diagnostics);
assert_eq!(diagnostics[0].span.line, 4);
assert!(diagnostics[0].message.contains("'namespace'"));
assert!(diagnostics[0].message.contains("as a parameter name"));
assert_eq!(diagnostics[1].span.line, 8);
assert!(diagnostics[1].message.contains("'trait'"));
assert!(diagnostics[1].message.contains("as a function name"));
assert!(diagnostics
.iter()
.all(|d| d.severity == crate::diagnostic::Severity::Error));
}
#[test]
fn every_declaration_position_is_checked() {
let source = "\
class_name yield
extends Node
signal void
signal ok(namespace)
enum trait { A }
enum E { OK, self }
enum { in }
const super = 1
static var static = 2
var namespace := 3
class preload:
\tpass
func f(ok, yield = g(1, 2), ...await) -> void:
\tvar true := 1
\tfor is in []:
\t\tpass
\tvar h := func(null): return 1
";
assert_eq!(
flagged_words(source),
vec![
"yield",
"void",
"namespace",
"trait",
"self",
"in",
"super",
"static",
"namespace",
"preload",
"yield",
"await",
"true",
"is",
"null",
]
);
}
#[test]
fn multi_line_parameter_list_with_comments() {
let source = "func f(\n\ta: int, # first\n\tnamespace: String,\n) -> void:\n\tpass\n";
assert_eq!(flagged_words(source), vec!["namespace"]);
}
#[test]
fn names_godot_accepts_are_not_flagged() {
let source = "\
var match := 1
var when := 2
const PI = 3
func f(match, when, a = self, b = null) -> void:
\tvar d := { \"namespace\": 1, \"trait\": 2 }
\tvar x = obj.namespace
\tvar s := \"yield\"
\tfor i in range(3):
\t\tpass
";
assert_eq!(flagged_words(source), Vec::<String>::new());
}
#[test]
fn values_nested_in_defaults_are_not_entries() {
let source = "func f(a := [null, true], b = {\"k\": self}, c = g(1, false), d: Array[int] = [1, 2]) -> void:\n\tpass\n";
assert_eq!(flagged_words(source), Vec::<String>::new());
}
#[test]
fn keyword_member_access_is_not_a_declaration() {
assert_eq!(
flagged_words("func f(o) -> void:\n\tif o.class in [1]:\n\t\tpass\n"),
Vec::<String>::new()
);
}
#[test]
fn node_paths_ending_in_keywords_are_not_declarations() {
let source = "\
@onready var light = $signal
var x = 1
func f() -> void:
\tvar n = $UI/class
\tif n:
\t\tpass
\tvar m = %for
\tpass
\tif $Menu/enum and %var is Node:
\t\treturn
";
assert_eq!(flagged_words(source), Vec::<String>::new());
}
#[test]
fn keyword_never_pairs_with_the_next_line() {
let source = "var\nself.x = 1\n";
assert_eq!(flagged_words(source), Vec::<String>::new());
}
#[test]
fn setter_parameters_are_checked_but_set_calls_are_not() {
let source = "\
var p: int:
\tset(namespace):
\t\tpass
var q: int:
\tget:
\t\treturn 1
\tset(value):
\t\tpass
func f() -> void:
\tset(\"p\", null)
\tobj.set(\"p\", self)
";
let diagnostics = reserved_diagnostics(source);
assert_eq!(diagnostics.len(), 1, "got {:?}", diagnostics);
assert_eq!(diagnostics[0].span.line, 2);
assert!(diagnostics[0].message.contains("'namespace'"));
}
#[test]
fn roles_starting_with_a_vowel_take_an() {
let messages: Vec<String> = reserved_diagnostics("enum trait { self }\n")
.into_iter()
.map(|d| d.message)
.collect();
assert_eq!(
messages,
vec![
"'trait' is a reserved word in GDScript and cannot be used as an enum name",
"'self' is a reserved word in GDScript and cannot be used as an enum member name",
]
);
}
}