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 @@
-
+
@@ -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
)