diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml new file mode 100644 index 0000000..4d0f39e --- /dev/null +++ b/.pre-commit-hooks.yaml @@ -0,0 +1,6 @@ +- id: vala-lint + name: Vala-Lint + language: docker_image + entry: --entrypoint /usr/bin/io.elementary.vala-lint valalang/lint:latest --fix + description: Check Vala code files for code-style errors. + files: \.vala$ diff --git a/.valalintignore b/.valalintignore new file mode 100644 index 0000000..6e6ddfd --- /dev/null +++ b/.valalintignore @@ -0,0 +1,3 @@ +po +data +build diff --git a/Dockerfile b/Dockerfile index c1fe4df..3e1ba38 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,7 +9,7 @@ RUN mkdir -p /opt/vala-lint-portable COPY . /opt/vala-lint RUN apt-get update \ - && apt-get install -y --no-install-recommends gcc libvala-dev valac meson\ + && apt-get install -y gcc libjson-glib-dev libvala-dev valac meson\ && cd /opt/vala-lint \ && meson build --prefix=/usr \ && cd build \ @@ -23,7 +23,7 @@ ENV DEBIAN_FRONTEND=noninteractive COPY --from=0 /opt/vala-lint-portable / RUN apt-get update \ - && apt-get install -y --no-install-recommends libvala-dev gio-2.0 \ + && apt-get install -y --no-install-recommends libvala-dev gio-2.0 libjson-glib-1.0-0 \ && mkdir -p /app \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* diff --git a/README.md b/README.md index d31ea55..413d01e 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Publish - Dockerhub + Dockerhub @@ -123,8 +123,46 @@ If you want to skip an entire file, you can use at the beginning of the file. +### Ignoring Files +You can disable linting of files matching certain patterns by creating a `.valalintignore` text file. +If the file is created in your home directory it will be applied globally. +The patterns must be like those used for globbing filenames. Type `man glob` into a terminal +for further information. + +If the file is created in the root directory of your project it will apply only to that project and +will override any global setting. +If no `.valalintignore` file is found then the patterns in any `.gitignore` file found in the +project root are ignored. + +The format of the file is one pattern per line. Usually you would want to ignore certain folders like + +```vala +build +po +data +``` + +Note that if you do provide a `.valalintignore` file, you must repeat any patterns in a `.gitignore` +file that you do not want to lint. + +Although `vala-lint` ignores non-Vala files, ignoring large directories significantly speeds up linting. + +You may also ignore specific kinds of `.vala` files like +```vala +~*.vala +``` ### Docker and Continuous Integration Vala-Lint is primarily intended to be used in Continuous Integration (CI). It's available in a convenient, always up-to-date Docker container `valalang/lint:latest` hosted on Docker Hub. docker run -v "$PWD":/app valalang/lint:latest + +### pre-commit Integration +You can use Vala-Lint via [pre-commit](https://pre-commit.com/) by adding the following entry to your `.pre-commit-config.yaml`: + +```yaml +- repo: https://github.com/vala-lang/vala-lint + rev: master + hooks: + - id: vala-lint +``` diff --git a/lib/Fixer.vala b/lib/Fixer.vala index 1ec03d1..51f948a 100644 --- a/lib/Fixer.vala +++ b/lib/Fixer.vala @@ -21,9 +21,9 @@ public class ValaLint.Fixer : Object { public void apply_fixes_for_file (File file, ref Vala.ArrayList mistakes) throws Error, IOError { - var filename = file.get_path (); - string contents; - FileUtils.get_contents (filename, out contents); + uint8[] contents_data; + file.load_contents (null, out contents_data, null); + string contents = (string) (owned) contents_data; var remaining_mistakes = new Vala.ArrayList ((a, b) => a.equal_to (b)); @@ -46,6 +46,6 @@ public class ValaLint.Fixer : Object { return a.begin.line - b.begin.line; }); - FileUtils.set_contents (filename, contents); + file.replace_contents (contents.data, null, false, FileCreateFlags.NONE, null); } } diff --git a/meson.build b/meson.build index 5126225..6059667 100644 --- a/meson.build +++ b/meson.build @@ -13,6 +13,7 @@ gio_dep = dependency('gio-2.0', version: '>=2.56.4') posix_dep = valac.find_library('posix') libvala_required_version = '>= 0.40.4' libvala_dep = dependency('libvala-@0@'.format(libvala_version), version: libvala_required_version) +json_dep = dependency('json-glib-1.0') subdir('lib') subdir('src') diff --git a/src/Application.vala b/src/Application.vala index 3c7b292..eeec7ff 100644 --- a/src/Application.vala +++ b/src/Application.vala @@ -28,7 +28,11 @@ public class ValaLint.Application : GLib.Application { private static bool exit_with_zero = false; private static bool generate_config_file = false; private static bool auto_fix = false; + private static bool json_output = false; private static string? config_file = null; + private static string ignore_pattern_list = ""; + private static int fnmatch_flags = Posix.FNM_EXTMATCH | Posix.FNM_PERIOD | Posix.FNM_PATHNAME; + private static File root_dir; private ApplicationCommandLine application_command_line; @@ -47,6 +51,8 @@ public class ValaLint.Application : GLib.Application { "Generate a sample configuration file with default values." }, { "fix", 'f', 0, OptionArg.NONE, ref auto_fix, "Fix any auto-fixable mistakes." }, + { "json-output", 'j', 0, OptionArg.NONE, ref json_output, + "Output in JSON format." }, { null } }; @@ -99,17 +105,84 @@ public class ValaLint.Application : GLib.Application { } this.application_command_line = command_line; + /* Get ignore patterns. Ignore patterns are glob patterns relative to the scanned directory */ + string[] ignore_patterns = {}; + string ignore_root = lint_directory != null ? lint_directory : args[1]; + if (ignore_root == null) { + ignore_root = tmp[1] = "."; + tmp.length += 1; + } + if (ignore_root.has_suffix (Path.DIR_SEPARATOR_S)) { + ignore_root = ignore_root[0 : -1]; + } + + var ignore_filepath = Path.build_filename ( + ignore_root, ".valalintignore" + ); + + var fallback_path = Path.build_filename ( + Environment.get_home_dir (), ".valalintignore" + ); + + if (!Path.is_absolute (ignore_filepath)) { + var ignore_file = File.new_for_commandline_arg_and_cwd ( + ignore_filepath, application_command_line.get_cwd () + ); + + ignore_filepath = ignore_file.get_path (); + } + + debug ("Absolute ignore file path %s", ignore_filepath); + + string contents; + size_t size = 0; + try { + FileUtils.get_contents (ignore_filepath, out contents, out size); + } catch (Error e) { + debug ("Error loading ignore file contents: %s", e.message); + contents = ""; + size = 0; + } + + if (size == 0) { + try { + FileUtils.get_contents (fallback_path, out contents, out size); + } catch (Error e) { + debug ("Error loading ignore file contents: %s", e.message); + contents = ""; + size = 0; + } + } + + // Basic sanity check + if (size > 10000UL) { + ///TRANSLATORS %s is a placeholder for a file path + command_line.print (_("%s is too large and will not be used.") + "\n", ignore_filepath); + } else if (size > 0) { + var ignore_split = contents.split ("\n", -1); + foreach (string ignore in ignore_split) { + if (ignore != "" && ignore.length <= 255) { + // Remove any trailing dir separator + if (ignore.has_suffix (Path.DIR_SEPARATOR_S)) { + ignore = ignore[0 : -1]; + } + + ignore_patterns += ignore; + } else if (ignore.length > 255) { + command_line.print (_("The pattern %s is too long and will not be used.") + "\n", ignore); + } + } + } - /* 1. Get list of files */ + /* Get list of files */ var file_data_list = new Vala.ArrayList (); try { string[] file_name_list = tmp[1:tmp.length]; if (lint_directory != null) { - // command_line.print (_("The directory flag is depreceated, just omit the flag for future versions.") + "\n"); file_name_list += lint_directory; } - file_data_list = get_files (command_line, file_name_list); + file_data_list = get_files (command_line, file_name_list, ignore_patterns); } catch (Error e) { critical (_("Error: %s") + "\n", e.message); } @@ -133,10 +206,17 @@ public class ValaLint.Application : GLib.Application { } /* 4. Print mistakes */ - print_mistakes (file_data_list); + bool has_errors = false; + if (json_output) { + has_errors = print_mistakes_json (file_data_list); + } else { + has_errors = print_mistakes (file_data_list); + } - if (exit_with_zero) { + if (exit_with_zero || !has_errors) { return 0; + } else { + return 1; } foreach (FileData file_data in file_data_list) { @@ -146,12 +226,23 @@ public class ValaLint.Application : GLib.Application { } } } + return 0; } - Vala.ArrayList get_files (ApplicationCommandLine command_line, string[] patterns) throws Error, IOError { - var result = new Vala.ArrayList (); + Vala.ArrayList get_files ( + ApplicationCommandLine command_line, string[] patterns, string[] ignore_patterns ) throws Error, IOError { + foreach (string pattern in ignore_patterns) { + ignore_pattern_list += ("|" + pattern); + } + + debug ("Ignore pattern list: %s", ignore_pattern_list); + if (ignore_pattern_list.length > 0) { + ignore_pattern_list = "+(" + ignore_pattern_list[1 : ignore_pattern_list.length] + ")"; + } + + var result = new Vala.ArrayList (); foreach (string pattern in patterns) { var matcher = Posix.Glob (); @@ -170,6 +261,7 @@ public class ValaLint.Application : GLib.Application { break; case FileType.DIRECTORY: + root_dir = file; foreach (File f in get_files_from_directory (file)) { string name = path + file.get_relative_path (f); result.add ({ f, name, new Vala.ArrayList () }); @@ -181,57 +273,75 @@ public class ValaLint.Application : GLib.Application { } } } + return result; } Vala.ArrayList get_files_from_directory (File dir) throws Error, IOError { var files = new Vala.ArrayList (); - FileEnumerator enumerator = dir.enumerate_children (FileAttribute.STANDARD_NAME, 0, null); + FileEnumerator enumerator = dir.enumerate_children ( + FileAttribute.STANDARD_NAME + "," + + FileAttribute.STANDARD_IS_HIDDEN + "," + + FileAttribute.STANDARD_TYPE, 0, null + ); var info = enumerator.next_file (null); while (info != null) { string child_name = info.get_name (); - File child_file = dir.resolve_relative_path (child_name); - if (info.get_file_type () == FileType.DIRECTORY) { - if (!info.get_is_hidden ()) { + var child_file = dir.resolve_relative_path (child_name); + var rel_path = root_dir.get_relative_path (child_file); + if (!info.get_is_hidden () && + Posix.fnmatch (ignore_pattern_list, rel_path, fnmatch_flags) != 0) { + + if (info.get_file_type () == FileType.DIRECTORY) { var sub_files = get_files_from_directory (child_file); files.add_all (sub_files); - } - } else if (info.get_file_type () == FileType.REGULAR) { - /* Check only .vala files */ - if (child_name.has_suffix (".vala")) { + } else if (info.get_file_type () == FileType.REGULAR && + child_name.has_suffix (".vala")) { + files.add (child_file); + } else { + debug ("%s ignored - not regular Vala file", child_name); } + } else { + debug ("%s ignored - hidden or matches ignore pattern", rel_path); } + info = enumerator.next_file (null); } + return files; } - void print_mistakes (Vala.ArrayList file_data_list) { + bool print_mistakes (Vala.ArrayList file_data_list) { + int num_errors = 0; + int num_warnings = 0; + foreach (FileData file_data in file_data_list) { if (!file_data.mistakes.is_empty) { application_command_line.print ("\x001b[1m\x001b[4m" + "%s" + "\x001b[0m\n", file_data.name); foreach (FormatMistake mistake in file_data.mistakes) { - string color_state = "%-5s"; - string mistakes_end = ""; - if (print_mistakes_end) { - mistakes_end = "-%5i.%-3i".printf (mistake.end.line, mistake.end.column); - } - switch (mistake.check.state) { case ERROR: - color_state = "\033[1;31m" + color_state + "\033[0m"; // red + num_errors++; break; case WARN: - color_state = "\033[1;33m" + color_state + "\033[0m"; // yellow + num_warnings++; break; default: break; } + string color_state = "%-5s"; + string mistakes_end = ""; + if (print_mistakes_end) { + mistakes_end = "-%5i.%-3i".printf (mistake.end.line, mistake.end.column); + } + + color_state = apply_color_for_state (color_state, mistake.check.state); + application_command_line.print ( "\x001b[0m%5i.%-3i %s " + color_state + " %-45s \033[2m%s\033[0m\n", mistake.begin.line, @@ -244,6 +354,100 @@ public class ValaLint.Application : GLib.Application { } } } + + if (num_errors + num_warnings == 0) { + application_command_line.print ("\033[1;32m" + _("No mistakes found") + "\033[0m\n"); + return false; + } + + string summary = ("\n" + _("%d %s, %d %s") + "\n").printf ( + num_errors, + num_errors == 1 ? _("error") : _("errors"), + num_warnings, + num_warnings == 1 ? _("warning") : _("warnings") + ); + if (num_errors > 0) { + application_command_line.print (apply_color_for_state (summary, Config.State.ERROR)); + } else { + application_command_line.print (apply_color_for_state (summary, Config.State.WARN)); + } + + return num_errors > 0; + } + + bool print_mistakes_json (Vala.ArrayList file_data_list) { + int num_errors = 0; + int num_warnings = 0; + Json.Builder builder = new Json.Builder (); + builder.begin_object (); + builder.set_member_name ("mistakes"); + builder.begin_array (); + + foreach (FileData file_data in file_data_list) { + if (!file_data.mistakes.is_empty) { + foreach (FormatMistake mistake in file_data.mistakes) { + switch (mistake.check.state) { + case ERROR: + num_errors++; + break; + + case WARN: + num_warnings++; + break; + + default: + break; + } + + builder.begin_object (); + builder.set_member_name ("filename"); + builder.add_string_value (file_data.name); + builder.set_member_name ("line"); + builder.add_int_value (mistake.begin.line); + builder.set_member_name ("column"); + builder.add_int_value (mistake.begin.column); + if (print_mistakes_end) { + builder.set_member_name ("endLine"); + builder.add_int_value (mistake.end.line); + builder.set_member_name ("endColumn"); + builder.add_int_value (mistake.end.column); + } + builder.set_member_name ("level"); + builder.add_string_value (mistake.check.state.to_string ()); + builder.set_member_name ("message"); + builder.add_string_value (mistake.mistake); + builder.set_member_name ("ruleId"); + builder.add_string_value (mistake.check.title); + builder.end_object (); + } + } + } + builder.end_array (); + builder.end_object (); + + Json.Generator generator = new Json.Generator (); + Json.Node root = builder.get_root (); + generator.set_root (root); + application_command_line.print ("%s\n", generator.to_data (null)); + + if (num_errors + num_warnings == 0) { + return false; + } + + return num_errors > 0; + } + + string apply_color_for_state (string str, Config.State state) { + switch (state) { + case ERROR: + return "\033[1;31m" + str + "\033[0m"; // red + + case WARN: + return "\033[1;33m" + str + "\033[0m"; // yellow + + default: + return str; + } } public static int main (string[] args) { diff --git a/src/meson.build b/src/meson.build index ad57c18..e143a76 100644 --- a/src/meson.build +++ b/src/meson.build @@ -8,7 +8,8 @@ vala_lint = executable( app_files, dependencies: [ vala_linter_dep, - posix_dep + posix_dep, + json_dep ], install : true )