diff --git a/README.md b/README.md index 26586ce..fef757e 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,7 @@ block-opening-brace-space-before=error double-semicolon=error double-spaces=error ellipsis=error +indentation=error line-length=warn naming-convention=error no-space=error @@ -89,6 +90,9 @@ unnecessary-string-template=error [Disabler] disable-by-inline-comments=true +[indentation] +indent-size=4 + [line-length] max-line-length=120 ignore-comments=true diff --git a/lib/Checks/IndentationCheck.vala b/lib/Checks/IndentationCheck.vala new file mode 100644 index 0000000..ca8a8e7 --- /dev/null +++ b/lib/Checks/IndentationCheck.vala @@ -0,0 +1,203 @@ +/* + * Copyright (c) 2019 elementary LLC. (https://github.com/elementary/vala-lint) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + */ + +public class ValaLint.Checks.IndentationCheck : Check { + const string MESSAGE = _("Indentation is %d but should %d"); + public int indent_size; + + public IndentationCheck () { + Object ( + title: _("indentation"), + description: _("Checks for correct indentation") + ); + + state = Config.get_state (title); + indent_size = Config.get_integer (title, "indent-size"); + } + + public override void check (Vala.ArrayList parse_result, + ref Vala.ArrayList mistake_list) { + + } + + public bool is_node_in_namespace (Vala.CodeNode n, Vala.Namespace ns) { + if (n is Vala.Class) { + return ns.get_classes ().contains ((Vala.Class)n); + } else if (n is Vala.Interface) { + return ns.get_interfaces ().contains ((Vala.Interface)n); + } else if (n is Vala.Struct) { + return ns.get_structs ().contains ((Vala.Struct)n); + } else if (n is Vala.Enum) { + return ns.get_enums ().contains ((Vala.Enum)n); + } else if (n is Vala.ErrorDomain) { + return ns.get_error_domains ().contains ((Vala.ErrorDomain)n); + } else if (n is Vala.Delegate) { + return ns.get_delegates ().contains ((Vala.Delegate)n); + } else if (n is Vala.Constant) { + return ns.get_constants ().contains ((Vala.Constant)n); + } else if (n is Vala.Field) { + return ns.get_fields ().contains ((Vala.Field)n); + } else if (n is Vala.Method) { + return ns.get_methods ().contains ((Vala.Method)n); + } else if (n is Vala.Namespace) { + return ns.get_namespaces ().contains ((Vala.Namespace)n); + } + + return false; + } + + public bool is_explicit_namespace (Vala.Namespace ns) { + foreach (var n in ns.get_classes ()) { + if (n.source_reference.begin.line == ns.source_reference.begin.line) { + return false; + } + } + + foreach (var n in ns.get_interfaces ()) { + if (n.source_reference.begin.line == ns.source_reference.begin.line) { + return false; + } + } + + foreach (var n in ns.get_structs ()) { + if (n.source_reference.begin.line == ns.source_reference.begin.line) { + return false; + } + } + + foreach (var n in ns.get_enums ()) { + if (n.source_reference.begin.line == ns.source_reference.begin.line) { + return false; + } + } + + foreach (var n in ns.get_error_domains ()) { + if (n.source_reference.begin.line == ns.source_reference.begin.line) { + return false; + } + } + + foreach (var n in ns.get_delegates ()) { + if (n.source_reference.begin.line == ns.source_reference.begin.line) { + return false; + } + } + + foreach (var n in ns.get_constants ()) { + if (n.source_reference.begin.line == ns.source_reference.begin.line) { + return false; + } + } + + foreach (var n in ns.get_fields ()) { + if (n.source_reference.begin.line == ns.source_reference.begin.line) { + return false; + } + } + + foreach (var n in ns.get_methods ()) { + if (n.source_reference.begin.line == ns.source_reference.begin.line) { + return false; + } + } + + foreach (var n in ns.get_namespaces ()) { + if (n.source_reference.begin.line == ns.source_reference.begin.line) { + return false; + } + } + + return true; + } + + public bool is_else_if_statement (Vala.IfStatement s) { + var b = s.parent_node; + if (b != null && b.parent_node is Vala.IfStatement) { + Vala.IfStatement if_statement = (Vala.IfStatement)b.parent_node; + if (if_statement.false_statement == b && b.source_reference.begin.line == s.source_reference.begin.line) { + return true; + } + } + + return false; + } + + public void check_block (Vala.Block b, int level, ref Vala.ArrayList mistake_list) { + if (state == Config.State.OFF) { + return; + } + + foreach (var s in b.get_statements ()) { + int offset = 0; + + if (s is Vala.IfStatement && is_else_if_statement ((Vala.IfStatement)s)) { + offset -= 1; + } + + var n = b.parent_node; + while (n != null) { + if (n is Vala.LockStatement) { + offset += 1; + } + + n = n.parent_node; + } + + if (s.parent_node == null || s.parent_node.source_reference.begin.line != s.source_reference.begin.line) { + check_line (s.source_reference, level + offset, ref mistake_list); + } + } + } + + public void check_symbol (Vala.Symbol s, int level, ref Vala.ArrayList mistake_list) { + if (state == Config.State.OFF) { + return; + } + + if (s.parent_node == null || s.parent_node.source_reference.begin.line != s.source_reference.begin.line) { + check_line (s.source_reference, level, ref mistake_list); + } + } + + private void check_line (Vala.SourceReference loc, int level, ref Vala.ArrayList mistake_list) { + Vala.SourceLocation line = loc.begin; + char* file_begin = loc.file.get_mapped_contents (); + + while (line.pos > file_begin && line.pos[0] != '\n') { + line.pos -= 1; + line.column -= 1; + } + + var first_char = line; + int indent = 0; + while (first_char.pos[0] == ' ' || first_char.pos[0] == '\n' || first_char.pos[0] == '\t') { + if (first_char.pos[0] == ' ') { + indent += 1; + } + + first_char.pos += 1; + first_char.column += 1; + } + + int indent_should = level * indent_size; + if (indent != indent_should) { + add_mistake ({ this, first_char, line, MESSAGE.printf (indent, indent_should) }, ref mistake_list); + } + } +} diff --git a/lib/Config.vala b/lib/Config.vala index 198b036..63aa7d7 100644 --- a/lib/Config.vala +++ b/lib/Config.vala @@ -48,6 +48,7 @@ public class ValaLint.Config { default_config.set_string ("Checks", "double-semicolon", State.ERROR.to_string ()); default_config.set_string ("Checks", "double-spaces", State.ERROR.to_string ()); default_config.set_string ("Checks", "ellipsis", State.ERROR.to_string ()); + default_config.set_string ("Checks", "indentation", State.ERROR.to_string ()); default_config.set_string ("Checks", "line-length", State.WARN.to_string ()); default_config.set_string ("Checks", "naming-convention", State.ERROR.to_string ()); default_config.set_string ("Checks", "no-space", State.ERROR.to_string ()); @@ -60,6 +61,8 @@ public class ValaLint.Config { default_config.set_boolean ("Disabler", "disable-by-inline-comments", true); + default_config.set_integer ("indentation", "indent-size", 4); + default_config.set_double ("line-length", "max-line-length", 120); default_config.set_boolean ("line-length", "ignore-comments", true); diff --git a/lib/Linter.vala b/lib/Linter.vala index f11e1f7..2a9c6ae 100644 --- a/lib/Linter.vala +++ b/lib/Linter.vala @@ -47,6 +47,7 @@ public class ValaLint.Linter : Object { visitor = new ValaLint.Visitor (); visitor.double_semicolon_check = new Checks.DoubleSemicolonCheck (); + visitor.indentation_check = new Checks.IndentationCheck (); visitor.ellipsis_check = new Checks.EllipsisCheck (); visitor.naming_convention_check = new Checks.NamingConventionCheck (); visitor.no_space_check = new Checks.NoSpaceCheck (); diff --git a/lib/ValaVisitor.vala b/lib/ValaVisitor.vala index a1ceff1..c6231b0 100644 --- a/lib/ValaVisitor.vala +++ b/lib/ValaVisitor.vala @@ -18,9 +18,12 @@ */ class ValaLint.Visitor : Vala.CodeVisitor { + private int level = 0; + public Vala.ArrayList mistake_list; public Checks.DoubleSemicolonCheck double_semicolon_check; + public Checks.IndentationCheck indentation_check; public Checks.EllipsisCheck ellipsis_check; public Checks.NamingConventionCheck naming_convention_check; public Checks.UnnecessaryStringTemplateCheck unnecessary_string_template_check; @@ -31,43 +34,84 @@ class ValaLint.Visitor : Vala.CodeVisitor { } public override void visit_source_file (Vala.SourceFile sf) { - sf.accept_children (this); + // Filter nodes which are already visited via Vala.Namespace + foreach (var node in sf.get_nodes ()) { + bool visit = true; + + foreach (var ns in sf.get_nodes ()) { + if (ns is Vala.Namespace && indentation_check.is_node_in_namespace (node, (Vala.Namespace)ns)) { + visit = false; + break; + } + } + + // print ("asdf: %d %d\n", node.source_reference.begin.line, (int)visit); + if (visit) { + node.accept (this); + } + } } public override void visit_namespace (Vala.Namespace ns) { /* namespace name may be null */ + indentation_check.check_symbol (ns, level, ref mistake_list); naming_convention_check.check_camel_case (ns, ref mistake_list); + int indent = indentation_check.is_explicit_namespace (ns) ? 1 : 0; + level += indent; ns.accept_children (this); + level -= indent; } public override void visit_class (Vala.Class cl) { + indentation_check.check_symbol (cl, level, ref mistake_list); naming_convention_check.check_camel_case (cl, ref mistake_list); + + level += 1; cl.accept_children (this); + level -= 1; } public override void visit_struct (Vala.Struct st) { + indentation_check.check_symbol (st, level, ref mistake_list); naming_convention_check.check_camel_case (st, ref mistake_list); + + level += 1; st.accept_children (this); + level -= 1; } public override void visit_interface (Vala.Interface iface) { + indentation_check.check_symbol (iface, level, ref mistake_list); naming_convention_check.check_camel_case (iface, ref mistake_list); + + level += 1; iface.accept_children (this); + level -= 1; } public override void visit_enum (Vala.Enum en) { + indentation_check.check_symbol (en, level, ref mistake_list); naming_convention_check.check_camel_case (en, ref mistake_list); + + level += 1; en.accept_children (this); + level -= 1; } public override void visit_enum_value (Vala.EnumValue ev) { + indentation_check.check_symbol (ev, level, ref mistake_list); naming_convention_check.check_all_caps (ev, ref mistake_list); ev.accept_children (this); } public override void visit_error_domain (Vala.ErrorDomain edomain) { + indentation_check.check_symbol (edomain, level, ref mistake_list); + naming_convention_check.check_camel_case (edomain, ref mistake_list); + + level += 1; edomain.accept_children (this); + level -= 1; } public override void visit_error_code (Vala.ErrorCode ecode) { @@ -75,37 +119,41 @@ class ValaLint.Visitor : Vala.CodeVisitor { } public override void visit_delegate (Vala.Delegate d) { + indentation_check.check_symbol (d, level, ref mistake_list); d.accept_children (this); } public override void visit_constant (Vala.Constant c) { + double_semicolon_check.check_statement (c, ref mistake_list); + indentation_check.check_symbol (c, level, ref mistake_list); naming_convention_check.check_all_caps (c, ref mistake_list); c.accept_children (this); } public override void visit_field (Vala.Field f) { + double_semicolon_check.check_statement (f, ref mistake_list); + indentation_check.check_symbol (f, level, ref mistake_list); naming_convention_check.check_underscore (f, ref mistake_list); f.accept_children (this); } public override void visit_method (Vala.Method m) { - /* method name may be null */ + /* Method name may be null */ + indentation_check.check_symbol (m, level, ref mistake_list); naming_convention_check.check_underscore (m, ref mistake_list); - no_space_check.check_list (m.get_parameters (), ref mistake_list); - /* Error types depend on the vala version. */ - // var error_types = new Vala.ArrayList (); - // m.get_error_types (error_types); - // no_space_check.check_list (error_types, ref mistake_list); - + level += 1; m.accept_children (this); + level -= 1; } public override void visit_creation_method (Vala.CreationMethod m) { + level += 1; m.accept_children (this); + level -= 1; } public override void visit_formal_parameter (Vala.Parameter p) { @@ -115,23 +163,39 @@ class ValaLint.Visitor : Vala.CodeVisitor { } public override void visit_property (Vala.Property prop) { + indentation_check.check_symbol (prop, level, ref mistake_list); + + level += 1; prop.accept_children (this); + level -= 1; } public override void visit_property_accessor (Vala.PropertyAccessor acc) { + level += 1; acc.accept_children (this); + level -= 1; } public override void visit_signal (Vala.Signal sig) { + indentation_check.check_symbol (sig, level, ref mistake_list); + sig.accept_children (this); } public override void visit_constructor (Vala.Constructor c) { + indentation_check.check_symbol (c, level, ref mistake_list); + + level += 1; c.accept_children (this); + level -= 1; } public override void visit_destructor (Vala.Destructor d) { + indentation_check.check_symbol (d, level, ref mistake_list); + + level += 1; d.accept_children (this); + level -= 1; } public override void visit_type_parameter (Vala.TypeParameter p) { @@ -147,6 +211,8 @@ class ValaLint.Visitor : Vala.CodeVisitor { } public override void visit_block (Vala.Block b) { + indentation_check.check_block (b, level, ref mistake_list); + b.accept_children (this); } @@ -168,7 +234,9 @@ class ValaLint.Visitor : Vala.CodeVisitor { public override void visit_initializer_list (Vala.InitializerList list) { no_space_check.check_list (list.get_initializers (), ref mistake_list); + level += 1; list.accept_children (this); + level -= 1; } public override void visit_expression_statement (Vala.ExpressionStatement stmt) { @@ -178,7 +246,10 @@ class ValaLint.Visitor : Vala.CodeVisitor { } public override void visit_if_statement (Vala.IfStatement stmt) { + int level_indent = indentation_check.is_else_if_statement (stmt) ? 0 : 1; + level += level_indent; stmt.accept_children (this); + level -= level_indent; } public override void visit_switch_statement (Vala.SwitchStatement stmt) { @@ -186,7 +257,9 @@ class ValaLint.Visitor : Vala.CodeVisitor { } public override void visit_switch_section (Vala.SwitchSection section) { + level += 1; section.accept_children (this); + level -= 1; } public override void visit_switch_label (Vala.SwitchLabel label) { @@ -198,19 +271,27 @@ class ValaLint.Visitor : Vala.CodeVisitor { } public override void visit_while_statement (Vala.WhileStatement stmt) { + level += 1; stmt.accept_children (this); + level -= 1; } public override void visit_do_statement (Vala.DoStatement stmt) { + level += 1; stmt.accept_children (this); + level -= 1; } public override void visit_for_statement (Vala.ForStatement stmt) { + level += 1; stmt.accept_children (this); + level -= 1; } public override void visit_foreach_statement (Vala.ForeachStatement stmt) { + level += 1; stmt.accept_children (this); + level -= 1; } public override void visit_break_statement (Vala.BreakStatement stmt) { @@ -240,7 +321,9 @@ class ValaLint.Visitor : Vala.CodeVisitor { } public override void visit_try_statement (Vala.TryStatement stmt) { + level += 1; stmt.accept_children (this); + level -= 1; } public override void visit_catch_clause (Vala.CatchClause clause) { @@ -248,7 +331,9 @@ class ValaLint.Visitor : Vala.CodeVisitor { } public override void visit_lock_statement (Vala.LockStatement stmt) { + level += 1; stmt.accept_children (this); + level -= 1; } public override void visit_unlock_statement (Vala.UnlockStatement stmt) { @@ -301,7 +386,9 @@ class ValaLint.Visitor : Vala.CodeVisitor { public override void visit_tuple (Vala.Tuple tuple) { no_space_check.check_list (tuple.get_expressions (), ref mistake_list); + level += 1; tuple.accept_children (this); + level -= 1; } public override void visit_null_literal (Vala.NullLiteral lit) { @@ -315,7 +402,9 @@ class ValaLint.Visitor : Vala.CodeVisitor { public override void visit_method_call (Vala.MethodCall expr) { no_space_check.check_list (expr.get_argument_list (), ref mistake_list); + level += 1; expr.accept_children (this); + level -= 1; } public override void visit_element_access (Vala.ElementAccess expr) { @@ -335,7 +424,9 @@ class ValaLint.Visitor : Vala.CodeVisitor { } public override void visit_object_creation_expression (Vala.ObjectCreationExpression expr) { + level += 1; expr.accept_children (this); + level -= 1; } public override void visit_sizeof_expression (Vala.SizeofExpression expr) { @@ -387,7 +478,11 @@ class ValaLint.Visitor : Vala.CodeVisitor { public override void visit_lambda_expression (Vala.LambdaExpression expr) { no_space_check.check_list (expr.get_parameters (), ref mistake_list); + bool parent_on_same_line = (expr.source_reference.begin.line != expr.parent_node.source_reference.begin.line); + int indent = (expr.statement_body != null && parent_on_same_line) ? 1 : 0; + level += indent; expr.accept_children (this); + level -= indent; } public override void visit_assignment (Vala.Assignment a) { diff --git a/lib/meson.build b/lib/meson.build index 7aa747b..c1495af 100644 --- a/lib/meson.build +++ b/lib/meson.build @@ -13,6 +13,7 @@ vala_linter_files = files( 'Checks/DoubleSemicolonCheck.vala', 'Checks/DoubleSpacesCheck.vala', 'Checks/EllipsisCheck.vala', + 'Checks/IndentationCheck.vala', 'Checks/LineLengthCheck.vala', 'Checks/NamingConventionCheck.vala', 'Checks/NoSpaceCheck.vala', diff --git a/test/FileTest.vala b/test/FileTest.vala index 0da7e18..f3d1eef 100644 --- a/test/FileTest.vala +++ b/test/FileTest.vala @@ -56,6 +56,7 @@ class FileTest : GLib.Object { int line = 0; // So that new tests can be added without changing every number... var m_warnings = new Vala.ArrayList (); m_warnings.add ({ "naming-convention", line += 4 }); + m_warnings.add ({ "indentation", line += 1 }); m_warnings.add ({ "space-before-paren", line += 2 }); m_warnings.add ({ "note", line += 1, "TODO" }); m_warnings.add ({ "note", line += 1, "TODO: Lorem ipsum" }); @@ -76,7 +77,8 @@ class FileTest : GLib.Object { m_warnings.add ({ "unnecessary-string-template", line += 1 }); m_warnings.add ({ "line-length", line += 2 }); m_warnings.add ({ "line-length", line += 1 }); - m_warnings.add ({ "trailing-newlines", line += 2 }); + m_warnings.add ({ "indentation", line += 3 }); + m_warnings.add ({ "trailing-newlines", line += 3 }); check_file_for_mistake (File.new_for_path ("../test/files/warnings.vala"), m_warnings); diff --git a/test/files/pass.vala b/test/files/pass.vala index c644d6d..8d94b49 100644 --- a/test/files/pass.vala +++ b/test/files/pass.vala @@ -1,4 +1,20 @@ -class FileTest : GLib.Object { +namespace NamespaceName { + class Test { + int a; + } +} + +class TestNamespace.FileTest : GLib.Object { + + // Property for indentation + public int test { get { return this._number; }; set; } + public int number { get { + return this._number; + } + set { + this._number = value; + } + } public static int main (string[] args) { string no_ellipsis = ".. lorem ipsum."; // This is a very long comment over the line-length limit of 120 characters... @@ -8,6 +24,56 @@ class FileTest : GLib.Object { string literal = "Lorem ipsum"; var string_template = @"$literal et al."; + // Some checks for indentation + if (true) { + var b = 3; + } else { + if (true) { + var a = 2; + + switch (a) { + case 2: { + a = 3; + break; + } + + default: + break; + } + } + } + + Bus.own_name (BusType.SESSION, "org.pantheon.greeter", BusNameOwnerFlags.NONE, + (connection) => { + if (instance == null) { + instance = new DBus (); + } + + try { + connection.register_object ("/org/pantheon/greeter", instance); + } catch (Error e) { + warning (e.message); + } + }, + () => {}, + () => warning ("Could not acquire name\n") + ); + + lock (notifications) { + notifications = null; + } + + new Thread (null, () => { + lock (notifications) { + try { + notifications = connection.get_proxy_sync ("org.freedesktop.Notifications", + "/org/freedesktop/Notifications", DBusProxyFlags.NONE); + } catch (Error e) { + notifications = null; + } + } + return null; + }); var /* comment */ string_double_space = /* */ "lorem"; return 0; diff --git a/test/files/warnings.vala b/test/files/warnings.vala index bbff8c6..b827684 100644 --- a/test/files/warnings.vala +++ b/test/files/warnings.vala @@ -2,6 +2,7 @@ class FileTest : GLib.Object { const int underscore_constant = 3; + int wrong_indentation; public static int main(string[] args) { test (); // TODO @@ -26,5 +27,9 @@ class FileTest : GLib.Object { string this_is_a_very_long_variable_name_to_get_over_the_120_character_line_length_limit = "lorem ipsum dolor amet test"; // despite the comment string this_is_a_very_long_variable_name_to_get_over_the_120_character_line_with_comment /* with comment */ = "lorem ipsum dolor amet test"; // and comment here + + if (empty_string_template == "") { + wrong_indentation = 3; + } } } \ No newline at end of file diff --git a/vala-lint.conf b/vala-lint.conf index 63a96e0..ac6c14f 100644 --- a/vala-lint.conf +++ b/vala-lint.conf @@ -3,6 +3,7 @@ block-opening-brace-space-before=error double-semicolon=error double-spaces=error ellipsis=error +indentation=error line-length=warn naming-convention=error no-space=error @@ -16,6 +17,9 @@ unnecessary-string-template=error [Disabler] disable-by-inline-comments=true +[indentation] +indent-size=4 + [line-length] max-line-length=120 ignore-comments=true