Added system for GDScript warnings

- Count and panel per script.
- Ability to disable warnings per script using special comments.
- Ability to disable warnings globally using Project Settings.
- Option to treat enabled warnings as errors.
This commit is contained in:
George Marques 2018-07-01 13:17:40 -03:00
parent 767fb2fa0b
commit eb48119821
No known key found for this signature in database
GPG key ID: 046BD46A3201E43D
19 changed files with 810 additions and 46 deletions

View file

@ -207,13 +207,20 @@ public:
virtual void finish() = 0;
/* EDITOR FUNCTIONS */
struct Warning {
int line;
int code;
String string_code;
String message;
};
virtual void get_reserved_words(List<String> *p_words) const = 0;
virtual void get_comment_delimiters(List<String> *p_delimiters) const = 0;
virtual void get_string_delimiters(List<String> *p_delimiters) const = 0;
virtual Ref<Script> get_template(const String &p_class_name, const String &p_base_class_name) const = 0;
virtual void make_template(const String &p_class_name, const String &p_base_class_name, Ref<Script> &p_script) {}
virtual bool is_using_templates() { return false; }
virtual bool validate(const String &p_script, int &r_line_error, int &r_col_error, String &r_test_error, const String &p_path = "", List<String> *r_functions = NULL, Set<int> *r_safe_lines = NULL) const = 0;
virtual bool validate(const String &p_script, int &r_line_error, int &r_col_error, String &r_test_error, const String &p_path = "", List<String> *r_functions = NULL, List<Warning> *r_warnings = NULL, Set<int> *r_safe_lines = NULL) const = 0;
virtual String validate_path(const String &p_path) const { return ""; }
virtual Script *create_script() const = 0;
virtual bool has_named_classes() const = 0;

View file

@ -1246,6 +1246,29 @@ CodeTextEditor::CodeTextEditor() {
status_bar->add_child(memnew(Label)); //to keep the height if the other labels are not visible
warning_label = memnew(Label);
status_bar->add_child(warning_label);
warning_label->set_align(Label::ALIGN_RIGHT);
warning_label->set_valign(Label::VALIGN_CENTER);
warning_label->set_v_size_flags(SIZE_FILL);
warning_label->set_default_cursor_shape(CURSOR_POINTING_HAND);
warning_label->set_mouse_filter(MOUSE_FILTER_STOP);
warning_label->set_text(TTR("Warnings:"));
warning_label->add_font_override("font", EditorNode::get_singleton()->get_gui_base()->get_font("status_source", "EditorFonts"));
warning_count_label = memnew(Label);
status_bar->add_child(warning_count_label);
warning_count_label->set_valign(Label::VALIGN_CENTER);
warning_count_label->set_v_size_flags(SIZE_FILL);
warning_count_label->set_autowrap(true); // workaround to prevent resizing the label on each change, do not touch
warning_count_label->set_clip_text(true); // workaround to prevent resizing the label on each change, do not touch
warning_count_label->set_custom_minimum_size(Size2(40, 1) * EDSCALE);
warning_count_label->set_align(Label::ALIGN_RIGHT);
warning_count_label->set_default_cursor_shape(CURSOR_POINTING_HAND);
warning_count_label->set_mouse_filter(MOUSE_FILTER_STOP);
warning_count_label->add_font_override("font", EditorNode::get_singleton()->get_gui_base()->get_font("status_source", "EditorFonts"));
warning_count_label->set_text("0");
Label *zoom_txt = memnew(Label);
status_bar->add_child(zoom_txt);
zoom_txt->set_align(Label::ALIGN_RIGHT);

View file

@ -142,6 +142,8 @@ class CodeTextEditor : public VBoxContainer {
TextEdit *text_editor;
FindReplaceBar *find_replace_bar;
HBoxContainer *status_bar;
Label *warning_label;
Label *warning_count_label;
Label *line_nb;
Label *col_nb;
@ -214,6 +216,8 @@ public:
void update_line_and_column() { _line_col_changed(); }
TextEdit *get_text_edit() { return text_editor; }
FindReplaceBar *get_find_replace_bar() { return find_replace_bar; }
Label *get_warning_label() const { return warning_label; }
Label *get_warning_count_label() const { return warning_count_label; }
virtual void apply_code() {}
void set_code_complete_func(CodeTextEditorCodeCompleteFunc p_code_complete_func, void *p_ud);

View file

@ -274,6 +274,23 @@ void ScriptTextEditor::_set_theme_for_script() {
}
}
void ScriptTextEditor::_toggle_warning_pannel(const Ref<InputEvent> &p_event) {
Ref<InputEventMouseButton> mb = p_event;
if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == BUTTON_LEFT) {
warnings_panel->set_visible(!warnings_panel->is_visible());
}
}
void ScriptTextEditor::_warning_clicked(Variant p_line) {
if (p_line.get_type() == Variant::INT) {
code_editor->get_text_edit()->cursor_set_line(p_line.operator int64_t());
} else if (p_line.get_type() == Variant::DICTIONARY) {
Dictionary meta = p_line.operator Dictionary();
code_editor->get_text_edit()->insert_at("#warning-ignore:" + meta["code"].operator String(), meta["line"].operator int64_t() - 1);
_validate_script();
}
}
void ScriptTextEditor::reload_text() {
ERR_FAIL_COND(script.is_null());
@ -421,8 +438,9 @@ void ScriptTextEditor::_validate_script() {
String text = te->get_text();
List<String> fnc;
Set<int> safe_lines;
List<ScriptLanguage::Warning> warnings;
if (!script->get_language()->validate(text, line, col, errortxt, script->get_path(), &fnc, &safe_lines)) {
if (!script->get_language()->validate(text, line, col, errortxt, script->get_path(), &fnc, &warnings, &safe_lines)) {
String error_text = "error(" + itos(line) + "," + itos(col) + "): " + errortxt;
code_editor->set_error(error_text);
} else {
@ -442,6 +460,37 @@ void ScriptTextEditor::_validate_script() {
}
}
code_editor->get_warning_count_label()->set_text(itos(warnings.size()));
warnings_panel->clear();
warnings_panel->push_table(3);
for (List<ScriptLanguage::Warning>::Element *E = warnings.front(); E; E = E->next()) {
ScriptLanguage::Warning w = E->get();
warnings_panel->push_cell();
warnings_panel->push_meta(w.line - 1);
warnings_panel->push_color(warnings_panel->get_color("warning_color", "Editor"));
warnings_panel->add_text(TTR("Line") + " " + itos(w.line));
warnings_panel->add_text(" (" + w.string_code + "):");
warnings_panel->pop(); // Color
warnings_panel->pop(); // Meta goto
warnings_panel->pop(); // Cell
warnings_panel->push_cell();
warnings_panel->add_text(w.message);
warnings_panel->pop(); // Cell
Dictionary ignore_meta;
ignore_meta["line"] = w.line;
ignore_meta["code"] = w.string_code.to_lower();
warnings_panel->push_cell();
warnings_panel->push_meta(ignore_meta);
warnings_panel->add_text(TTR("(ignore)"));
warnings_panel->pop(); // Meta ignore
warnings_panel->pop(); // Cell
//warnings_panel->add_newline();
}
warnings_panel->pop(); // Table
line--;
bool highlight_safe = EDITOR_DEF("text_editor/highlighting/highlight_type_safe_lines", true);
bool last_is_safe = false;
@ -1022,6 +1071,8 @@ void ScriptTextEditor::_bind_methods() {
ClassDB::bind_method("_goto_line", &ScriptTextEditor::_goto_line);
ClassDB::bind_method("_lookup_symbol", &ScriptTextEditor::_lookup_symbol);
ClassDB::bind_method("_text_edit_gui_input", &ScriptTextEditor::_text_edit_gui_input);
ClassDB::bind_method("_toggle_warning_pannel", &ScriptTextEditor::_toggle_warning_pannel);
ClassDB::bind_method("_warning_clicked", &ScriptTextEditor::_warning_clicked);
ClassDB::bind_method("_color_changed", &ScriptTextEditor::_color_changed);
ClassDB::bind_method("get_drag_data_fw", &ScriptTextEditor::get_drag_data_fw);
@ -1333,8 +1384,13 @@ ScriptTextEditor::ScriptTextEditor() {
theme_loaded = false;
VSplitContainer *editor_box = memnew(VSplitContainer);
add_child(editor_box);
editor_box->set_anchors_and_margins_preset(Control::PRESET_WIDE);
editor_box->set_v_size_flags(SIZE_EXPAND_FILL);
code_editor = memnew(CodeTextEditor);
add_child(code_editor);
editor_box->add_child(code_editor);
code_editor->add_constant_override("separation", 0);
code_editor->set_anchors_and_margins_preset(Control::PRESET_WIDE);
code_editor->connect("validate_script", this, "_validate_script");
@ -1342,7 +1398,20 @@ ScriptTextEditor::ScriptTextEditor() {
code_editor->set_code_complete_func(_code_complete_scripts, this);
code_editor->get_text_edit()->connect("breakpoint_toggled", this, "_breakpoint_toggled");
code_editor->get_text_edit()->connect("symbol_lookup", this, "_lookup_symbol");
code_editor->set_v_size_flags(Control::SIZE_EXPAND_FILL);
code_editor->set_v_size_flags(SIZE_EXPAND_FILL);
warnings_panel = memnew(RichTextLabel);
editor_box->add_child(warnings_panel);
warnings_panel->set_custom_minimum_size(Size2(0, 100 * EDSCALE));
warnings_panel->set_h_size_flags(SIZE_EXPAND_FILL);
warnings_panel->set_meta_underline(true);
warnings_panel->set_selection_enabled(true);
warnings_panel->set_focus_mode(FOCUS_CLICK);
warnings_panel->hide();
code_editor->get_warning_label()->connect("gui_input", this, "_toggle_warning_pannel");
code_editor->get_warning_count_label()->connect("gui_input", this, "_toggle_warning_pannel");
warnings_panel->connect("meta_clicked", this, "_warning_clicked");
update_settings();

View file

@ -39,6 +39,7 @@ class ScriptTextEditor : public ScriptEditorBase {
GDCLASS(ScriptTextEditor, ScriptEditorBase);
CodeTextEditor *code_editor;
RichTextLabel *warnings_panel;
Ref<Script> script;
@ -124,6 +125,8 @@ protected:
void _code_complete_script(const String &p_code, List<String> *r_options, bool &r_force);
void _load_theme_settings();
void _set_theme_for_script();
void _toggle_warning_pannel(const Ref<InputEvent> &p_event);
void _warning_clicked(Variant p_line);
void _notification(int p_what);
static void _bind_methods();

View file

@ -1060,7 +1060,7 @@ Ref<Script> NativeScriptLanguage::get_template(const String &p_class_name, const
s->set_class_name(p_class_name);
return Ref<NativeScript>(s);
}
bool NativeScriptLanguage::validate(const String &p_script, int &r_line_error, int &r_col_error, String &r_test_error, const String &p_path, List<String> *r_functions, Set<int> *r_safe_lines) const {
bool NativeScriptLanguage::validate(const String &p_script, int &r_line_error, int &r_col_error, String &r_test_error, const String &p_path, List<String> *r_functions, List<ScriptLanguage::Warning> *r_warnings, Set<int> *r_safe_lines) const {
return true;
}

View file

@ -295,7 +295,7 @@ public:
virtual void get_comment_delimiters(List<String> *p_delimiters) const;
virtual void get_string_delimiters(List<String> *p_delimiters) const;
virtual Ref<Script> get_template(const String &p_class_name, const String &p_base_class_name) const;
virtual bool validate(const String &p_script, int &r_line_error, int &r_col_error, String &r_test_error, const String &p_path, List<String> *r_functions, Set<int> *r_safe_lines = NULL) const;
virtual bool validate(const String &p_script, int &r_line_error, int &r_col_error, String &r_test_error, const String &p_path, List<String> *r_functions, List<ScriptLanguage::Warning> *r_warnings = NULL, Set<int> *r_safe_lines = NULL) const;
virtual Script *create_script() const;
virtual bool has_named_classes() const;
virtual bool supports_builtin_mode() const;

View file

@ -108,7 +108,7 @@ Ref<Script> PluginScriptLanguage::get_template(const String &p_class_name, const
return script;
}
bool PluginScriptLanguage::validate(const String &p_script, int &r_line_error, int &r_col_error, String &r_test_error, const String &p_path, List<String> *r_functions, Set<int> *r_safe_lines) const {
bool PluginScriptLanguage::validate(const String &p_script, int &r_line_error, int &r_col_error, String &r_test_error, const String &p_path, List<String> *r_functions, List<ScriptLanguage::Warning> *r_warnings, Set<int> *r_safe_lines) const {
PoolStringArray functions;
if (_desc.validate) {
bool ret = _desc.validate(

View file

@ -74,7 +74,7 @@ public:
virtual void get_comment_delimiters(List<String> *p_delimiters) const;
virtual void get_string_delimiters(List<String> *p_delimiters) const;
virtual Ref<Script> get_template(const String &p_class_name, const String &p_base_class_name) const;
virtual bool validate(const String &p_script, int &r_line_error, int &r_col_error, String &r_test_error, const String &p_path = "", List<String> *r_functions = NULL, Set<int> *r_safe_lines = NULL) const;
virtual bool validate(const String &p_script, int &r_line_error, int &r_col_error, String &r_test_error, const String &p_path = "", List<String> *r_functions = NULL, List<ScriptLanguage::Warning> *r_warnings = NULL, Set<int> *r_safe_lines = NULL) const;
virtual Script *create_script() const;
virtual bool has_named_classes() const;
virtual bool supports_builtin_mode() const;

View file

@ -596,6 +596,13 @@ Error GDScript::reload(bool p_keep_state) {
return err;
}
}
#if DEBUG_ENABLED
for (const List<GDScriptWarning>::Element *E = parser.get_warnings().front(); E; E = E->next()) {
String msg = "Script warning: " + E->get().get_name() + " (" + path + ") line " + itos(E->get().line) + ": ";
msg += E->get().get_message();
WARN_PRINTS(msg);
}
#endif
valid = true;
@ -1867,6 +1874,162 @@ String GDScriptLanguage::get_global_class_name(const String &p_path, String *r_b
return String();
}
#ifdef DEBUG_ENABLED
String GDScriptWarning::get_message() const {
#define CHECK_SYMBOLS(m_amount) ERR_FAIL_COND_V(symbols.size() < m_amount, String());
switch (code) {
case UNASSIGNED_VARIABLE_OP_ASSIGN: {
CHECK_SYMBOLS(1);
return "Using assignment with operation but the variable '" + symbols[0] + "' was not previously assigned a value.";
} break;
case UNASSIGNED_VARIABLE: {
CHECK_SYMBOLS(1);
return "The variable '" + symbols[0] + "' was used but never assigned a value.";
} break;
case UNUSED_VARIABLE: {
CHECK_SYMBOLS(1);
return "The local variable '" + symbols[0] + "' is declared but never used in the block.";
} break;
case UNUSED_CLASS_VARIABLE: {
CHECK_SYMBOLS(1);
return "The class variable '" + symbols[0] + "' is declared but never used in the script.";
} break;
case UNUSED_ARGUMENT: {
CHECK_SYMBOLS(2);
return "The argument '" + symbols[1] + "' is never used in the function '" + symbols[0] + "'.";
} break;
case UNREACHABLE_CODE: {
CHECK_SYMBOLS(1);
return "Unreachable code (statement after return) in function '" + symbols[0] + "()'.";
} break;
case STANDALONE_EXPRESSION: {
return "Standalone expression (the line has no effect).";
} break;
case VOID_ASSIGNMENT: {
CHECK_SYMBOLS(1);
return "Assignment operation, but the function '" + symbols[0] + "()' returns void.";
} break;
case NARROWING_CONVERSION: {
return "Narrowing coversion (float is converted to int and lose precision).";
} break;
case FUNCTION_MAY_YIELD: {
CHECK_SYMBOLS(1);
return "Assigned variable is typed but the function '" + symbols[0] + "()' may yield and return a GDScriptFunctionState instead.";
} break;
case VARIABLE_CONFLICTS_FUNCTION: {
CHECK_SYMBOLS(1);
return "Variable declaration of '" + symbols[0] + "' conflicts with a function of the same name.";
} break;
case FUNCTION_CONFLICTS_VARIABLE: {
CHECK_SYMBOLS(1);
return "Function declaration of '" + symbols[0] + "()' conflicts with a variable of the same name.";
} break;
case FUNCTION_CONFLICTS_CONSTANT: {
CHECK_SYMBOLS(1);
return "Function declaration of '" + symbols[0] + "()' conflicts with a constant of the same name.";
} break;
case INCOMPATIBLE_TERNARY: {
return "Values of the ternary conditional are not mutually compatible.";
} break;
case UNUSED_SIGNAL: {
CHECK_SYMBOLS(1);
return "The signal '" + symbols[0] + "' is declared but never emitted.";
} break;
case RETURN_VALUE_DISCARDED: {
CHECK_SYMBOLS(1);
return "The function '" + symbols[0] + "()' returns a value, but this value is never used.";
} break;
case PROPERTY_USED_AS_FUNCTION: {
CHECK_SYMBOLS(2);
return "The method '" + symbols[0] + "()' was not found in base '" + symbols[1] + "' but there's a property with the same name. Did you mean to access it?";
} break;
case CONSTANT_USED_AS_FUNCTION: {
CHECK_SYMBOLS(2);
return "The method '" + symbols[0] + "()' was not found in base '" + symbols[1] + "' but there's a constant with the same name. Did you mean to access it?";
} break;
case FUNCTION_USED_AS_PROPERTY: {
CHECK_SYMBOLS(2);
return "The property '" + symbols[0] + "' was not found in base '" + symbols[1] + "' but there's a method with the same name. Did you mean to call it?";
} break;
case INTEGER_DIVISION: {
return "Integer division, decimal part will be discarded.";
} break;
case UNSAFE_PROPERTY_ACCESS: {
CHECK_SYMBOLS(2);
return "The property '" + symbols[0] + "' is not present on the inferred type '" + symbols[1] + "' (but may be present on a subtype).";
} break;
case UNSAFE_METHOD_ACCESS: {
CHECK_SYMBOLS(2);
return "The method '" + symbols[0] + "' is not present on the inferred type '" + symbols[1] + "' (but may be present on a subtype).";
} break;
case UNSAFE_CAST: {
CHECK_SYMBOLS(1);
return "The value is cast to '" + symbols[0] + "' but has an unkown type.";
} break;
case UNSAFE_CALL_ARGUMENT: {
CHECK_SYMBOLS(4);
return "The argument '" + symbols[0] + "' of the function '" + symbols[1] + "' requires a the subtype '" + symbols[2] + "' but the supertype '" + symbols[3] + "' was provided";
} break;
}
ERR_EXPLAIN("Invalid GDScript waring code: " + get_name_from_code(code));
ERR_FAIL_V(String());
#undef CHECK_SYMBOLS
}
String GDScriptWarning::get_name() const {
return get_name_from_code(code);
}
String GDScriptWarning::get_name_from_code(Code p_code) {
ERR_FAIL_COND_V(p_code < 0 || p_code >= WARNING_MAX, String());
static const char *names[] = {
"UNASSIGNED_VARIABLE",
"UNASSIGNED_VARIABLE_OP_ASSIGN",
"UNUSED_VARIABLE",
"UNUSED_CLASS_VARIABLE",
"UNUSED_ARGUMENT",
"UNREACHABLE_CODE",
"STANDALONE_EXPRESSION",
"VOID_ASSIGNMENT",
"NARROWING_CONVERSION",
"FUNCTION_MAY_YIELD",
"VARIABLE_CONFLICTS_FUNCTION",
"FUNCTION_CONFLICTS_VARIABLE",
"FUNCTION_CONFLICTS_CONSTANT",
"INCOMPATIBLE_TERNARY",
"UNUSED_SIGNAL",
"RETURN_VALUE_DISCARDED",
"PROPERTY_USED_AS_FUNCTION",
"CONSTANT_USED_AS_FUNCTION",
"FUNCTION_USED_AS_PROPERTY",
"INTEGER_DIVISION",
"UNSAFE_PROPERTY_ACCESS",
"UNSAFE_METHOD_ACCESS",
"UNSAFE_CAST",
"UNSAFE_CALL_ARGUMENT",
NULL
};
return names[(int)p_code];
}
GDScriptWarning::Code GDScriptWarning::get_code_from_name(const String &p_name) {
for (int i = 0; i < WARNING_MAX; i++) {
if (get_name_from_code((Code)i) == p_name) {
return (Code)i;
}
}
ERR_EXPLAIN("Invalid GDScript waring name: " + p_name);
ERR_FAIL_V(WARNING_MAX);
}
#endif // DEBUG_ENABLED
GDScriptLanguage::GDScriptLanguage() {
calls = 0;
@ -1903,6 +2066,15 @@ GDScriptLanguage::GDScriptLanguage() {
_debug_max_call_stack = 0;
_call_stack = NULL;
}
#ifdef DEBUG_ENABLED
GLOBAL_DEF("debug/gdscript/warnings/enable", true);
GLOBAL_DEF("debug/gdscript/warnings/treat_warnings_as_errors", false);
for (int i = 0; i < (int)GDScriptWarning::WARNING_MAX; i++) {
String warning = GDScriptWarning::get_name_from_code((GDScriptWarning::Code)i).to_lower();
GLOBAL_DEF("debug/gdscript/warnings/" + warning, !warning.begins_with("unsafe_"));
}
#endif // DEBUG_ENABLED
}
GDScriptLanguage::~GDScriptLanguage() {

View file

@ -261,6 +261,49 @@ public:
~GDScriptInstance();
};
#ifdef DEBUG_ENABLED
struct GDScriptWarning {
enum Code {
UNASSIGNED_VARIABLE, // Variable used but never assigned
UNASSIGNED_VARIABLE_OP_ASSIGN, // Variable never assigned but used in an assignment operation (+=, *=, etc)
UNUSED_VARIABLE, // Local variable is declared but never used
UNUSED_CLASS_VARIABLE, // Class variable is declared but never used in the file
UNUSED_ARGUMENT, // Function argument is never used
UNREACHABLE_CODE, // Code after a return statement
STANDALONE_EXPRESSION, // Expression not assigned to a variable
VOID_ASSIGNMENT, // Function returns void but it's assigned to a variable
NARROWING_CONVERSION, // Float value into an integer slot, precision is lost
FUNCTION_MAY_YIELD, // Typed assign of function call that yields (it may return a function state)
VARIABLE_CONFLICTS_FUNCTION, // Variable has the same name of a function
FUNCTION_CONFLICTS_VARIABLE, // Function has the same name of a variable
FUNCTION_CONFLICTS_CONSTANT, // Function has the same name of a constant
INCOMPATIBLE_TERNARY, // Possible values of a ternary if are not mutually compatible
UNUSED_SIGNAL, // Signal is defined but never emitted
RETURN_VALUE_DISCARDED, // Function call returns something but the value isn't used
PROPERTY_USED_AS_FUNCTION, // Function not found, but there's a property with the same name
CONSTANT_USED_AS_FUNCTION, // Function not found, but there's a constant with the same name
FUNCTION_USED_AS_PROPERTY, // Property not found, but there's a function with the same name
INTEGER_DIVISION, // Integer divide by integer, decimal part is discarded
UNSAFE_PROPERTY_ACCESS, // Property not found in the detected type (but can be in subtypes)
UNSAFE_METHOD_ACCESS, // Fucntion not found in the detected type (but can be in subtypes)
UNSAFE_CAST, // Cast used in an unknown type
UNSAFE_CALL_ARGUMENT, // Function call argument is of a supertype of the require argument
WARNING_MAX,
} code;
Vector<String> symbols;
int line;
String get_name() const;
String get_message() const;
static String get_name_from_code(Code p_code);
static Code get_code_from_name(const String &p_name);
GDScriptWarning() :
line(-1),
code(WARNING_MAX) {}
};
#endif // DEBUG_ENABLED
class GDScriptLanguage : public ScriptLanguage {
static GDScriptLanguage *singleton;
@ -397,7 +440,7 @@ public:
virtual Ref<Script> get_template(const String &p_class_name, const String &p_base_class_name) const;
virtual bool is_using_templates();
virtual void make_template(const String &p_class_name, const String &p_base_class_name, Ref<Script> &p_script);
virtual bool validate(const String &p_script, int &r_line_error, int &r_col_error, String &r_test_error, const String &p_path = "", List<String> *r_functions = NULL, Set<int> *r_safe_lines = NULL) const;
virtual bool validate(const String &p_script, int &r_line_error, int &r_col_error, String &r_test_error, const String &p_path = "", List<String> *r_functions = NULL, List<ScriptLanguage::Warning> *r_warnings = NULL, Set<int> *r_safe_lines = NULL) const;
virtual Script *create_script() const;
virtual bool has_named_classes() const;
virtual bool supports_builtin_mode() const;

View file

@ -116,11 +116,24 @@ void GDScriptLanguage::make_template(const String &p_class_name, const String &p
p_script->set_source_code(src);
}
bool GDScriptLanguage::validate(const String &p_script, int &r_line_error, int &r_col_error, String &r_test_error, const String &p_path, List<String> *r_functions, Set<int> *r_safe_lines) const {
bool GDScriptLanguage::validate(const String &p_script, int &r_line_error, int &r_col_error, String &r_test_error, const String &p_path, List<String> *r_functions, List<ScriptLanguage::Warning> *r_warnings, Set<int> *r_safe_lines) const {
GDScriptParser parser;
Error err = parser.parse(p_script, p_path.get_base_dir(), true, p_path, false, r_safe_lines);
#ifdef DEBUG_ENABLED
if (r_warnings) {
for (const List<GDScriptWarning>::Element *E = parser.get_warnings().front(); E; E = E->next()) {
const GDScriptWarning &warn = E->get();
ScriptLanguage::Warning w;
w.line = warn.line;
w.code = (int)warn.code;
w.string_code = GDScriptWarning::get_name_from_code(warn.code);
w.message = warn.get_message();
r_warnings->push_back(w);
}
}
#endif
if (err) {
r_line_error = parser.get_error_line();
r_col_error = parser.get_error_column();

View file

@ -38,6 +38,7 @@
#include "io/resource_loader.h"
#include "os/file_access.h"
#include "print_string.h"
#include "project_settings.h"
#include "script_language.h"
template <class T>
@ -56,6 +57,8 @@ T *GDScriptParser::alloc_node() {
return t;
}
static String _find_function_name(const GDScriptParser::OperatorNode *p_call);
bool GDScriptParser::_end_statement() {
if (tokenizer->get_token() == GDScriptTokenizer::TK_SEMICOLON) {
@ -726,7 +729,7 @@ GDScriptParser::Node *GDScriptParser::_parse_expression(Node *p_parent, bool p_s
}
BlockNode *b = current_block;
while (b) {
while (!bfn && b) {
if (b->variables.has(identifier)) {
IdentifierNode *id = alloc_node<IdentifierNode>();
LocalVarNode *lv = b->variables[identifier];
@ -736,6 +739,7 @@ GDScriptParser::Node *GDScriptParser::_parse_expression(Node *p_parent, bool p_s
expr = id;
bfn = true;
#ifdef DEBUG_ENABLED
switch (tokenizer->get_token()) {
case GDScriptTokenizer::TK_OP_ASSIGN_ADD:
case GDScriptTokenizer::TK_OP_ASSIGN_BIT_AND:
@ -747,15 +751,23 @@ GDScriptParser::Node *GDScriptParser::_parse_expression(Node *p_parent, bool p_s
case GDScriptTokenizer::TK_OP_ASSIGN_SHIFT_LEFT:
case GDScriptTokenizer::TK_OP_ASSIGN_SHIFT_RIGHT:
case GDScriptTokenizer::TK_OP_ASSIGN_SUB: {
if (lv->assignments == 0 && !lv->datatype.has_type) {
_set_error("Using assignment with operation on a variable that was never assigned.");
return NULL;
if (lv->assignments == 0) {
if (!lv->datatype.has_type) {
_set_error("Using assignment with operation on a variable that was never assigned.");
return NULL;
}
_add_warning(GDScriptWarning::UNASSIGNED_VARIABLE_OP_ASSIGN, -1, identifier.operator String());
}
} // fallthrough
case GDScriptTokenizer::TK_OP_ASSIGN: {
lv->assignments += 1;
lv->usages--; // Assignment is not really usage
} break;
default: {
lv->usages++;
}
}
#endif // DEBUG_ENABLED
break;
}
b = b->parent_block;
@ -785,6 +797,32 @@ GDScriptParser::Node *GDScriptParser::_parse_expression(Node *p_parent, bool p_s
}
if (!bfn) {
#ifdef DEBUG_ENABLED
if (current_function) {
int arg_idx = current_function->arguments.find(identifier);
if (arg_idx != -1) {
switch (tokenizer->get_token()) {
case GDScriptTokenizer::TK_OP_ASSIGN_ADD:
case GDScriptTokenizer::TK_OP_ASSIGN_BIT_AND:
case GDScriptTokenizer::TK_OP_ASSIGN_BIT_OR:
case GDScriptTokenizer::TK_OP_ASSIGN_BIT_XOR:
case GDScriptTokenizer::TK_OP_ASSIGN_DIV:
case GDScriptTokenizer::TK_OP_ASSIGN_MOD:
case GDScriptTokenizer::TK_OP_ASSIGN_MUL:
case GDScriptTokenizer::TK_OP_ASSIGN_SHIFT_LEFT:
case GDScriptTokenizer::TK_OP_ASSIGN_SHIFT_RIGHT:
case GDScriptTokenizer::TK_OP_ASSIGN_SUB:
case GDScriptTokenizer::TK_OP_ASSIGN: {
// Assignment is not really usage
current_function->arguments_usage.write[arg_idx] = current_function->arguments_usage[arg_idx] - 1;
} break;
default: {
current_function->arguments_usage.write[arg_idx] = current_function->arguments_usage[arg_idx] + 1;
}
}
}
}
#endif // DEBUG_ENABLED
IdentifierNode *id = alloc_node<IdentifierNode>();
id->name = identifier;
id->line = id_line;
@ -2601,6 +2639,7 @@ void GDScriptParser::_parse_block(BlockNode *p_block, bool p_static) {
pending_newline = -1;
}
#ifdef DEBUG_ENABLED
switch (token) {
case GDScriptTokenizer::TK_EOF:
case GDScriptTokenizer::TK_ERROR:
@ -2609,13 +2648,13 @@ void GDScriptParser::_parse_block(BlockNode *p_block, bool p_static) {
// will check later
} break;
default: {
// TODO: Make this a warning
/*if (p_block->has_return) {
_set_error("Unreacheable code.");
return;
}*/
if (p_block->has_return && !current_function->has_unreachable_code) {
_add_warning(GDScriptWarning::UNREACHABLE_CODE, -1, current_function->name.operator String());
current_function->has_unreachable_code = true;
}
} break;
}
#endif // DEBUG_ENABLED
switch (token) {
case GDScriptTokenizer::TK_EOF:
p_block->end_line = tokenizer->get_token_line();
@ -2728,6 +2767,7 @@ void GDScriptParser::_parse_block(BlockNode *p_block, bool p_static) {
c->line = var_line;
assigned = c;
}
lv->assign = assigned;
//must be added later, to avoid self-referencing.
p_block->variables.insert(n, lv);
@ -2745,6 +2785,8 @@ void GDScriptParser::_parse_block(BlockNode *p_block, bool p_static) {
lv->assign_op = op;
lv->assign = assigned;
lv->assign_op = op;
if (!_end_statement()) {
_set_error("Expected end of statement (var)");
return;
@ -3513,6 +3555,17 @@ void GDScriptParser::_parse_class(ClassNode *p_class) {
}
}
#ifdef DEBUG_ENABLED
if (p_class->constant_expressions.has(name)) {
_add_warning(GDScriptWarning::FUNCTION_CONFLICTS_CONSTANT, -1, name);
}
for (int i = 0; i < p_class->variables.size(); i++) {
if (p_class->variables[i].identifier == name) {
_add_warning(GDScriptWarning::FUNCTION_CONFLICTS_VARIABLE, -1, name);
}
}
#endif // DEBUG_ENABLED
if (tokenizer->get_token() != GDScriptTokenizer::TK_PARENTHESIS_OPEN) {
_set_error("Expected '(' after identifier (syntax: 'func <identifier>([arguments]):' ).");
@ -3524,6 +3577,9 @@ void GDScriptParser::_parse_class(ClassNode *p_class) {
Vector<StringName> arguments;
Vector<DataType> argument_types;
Vector<Node *> default_values;
#ifdef DEBUG_ENABLED
Vector<int> arguments_usage;
#endif // DEBUG_ENABLED
int fnline = tokenizer->get_token_line();
@ -3550,6 +3606,9 @@ void GDScriptParser::_parse_class(ClassNode *p_class) {
StringName argname = tokenizer->get_token_identifier();
arguments.push_back(argname);
#ifdef DEBUG_ENABLED
arguments_usage.push_back(0);
#endif // DEBUG_ENABLED
tokenizer->advance();
@ -3703,7 +3762,9 @@ void GDScriptParser::_parse_class(ClassNode *p_class) {
function->default_values = default_values;
function->_static = _static;
function->line = fnline;
#ifdef DEBUG_ENABLED
function->arguments_usage = arguments_usage;
#endif // DEBUG_ENABLED
function->rpc_mode = rpc_mode;
rpc_mode = MultiplayerAPI::RPC_MODE_DISABLED;
@ -3730,6 +3791,8 @@ void GDScriptParser::_parse_class(ClassNode *p_class) {
ClassNode::Signal sig;
sig.name = tokenizer->get_token_identifier();
sig.emissions = 0;
sig.line = tokenizer->get_token_line();
tokenizer->advance();
if (tokenizer->get_token() == GDScriptTokenizer::TK_PARENTHESIS_OPEN) {
@ -4413,6 +4476,7 @@ void GDScriptParser::_parse_class(ClassNode *p_class) {
member.expression = NULL;
member._export.name = member.identifier;
member.line = tokenizer->get_token_line();
member.usages = 0;
member.rpc_mode = rpc_mode;
if (current_class->constant_expressions.has(member.identifier)) {
@ -4428,7 +4492,20 @@ void GDScriptParser::_parse_class(ClassNode *p_class) {
return;
}
}
#ifdef DEBUG_ENABLED
for (int i = 0; i < current_class->functions.size(); i++) {
if (current_class->functions[i]->name == member.identifier) {
_add_warning(GDScriptWarning::VARIABLE_CONFLICTS_FUNCTION, member.line, member.identifier);
break;
}
}
for (int i = 0; i < current_class->static_functions.size(); i++) {
if (current_class->static_functions[i]->name == member.identifier) {
_add_warning(GDScriptWarning::VARIABLE_CONFLICTS_FUNCTION, member.line, member.identifier);
break;
}
}
#endif // DEBUG_ENABLED
tokenizer->advance();
rpc_mode = MultiplayerAPI::RPC_MODE_DISABLED;
@ -5689,11 +5766,26 @@ GDScriptParser::DataType GDScriptParser::_reduce_node_type(Node *p_node) {
node_type.has_type = true;
node_type.kind = DataType::BUILTIN;
node_type.builtin_type = Variant::ARRAY;
#ifdef DEBUG_ENABLED
// Check stuff inside the array
ArrayNode *an = static_cast<ArrayNode *>(p_node);
for (int i = 0; i < an->elements.size(); i++) {
_reduce_node_type(an->elements[i]);
}
#endif // DEBUG_ENABLED
} break;
case Node::TYPE_DICTIONARY: {
node_type.has_type = true;
node_type.kind = DataType::BUILTIN;
node_type.builtin_type = Variant::DICTIONARY;
#ifdef DEBUG_ENABLED
// Check stuff inside the dictionarty
DictionaryNode *dn = static_cast<DictionaryNode *>(p_node);
for (int i = 0; i < dn->elements.size(); i++) {
_reduce_node_type(dn->elements[i].key);
_reduce_node_type(dn->elements[i].value);
}
#endif // DEBUG_ENABLED
} break;
case Node::TYPE_SELF: {
node_type.has_type = true;
@ -5704,6 +5796,8 @@ GDScriptParser::DataType GDScriptParser::_reduce_node_type(Node *p_node) {
IdentifierNode *id = static_cast<IdentifierNode *>(p_node);
if (id->declared_block) {
node_type = id->declared_block->variables[id->name]->get_datatype();
id->declared_block->variables[id->name]->usages += 1;
print_line("var " + id->name + " line " + itos(id->line) + " usages " + itos(id->declared_block->variables[id->name]->usages));
} else if (id->name == "#match_value") {
// It's a special id just for the match statetement, ignore
break;
@ -5738,6 +5832,9 @@ GDScriptParser::DataType GDScriptParser::_reduce_node_type(Node *p_node) {
}
}
} else {
#ifdef DEBUG_ENABLED
_add_warning(GDScriptWarning::UNSAFE_CAST, cn->line, cn->cast_type.to_string());
#endif // DEBUG_ENABLED
_mark_line_as_unsafe(cn->line);
}
@ -5864,6 +5961,12 @@ GDScriptParser::DataType GDScriptParser::_reduce_node_type(Node *p_node) {
op->line, op->column);
return DataType();
}
#ifdef DEBUG_ENABLED
if (var_op == Variant::OP_DIVIDE && argument_a_type.has_type && argument_a_type.kind == DataType::BUILTIN && argument_a_type.builtin_type == Variant::INT &&
argument_b_type.has_type && argument_b_type.kind == DataType::BUILTIN && argument_b_type.builtin_type == Variant::INT) {
_add_warning(GDScriptWarning::INTEGER_DIVISION, op->line);
}
#endif // DEBUG_ENABLED
} break;
// Ternary operators
@ -5882,10 +5985,11 @@ GDScriptParser::DataType GDScriptParser::_reduce_node_type(Node *p_node) {
node_type = true_type;
} else if (_is_type_compatible(false_type, true_type)) {
node_type = false_type;
} else {
#ifdef DEBUG_ENABLED
_add_warning(GDScriptWarning::INCOMPATIBLE_TERNARY, op->line);
#endif // DEBUG_ENABLED
}
// TODO: Warn if types aren't compatible
} break;
// Assignment should never happen within an expression
case OperatorNode::OP_ASSIGN:
@ -5948,6 +6052,11 @@ GDScriptParser::DataType GDScriptParser::_reduce_node_type(Node *p_node) {
node_type = result;
} else {
node_type = _reduce_identifier_type(&base_type, member_id->name, op->line);
#ifdef DEBUG_ENABLED
if (!node_type.has_type) {
_add_warning(GDScriptWarning::UNSAFE_PROPERTY_ACCESS, op->line, member_id->name.operator String(), base_type.to_string());
}
#endif // DEBUG_ENABLED
}
} else {
_mark_line_as_unsafe(op->line);
@ -6367,6 +6476,15 @@ GDScriptParser::DataType GDScriptParser::_reduce_function_call_type(const Operat
if (!_is_type_compatible(arg_type, par_types[i], true)) {
types_match = false;
break;
} else {
#ifdef DEBUG_ENABLED
if (arg_type.kind == DataType::BUILTIN && arg_type.builtin_type == Variant::INT && par_types[i].kind == DataType::BUILTIN && par_types[i].builtin_type == Variant::REAL) {
_add_warning(GDScriptWarning::NARROWING_CONVERSION, p_call->line, Variant::get_type_name(tn->vtype));
}
if (par_types[i].may_yield && p_call->arguments[i + 1]->type == Node::TYPE_OPERATOR) {
_add_warning(GDScriptWarning::FUNCTION_MAY_YIELD, p_call->line, _find_function_name(static_cast<OperatorNode *>(p_call->arguments[i + 1])));
}
#endif // DEBUG_ENABLED
}
}
@ -6400,6 +6518,13 @@ GDScriptParser::DataType GDScriptParser::_reduce_function_call_type(const Operat
return_type = _type_from_property(mi.return_val, false);
#ifdef DEBUG_ENABLED
// Check all arguments beforehand to solve warnings
for (int i = 1; i < p_call->arguments.size(); i++) {
_reduce_node_type(p_call->arguments[i]);
}
#endif // DEBUG_ENABLED
// Check arguments
is_vararg = mi.flags & METHOD_FLAG_VARARG;
@ -6426,6 +6551,13 @@ GDScriptParser::DataType GDScriptParser::_reduce_function_call_type(const Operat
ERR_FAIL_V(DataType());
}
#ifdef DEBUG_ENABLED
// Check all arguments beforehand to solve warnings
for (int i = arg_id + 1; i < p_call->arguments.size(); i++) {
_reduce_node_type(p_call->arguments[i]);
}
#endif // DEBUG_ENABLED
IdentifierNode *func_id = static_cast<IdentifierNode *>(p_call->arguments[arg_id]);
callee_name = func_id->name;
arg_count -= 1 + arg_id;
@ -6505,8 +6637,18 @@ GDScriptParser::DataType GDScriptParser::_reduce_function_call_type(const Operat
_set_error("Method '" + callee_name + "' is not declared in the current class.", p_call->line);
return DataType();
}
DataType tmp_type;
valid = _get_member_type(original_type, func_id->name, tmp_type);
if (valid) {
if (tmp_type.is_constant) {
_add_warning(GDScriptWarning::CONSTANT_USED_AS_FUNCTION, p_call->line, callee_name, original_type.to_string());
} else {
_add_warning(GDScriptWarning::PROPERTY_USED_AS_FUNCTION, p_call->line, callee_name, original_type.to_string());
}
}
_add_warning(GDScriptWarning::UNSAFE_METHOD_ACCESS, p_call->line, callee_name, original_type.to_string());
_mark_line_as_unsafe(p_call->line);
#endif
#endif // DEBUG_ENABLED
return DataType();
}
@ -6522,7 +6664,19 @@ GDScriptParser::DataType GDScriptParser::_reduce_function_call_type(const Operat
_set_error("Non-static function '" + String(callee_name) + "' can only be called from an instance.", p_call->line);
return DataType();
}
#endif
// Check signal emission for warnings
if (callee_name == "emit_signal" && p_call->op == OperatorNode::OP_CALL && p_call->arguments[0]->type == Node::TYPE_SELF && p_call->arguments.size() >= 3 && p_call->arguments[2]->type == Node::TYPE_CONSTANT) {
ConstantNode *sig = static_cast<ConstantNode *>(p_call->arguments[2]);
String emitted = sig->value.get_type() == Variant::STRING ? sig->value.operator String() : "";
for (int i = 0; i < current_class->_signals.size(); i++) {
if (current_class->_signals[i].name == emitted) {
current_class->_signals.write[i].emissions += 1;
break;
}
}
}
#endif // DEBUG_ENABLED
} break;
}
@ -6547,8 +6701,15 @@ GDScriptParser::DataType GDScriptParser::_reduce_function_call_type(const Operat
continue;
}
DataType arg_type = arg_types[i - arg_diff];
if (!par_type.has_type) {
_mark_line_as_unsafe(p_call->line);
#ifdef DEBUG_ENABLED
if (par_type.may_yield && p_call->arguments[i]->type == Node::TYPE_OPERATOR) {
_add_warning(GDScriptWarning::FUNCTION_MAY_YIELD, p_call->line, _find_function_name(static_cast<OperatorNode *>(p_call->arguments[i])));
}
#endif // DEBUG_ENABLED
} else if (!_is_type_compatible(arg_types[i - arg_diff], par_type, true)) {
// Supertypes are acceptable for dynamic compliance
if (!_is_type_compatible(par_type, arg_types[i - arg_diff])) {
@ -6560,6 +6721,12 @@ GDScriptParser::DataType GDScriptParser::_reduce_function_call_type(const Operat
} else {
_mark_line_as_unsafe(p_call->line);
}
} else {
#ifdef DEBUG_ENABLED
if (arg_type.kind == DataType::BUILTIN && arg_type.builtin_type == Variant::INT && par_type.kind == DataType::BUILTIN && par_type.builtin_type == Variant::REAL) {
_add_warning(GDScriptWarning::NARROWING_CONVERSION, p_call->line, callee_name);
}
#endif // DEBUG_ENABLED
}
}
@ -6795,6 +6962,15 @@ GDScriptParser::DataType GDScriptParser::_reduce_identifier_type(const DataType
DataType member_type;
for (int i = 0; i < current_class->variables.size(); i++) {
ClassNode::Member m = current_class->variables[i];
if (current_class->variables[i].identifier == p_identifier) {
member_type = current_class->variables[i].data_type;
current_class->variables.write[i].usages += 1;
return member_type;
}
}
if (_get_member_type(base_type, p_identifier, member_type)) {
return member_type;
}
@ -6922,6 +7098,19 @@ GDScriptParser::DataType GDScriptParser::_reduce_identifier_type(const DataType
_set_error("Identifier '" + p_identifier.operator String() + "' is not declared in the current scope.", p_line);
}
#ifdef DEBUG_ENABLED
{
DataType tmp_type;
List<DataType> arg_types;
int argcount;
bool _static;
bool vararg;
if (_get_function_signature(base_type, p_identifier, tmp_type, arg_types, argcount, _static, vararg)) {
_add_warning(GDScriptWarning::FUNCTION_USED_AS_PROPERTY, p_line, p_identifier.operator String(), base_type.to_string());
}
}
#endif // DEBUG_ENABLED
_mark_line_as_unsafe(p_line);
return DataType();
}
@ -7174,6 +7363,11 @@ void GDScriptParser::_check_function_types(FunctionNode *p_function) {
}
}
}
#ifdef DEBUG_ENABLED
if (p_function->arguments_usage[i] == 0) {
_add_warning(GDScriptWarning::UNUSED_ARGUMENT, p_function->line, p_function->name, p_function->arguments[i].operator String());
}
#endif // DEBUG_ENABLED
}
if (!(p_function->name == "_init")) {
@ -7244,6 +7438,7 @@ void GDScriptParser::_check_function_types(FunctionNode *p_function) {
if (p_function->has_yield) {
// yield() will make the function return a GDScriptFunctionState, so the type is ambiguous
p_function->return_type.has_type = false;
p_function->return_type.may_yield = true;
}
}
@ -7270,6 +7465,20 @@ void GDScriptParser::_check_class_blocks_types(ClassNode *p_class) {
if (error_set) return;
}
#ifdef DEBUG_ENABLED
// Warnings
for (int i = 0; i < p_class->variables.size(); i++) {
if (p_class->variables[i].usages == 0) {
_add_warning(GDScriptWarning::UNUSED_CLASS_VARIABLE, p_class->variables[i].line, p_class->variables[i].identifier);
}
}
for (int i = 0; i < p_class->_signals.size(); i++) {
if (p_class->_signals[i].emissions == 0) {
_add_warning(GDScriptWarning::UNUSED_SIGNAL, p_class->_signals[i].line, p_class->_signals[i].name);
}
}
#endif // DEBUG_ENABLED
// Inner classes
for (int i = 0; i < p_class->subclasses.size(); i++) {
current_class = p_class->subclasses[i];
@ -7279,6 +7488,26 @@ void GDScriptParser::_check_class_blocks_types(ClassNode *p_class) {
}
}
#ifdef DEBUG_ENABLED
static String _find_function_name(const GDScriptParser::OperatorNode *p_call) {
switch (p_call->arguments[0]->type) {
case GDScriptParser::Node::TYPE_TYPE: {
return Variant::get_type_name(static_cast<GDScriptParser::TypeNode *>(p_call->arguments[0])->vtype);
} break;
case GDScriptParser::Node::TYPE_BUILT_IN_FUNCTION: {
return GDScriptFunctions::get_func_name(static_cast<GDScriptParser::BuiltInFunctionNode *>(p_call->arguments[0])->function);
} break;
default: {
int id_index = p_call->op == GDScriptParser::OperatorNode::OP_PARENT_CALL ? 0 : 1;
if (p_call->arguments.size() > id_index && p_call->arguments[id_index]->type == GDScriptParser::Node::TYPE_IDENTIFIER) {
return static_cast<GDScriptParser::IdentifierNode *>(p_call->arguments[id_index])->name;
}
} break;
}
return String();
}
#endif // DEBUG_ENABLED
void GDScriptParser::_check_block_types(BlockNode *p_block) {
Node *last_var_assign = NULL;
@ -7297,8 +7526,23 @@ void GDScriptParser::_check_block_types(BlockNode *p_block) {
lv->datatype = _resolve_type(lv->datatype, lv->line);
_mark_line_as_safe(lv->line);
last_var_assign = lv->assign;
if (lv->assign) {
DataType assign_type = _reduce_node_type(lv->assign);
#ifdef DEBUG_ENABLED
if (assign_type.has_type && assign_type.kind == DataType::BUILTIN && assign_type.builtin_type == Variant::NIL) {
if (lv->assign->type == Node::TYPE_OPERATOR) {
OperatorNode *call = static_cast<OperatorNode *>(lv->assign);
if (call->op == OperatorNode::OP_CALL || call->op == OperatorNode::OP_PARENT_CALL) {
_add_warning(GDScriptWarning::VOID_ASSIGNMENT, lv->line, _find_function_name(call));
}
}
}
if (lv->datatype.has_type && assign_type.may_yield && lv->assign->type == Node::TYPE_OPERATOR) {
_add_warning(GDScriptWarning::FUNCTION_MAY_YIELD, lv->line, _find_function_name(static_cast<OperatorNode *>(lv->assign)));
}
#endif // DEBUG_ENABLED
if (!_is_type_compatible(lv->datatype, assign_type)) {
// Try supertype test
if (_is_type_compatible(assign_type, lv->datatype)) {
@ -7329,6 +7573,11 @@ void GDScriptParser::_check_block_types(BlockNode *p_block) {
lv->assign = convert_call;
lv->assign_op->arguments.write[1] = convert_call;
#ifdef DEBUG_ENABLED
if (lv->datatype.builtin_type == Variant::INT && assign_type.builtin_type == Variant::REAL) {
_add_warning(GDScriptWarning::NARROWING_CONVERSION, lv->line);
}
#endif // DEBUG_ENABLED
}
}
if (lv->datatype.infer_type) {
@ -7343,15 +7592,6 @@ void GDScriptParser::_check_block_types(BlockNode *p_block) {
_mark_line_as_unsafe(lv->line);
}
}
last_var_assign = lv->assign;
// TODO: Make a warning
/*
if (lv->assignments == 0) {
_set_error("Variable '" + String(lv->name) + "' is never assigned.", lv->line);
return;
}
*/
} break;
case Node::TYPE_OPERATOR: {
OperatorNode *op = static_cast<OperatorNode *>(statement);
@ -7417,6 +7657,19 @@ void GDScriptParser::_check_block_types(BlockNode *p_block) {
} else {
rh_type = _reduce_node_type(op->arguments[1]);
}
#ifdef DEBUG_ENABLED
if (rh_type.has_type && rh_type.kind == DataType::BUILTIN && rh_type.builtin_type == Variant::NIL) {
if (op->arguments[1]->type == Node::TYPE_OPERATOR) {
OperatorNode *call = static_cast<OperatorNode *>(op->arguments[1]);
if (call->op == OperatorNode::OP_CALL || call->op == OperatorNode::OP_PARENT_CALL) {
_add_warning(GDScriptWarning::VOID_ASSIGNMENT, op->line, _find_function_name(call));
}
}
}
if (lh_type.has_type && rh_type.may_yield && op->arguments[1]->type == Node::TYPE_OPERATOR) {
_add_warning(GDScriptWarning::FUNCTION_MAY_YIELD, op->line, _find_function_name(static_cast<OperatorNode *>(op->arguments[1])));
}
#endif // DEBUG_ENABLED
if (!_is_type_compatible(lh_type, rh_type)) {
// Try supertype test
@ -7447,6 +7700,11 @@ void GDScriptParser::_check_block_types(BlockNode *p_block) {
convert_call->arguments.push_back(tgt_type);
op->arguments.write[1] = convert_call;
#ifdef DEBUG_ENABLED
if (lh_type.builtin_type == Variant::INT && rh_type.builtin_type == Variant::REAL) {
_add_warning(GDScriptWarning::NARROWING_CONVERSION, op->line);
}
#endif // DEBUG_ENABLED
}
}
if (!rh_type.has_type && (op->op != OperatorNode::OP_ASSIGN || lh_type.has_type || op->arguments[0]->type == Node::TYPE_OPERATOR)) {
@ -7456,15 +7714,29 @@ void GDScriptParser::_check_block_types(BlockNode *p_block) {
case OperatorNode::OP_CALL:
case OperatorNode::OP_PARENT_CALL: {
_mark_line_as_safe(op->line);
_reduce_function_call_type(op);
DataType func_type = _reduce_function_call_type(op);
#ifdef DEBUG_ENABLED
if (func_type.has_type && (func_type.kind != DataType::BUILTIN || func_type.builtin_type != Variant::NIL)) {
// Figure out function name for warning
String func_name = _find_function_name(op);
if (func_name.empty()) {
func_name == "<undetected name>";
}
_add_warning(GDScriptWarning::RETURN_VALUE_DISCARDED, op->line, func_name);
}
#endif // DEBUG_ENABLED
if (error_set) return;
} break;
case OperatorNode::OP_YIELD: {
_mark_line_as_safe(op->line);
_reduce_node_type(op);
} break;
default: {
_mark_line_as_safe(op->line);
_reduce_node_type(op); // Test for safety anyway
// TODO: Make this a warning
/*_set_error("Standalone expression, nothing is done in this line.", statement->line);
return; */
#ifdef DEBUG_ENABLED
_add_warning(GDScriptWarning::STANDALONE_EXPRESSION, statement->line);
#endif // DEBUG_ENABLED
}
}
} break;
@ -7531,9 +7803,9 @@ void GDScriptParser::_check_block_types(BlockNode *p_block) {
default: {
_mark_line_as_safe(statement->line);
_reduce_node_type(statement); // Test for safety anyway
// TODO: Make this a warning
/* _set_error("Standalone expression, nothing is done in this line.", statement->line);
return; */
#ifdef DEBUG_ENABLED
_add_warning(GDScriptWarning::STANDALONE_EXPRESSION, statement->line);
#endif // DEBUG_ENABLED
}
}
}
@ -7545,6 +7817,18 @@ void GDScriptParser::_check_block_types(BlockNode *p_block) {
current_block = p_block;
if (error_set) return;
}
#ifdef DEBUG_ENABLED
// Warnings check
for (Map<StringName, LocalVarNode *>::Element *E = p_block->variables.front(); E; E = E->next()) {
LocalVarNode *lv = E->get();
if (lv->usages == 0) {
_add_warning(GDScriptWarning::UNUSED_VARIABLE, lv->line, lv->name);
} else if (lv->assignments == 0) {
_add_warning(GDScriptWarning::UNASSIGNED_VARIABLE, lv->line, lv->name);
}
}
#endif // DEBUG_ENABLED
}
void GDScriptParser::_set_error(const String &p_error, int p_line, int p_column) {
@ -7558,6 +7842,56 @@ void GDScriptParser::_set_error(const String &p_error, int p_line, int p_column)
error_set = true;
}
#ifdef DEBUG_ENABLED
void GDScriptParser::_add_warning(int p_code, int p_line, const String &p_symbol1, const String &p_symbol2, const String &p_symbol3, const String &p_symbol4) {
Vector<String> symbols;
if (!p_symbol1.empty()) {
symbols.push_back(p_symbol1);
}
if (!p_symbol2.empty()) {
symbols.push_back(p_symbol2);
}
if (!p_symbol3.empty()) {
symbols.push_back(p_symbol3);
}
if (!p_symbol4.empty()) {
symbols.push_back(p_symbol4);
}
_add_warning(p_code, p_line, symbols);
}
void GDScriptParser::_add_warning(int p_code, int p_line, const Vector<String> &p_symbols) {
if (tokenizer->is_ignoring_warnings() || !GLOBAL_GET("debug/gdscript/warnings/enable").booleanize()) {
return;
}
String warn_name = GDScriptWarning::get_name_from_code((GDScriptWarning::Code)p_code).to_lower();
if (tokenizer->get_warning_global_skips().has(warn_name)) {
return;
}
if (!GLOBAL_GET("debug/gdscript/warnings/" + warn_name)) {
return;
}
GDScriptWarning warn;
warn.code = (GDScriptWarning::Code)p_code;
warn.symbols = p_symbols;
warn.line = p_line == -1 ? tokenizer->get_token_line() : p_line;
List<GDScriptWarning>::Element *before = NULL;
for (List<GDScriptWarning>::Element *E = warnings.front(); E; E = E->next()) {
if (E->get().line > warn.line) {
break;
}
before = E;
}
if (before) {
warnings.insert_after(before, warn);
} else {
warnings.push_front(warn);
}
}
#endif // DEBUG_ENABLED
String GDScriptParser::get_error() const {
return error;
@ -7624,6 +7958,37 @@ Error GDScriptParser::_parse(const String &p_base_path) {
return ERR_PARSE_ERROR;
}
#ifdef DEBUG_ENABLED
// Resolve warning ignores
Vector<Pair<int, String> > warning_skips = tokenizer->get_warning_skips();
bool warning_is_error = GLOBAL_GET("debug/gdscript/warnings/treat_warnings_as_errors").booleanize();
for (List<GDScriptWarning>::Element *E = warnings.front(); E;) {
GDScriptWarning &w = E->get();
int skip_index = -1;
for (int i = 0; i < warning_skips.size(); i++) {
if (warning_skips[i].first >= w.line) {
break;
}
skip_index = i;
}
List<GDScriptWarning>::Element *next = E->next();
bool erase = false;
if (skip_index != -1) {
if (warning_skips[skip_index].second == GDScriptWarning::get_name_from_code(w.code).to_lower()) {
erase = true;
}
warning_skips.remove(skip_index);
}
if (erase) {
warnings.erase(E);
} else if (warning_is_error) {
_set_error(w.get_message() + " (warning treated as error)", w.line);
return ERR_PARSE_ERROR;
}
E = next;
}
#endif // DEBUG_ENABLED
return OK;
}

View file

@ -38,6 +38,7 @@
#include "script_language.h"
struct GDScriptDataType;
struct GDScriptWarning;
class GDScriptParser {
public:
@ -57,6 +58,7 @@ public:
bool is_constant;
bool is_meta_type; // Whether the value can be used as a type
bool infer_type;
bool may_yield; // For function calls
Variant::Type builtin_type;
StringName native_type;
@ -95,6 +97,7 @@ public:
is_constant(false),
is_meta_type(false),
infer_type(false),
may_yield(false),
builtin_type(Variant::NIL),
class_type(NULL) {}
};
@ -160,6 +163,7 @@ public:
Node *expression;
OperatorNode *initial_assignment;
MultiplayerAPI::RPCMode rpc_mode;
int usages;
};
struct Constant {
Node *expression;
@ -169,6 +173,8 @@ public:
struct Signal {
StringName name;
Vector<StringName> arguments;
int emissions;
int line;
};
Vector<ClassNode *> subclasses;
@ -197,12 +203,16 @@ public:
bool _static;
MultiplayerAPI::RPCMode rpc_mode;
bool has_yield;
bool has_unreachable_code;
StringName name;
DataType return_type;
Vector<StringName> arguments;
Vector<DataType> argument_types;
Vector<Node *> default_values;
BlockNode *body;
#ifdef DEBUG_ENABLED
Vector<int> arguments_usage;
#endif // DEBUG_ENABLED
virtual DataType get_datatype() const { return return_type; }
virtual void set_datatype(const DataType &p_datatype) { return_type = p_datatype; }
@ -212,6 +222,7 @@ public:
_static = false;
rpc_mode = MultiplayerAPI::RPC_MODE_DISABLED;
has_yield = false;
has_unreachable_code = false;
}
};
@ -267,6 +278,7 @@ public:
Node *assign;
OperatorNode *assign_op;
int assignments;
int usages;
DataType datatype;
virtual DataType get_datatype() const { return datatype; }
virtual void set_datatype(const DataType &p_datatype) { datatype = p_datatype; }
@ -275,6 +287,7 @@ public:
assign = NULL;
assign_op = NULL;
assignments = 0;
usages = 0;
}
};
@ -518,6 +531,10 @@ private:
Set<int> *safe_lines;
#endif // DEBUG_ENABLED
#ifdef DEBUG_ENABLED
List<GDScriptWarning> warnings;
#endif // DEBUG_ENABLED
int pending_newline;
List<int> tab_level;
@ -550,6 +567,10 @@ private:
MultiplayerAPI::RPCMode rpc_mode;
void _set_error(const String &p_error, int p_line = -1, int p_column = -1);
#ifdef DEBUG_ENABLED
void _add_warning(int p_code, int p_line = -1, const String &p_symbol1 = String(), const String &p_symbol2 = String(), const String &p_symbol3 = String(), const String &p_symbol4 = String());
void _add_warning(int p_code, int p_line, const Vector<String> &p_symbols);
#endif // DEBUG_ENABLED
bool _recover_from_completion();
bool _parse_arguments(Node *p_parent, Vector<Node *> &p_args, bool p_static, bool p_can_codecomplete = false);
@ -605,6 +626,9 @@ public:
String get_error() const;
int get_error_line() const;
int get_error_column() const;
#ifdef DEBUG_ENABLED
const List<GDScriptWarning> &get_warnings() const { return warnings; }
#endif // DEBUG_ENABLED
Error parse(const String &p_code, const String &p_base_path = "", bool p_just_validate = false, const String &p_self_path = "", bool p_for_completion = false, Set<int> *r_safe_lines = NULL);
Error parse_bytecode(const Vector<uint8_t> &p_bytecode, const String &p_base_path = "", const String &p_self_path = "");

View file

@ -526,8 +526,13 @@ void GDScriptTokenizerText::_advance() {
return;
}
case '#': { // line comment skip
#ifdef DEBUG_ENABLED
String comment;
#endif // DEBUG_ENABLED
while (GETCHAR(0) != '\n') {
#ifdef DEBUG_ENABLED
comment += GETCHAR(0);
#endif // DEBUG_ENABLED
code_pos++;
if (GETCHAR(0) == 0) { //end of file
//_make_error("Unterminated Comment");
@ -535,6 +540,17 @@ void GDScriptTokenizerText::_advance() {
return;
}
}
#ifdef DEBUG_ENABLED
if (comment.begins_with("#warning-ignore:")) {
String code = comment.get_slice(":", 1);
warning_skips.push_back(Pair<int, String>(line, code.strip_edges().to_lower()));
} else if (comment.begins_with("#warning-ignore-all:")) {
String code = comment.get_slice(":", 1);
warning_global_skips.insert(code.strip_edges().to_lower());
} else if (comment.strip_edges() == "#warnings-disable") {
ignore_warnings = true;
}
#endif // DEBUG_ENABLED
INCPOS(1);
column = 1;
line++;
@ -1045,6 +1061,9 @@ void GDScriptTokenizerText::set_code(const String &p_code) {
column = 1; //the same holds for columns
tk_rb_pos = 0;
error_flag = false;
#ifdef DEBUG_ENABLED
ignore_warnings = false;
#endif // DEBUG_ENABLED
last_error = "";
for (int i = 0; i < MAX_LOOKAHEAD + 1; i++)
_advance();

View file

@ -31,6 +31,7 @@
#ifndef GDSCRIPT_TOKENIZER_H
#define GDSCRIPT_TOKENIZER_H
#include "core/pair.h"
#include "gdscript_functions.h"
#include "string_db.h"
#include "ustring.h"
@ -171,6 +172,11 @@ public:
virtual int get_token_line_indent(int p_offset = 0) const = 0;
virtual String get_token_error(int p_offset = 0) const = 0;
virtual void advance(int p_amount = 1) = 0;
#ifdef DEBUG_ENABLED
virtual const Vector<Pair<int, String> > &get_warning_skips() const = 0;
virtual const Set<String> &get_warning_global_skips() const = 0;
virtual const bool is_ignoring_warnings() const = 0;
#endif // DEBUG_ENABLED
virtual ~GDScriptTokenizer(){};
};
@ -190,6 +196,7 @@ class GDScriptTokenizerText : public GDScriptTokenizer {
union {
Variant::Type vtype; //for type types
GDScriptFunctions::Function func; //function for built in functions
int warning_code; //for warning skip
};
int line, col;
TokenData() {
@ -217,6 +224,11 @@ class GDScriptTokenizerText : public GDScriptTokenizer {
int tk_rb_pos;
String last_error;
bool error_flag;
#ifdef DEBUG_ENABLED
Vector<Pair<int, String> > warning_skips;
Set<String> warning_global_skips;
bool ignore_warnings;
#endif // DEBUG_ENABLED
void _advance();
@ -232,6 +244,11 @@ public:
virtual const Variant &get_token_constant(int p_offset = 0) const;
virtual String get_token_error(int p_offset = 0) const;
virtual void advance(int p_amount = 1);
#ifdef DEBUG_ENABLED
virtual const Vector<Pair<int, String> > &get_warning_skips() const { return warning_skips; }
virtual const Set<String> &get_warning_global_skips() const { return warning_global_skips; }
virtual const bool is_ignoring_warnings() const { return ignore_warnings; }
#endif // DEBUG_ENABLED
};
class GDScriptTokenizerBuffer : public GDScriptTokenizer {
@ -265,6 +282,11 @@ public:
virtual const Variant &get_token_constant(int p_offset = 0) const;
virtual String get_token_error(int p_offset = 0) const;
virtual void advance(int p_amount = 1);
#ifdef DEBUG_ENABLED
virtual const Vector<Pair<int, String> > &get_warning_skips() const { return Vector<Pair<int, String> >(); }
virtual const Set<String> &get_warning_global_skips() const { return Set<String>(); }
virtual const bool is_ignoring_warnings() const { return true; }
#endif // DEBUG_ENABLED
GDScriptTokenizerBuffer();
};

View file

@ -292,7 +292,7 @@ public:
virtual Ref<Script> get_template(const String &p_class_name, const String &p_base_class_name) const;
virtual bool is_using_templates();
virtual void make_template(const String &p_class_name, const String &p_base_class_name, Ref<Script> &p_script);
/* TODO */ virtual bool validate(const String &p_script, int &r_line_error, int &r_col_error, String &r_test_error, const String &p_path, List<String> *r_functions, Set<int> *r_safe_lines = NULL) const { return true; }
/* TODO */ virtual bool validate(const String &p_script, int &r_line_error, int &r_col_error, String &r_test_error, const String &p_path, List<String> *r_functions, List<ScriptLanguage::Warning> *r_warnings = NULL, Set<int> *r_safe_lines = NULL) const { return true; }
virtual String validate_path(const String &p_path) const;
virtual Script *create_script() const;
virtual bool has_named_classes() const;

View file

@ -2415,7 +2415,7 @@ void VisualScriptLanguage::make_template(const String &p_class_name, const Strin
script->set_instance_base_type(p_base_class_name);
}
bool VisualScriptLanguage::validate(const String &p_script, int &r_line_error, int &r_col_error, String &r_test_error, const String &p_path, List<String> *r_functions, Set<int> *r_safe_lines) const {
bool VisualScriptLanguage::validate(const String &p_script, int &r_line_error, int &r_col_error, String &r_test_error, const String &p_path, List<String> *r_functions, List<ScriptLanguage::Warning> *r_warnings, Set<int> *r_safe_lines) const {
return false;
}

View file

@ -564,7 +564,7 @@ public:
virtual Ref<Script> get_template(const String &p_class_name, const String &p_base_class_name) const;
virtual bool is_using_templates();
virtual void make_template(const String &p_class_name, const String &p_base_class_name, Ref<Script> &p_script);
virtual bool validate(const String &p_script, int &r_line_error, int &r_col_error, String &r_test_error, const String &p_path = "", List<String> *r_functions = NULL, Set<int> *r_safe_lines = NULL) const;
virtual bool validate(const String &p_script, int &r_line_error, int &r_col_error, String &r_test_error, const String &p_path = "", List<String> *r_functions = NULL, List<ScriptLanguage::Warning> *r_warnings = NULL, Set<int> *r_safe_lines = NULL) const;
virtual Script *create_script() const;
virtual bool has_named_classes() const;
virtual bool supports_builtin_mode() const;