A Zig implementation of Google's IFTTT (IfThisThenThat) lint. IFCTC stands for "IF Changes Then Change".
Warning
This project isn't actively used yet. Bugs may be lurking.
Feed ifctc
a unified patch file, and it will verify that all files were
modified as specified. Files are read relative to the current working directory.
$ git diff --unified | ifctc
Recognized directives are:
-
LINT.IfChange
(orLINT.IfChange(label)
), which starts a code block. -
LINT.ThenChange(paths, ...)
, which reports an error if its contents were modified, but not the contents inpaths
.Paths may contain a
:label
, in which case the code with that label must be modified.Paths may be relative, in which case they are relative to the file which has the directive. They may also be absolute (i.e. start with
/
), in which case they are relative to the directory whereifctc
is invoked.
As a concrete example, let's say you have two files with the same constant, which must be kept in sync:
# constants.py
MIN_VERSION = "0.2.0"
// constants.rs
const MIN_VERSION: &str = "0.2.0";
We'll add LINT
directives to keep them in sync:
# LINT.IfChange
MIN_VERSION = "0.2.0"
# LINT.ThenChange(constants.rs)
// LINT.IfChange
const MIN_VERSION: &str = "0.2.0";
// LINT.ThenChange(constants.py)
And run ifctc
-- everything should be okay:
$ git diff --unified | ifctc && echo ok
ok
Then, if we change one version and forget to update the other:
diff --git a/constants.rs b/constants.rs
--- a/constants.rs
+++ b/constants.rs
@@ -1,3 +1,3 @@
// LINT.IfChange
-const MIN_VERSION: &str = "0.2.0";
+const MIN_VERSION: &str = "0.3.0";
// LINT.ThenChange(constants.py)
Then ifctc
will report an error:
$ git diff --unified | ifctc
constants.rs:3: file was not modified: constants.py
However, if both files are modified instead:
diff --git a/constants.py b/constants.py
--- a/constants.py
+++ b/constants.py
@@ -1,3 +1,3 @@
# LINT.IfChange
-MIN_VERSION = "0.2.0"
+MIN_VERSION = "0.3.0"
# LINT.ThenChange(constants.rs)
diff --git a/constants.rs b/constants.rs
--- a/constants.rs
+++ b/constants.rs
@@ -1,3 +1,3 @@
// LINT.IfChange
-const MIN_VERSION: &str = "0.2.0";
+const MIN_VERSION: &str = "0.3.0";
// LINT.ThenChange(constants.py)
Then ifctc
succeeds:
$ git diff --unified | ifctc && echo ok
ok
Use zig build test
to run tests.
Because the parsers in this repository have both fast paths and slow paths, they
are typically tested with SplitBufferIterator
, which
ensures that they behave the same way on different chunk sizes.
Fuzzing is not currently used because it is not available on macOS.
I made this for two reasons:
-
I needed an implementation of IFTTT, which only seems to exist here, but isn't compatible with Google's (e.g. it uses "#" for labels, instead of ":").
-
I wanted to make a small project using Zig.
And because it's Zig, I really wanted to play the game and tried to optimize the tool quite deeply, at the cost of a more complex implementation:
-
Scanning happens in multiple threads at once, without any locking.
- We achieve this by allocating the map of possibly modified files at the start of the program, before we start scanning files. After that, most state is thread-local, and consolidated once all threads have finished scanning for changes.
-
LINT.IfChange
andLINT.ThenChange
directives are found using SIMD. -
Overall, allocations and copies are kept to a minimum:
-
Lines from the patch that don't contribute to the output are skipped without copying them.
-
Nothing from scanned files is copied, except for arguments of
IfChange
andThenChange
directives. -
When allocations are needed, that's usually in an arena allocator backed by a stack-fallback allocator. Unless you have long paths in your
LINT.ThenChange
arguments, no allocation will take place once threads have spawned.
-