@@ -75,16 +75,35 @@ pub fn builder(b: *std.Build, options: BuilderOptions) StepBuilder {
7575 .rules = .empty ,
7676 .include_paths = .empty ,
7777 .exclude_paths = .empty ,
78+ .sources = .empty ,
7879 .b = b ,
7980 .optimize = options .optimize ,
8081 .target = options .target orelse b .graph .host ,
8182 };
8283}
8384
85+ /// Represents a source that can be linted.
86+ pub const LintSource = union (enum ) {
87+ /// e.g., library or executable.
88+ compiled_unit : struct {
89+ compile_step : * std.Build.Step.Compile ,
90+ },
91+
92+ pub fn compiled (compile : * std.Build.Step.Compile ) LintSource {
93+ return .{
94+ .compiled_unit = .{
95+ .compile_step = compile ,
96+ },
97+ };
98+ }
99+ };
100+
84101const StepBuilder = struct {
85102 rules : shims .ArrayList (BuiltRule ),
103+ // TODO: Collapse paths and sources into one array for union `LintSource`.
86104 include_paths : shims .ArrayList (std .Build .LazyPath ),
87105 exclude_paths : shims .ArrayList (std .Build .LazyPath ),
106+ sources : shims .ArrayList (LintSource ),
88107 b : * std.Build ,
89108 target : std.Build.ResolvedTarget ,
90109 optimize : std.builtin.OptimizeMode ,
@@ -110,11 +129,27 @@ const StepBuilder = struct {
110129 ) catch @panic ("OOM" );
111130 }
112131
132+ /// Adds a source to be linted (e.g., library or executable). Only inputs
133+ /// resolved to this source within the projects path will be linted.
134+ ///
135+ /// If a source is not set then it falls back to linting the include paths.
136+ /// If no include paths are given then it falls back to linting all source
137+ /// files under the current working directory.
138+ pub fn addSource (self : * StepBuilder , source : LintSource ) void {
139+ const arena = self .b .allocator ;
140+ self .sources .append (arena , source ) catch @panic ("OOM" );
141+ }
142+
113143 /// Set the paths to include or exclude when running the linter.
114144 ///
115- /// Include defaults to the current working directory. `zig-out` and
116- /// `.zig-cache` are always excluded - you don't need to explicitly include
117- /// them if setting exclude paths.
145+ /// Unless a source is set, includes defaults to the current working
146+ /// directory.
147+ ///
148+ /// If a source is set then paths included here included in combination with
149+ /// the inputs resolved from the set source.
150+ ///
151+ /// `zig-out` and `.zig-cache` are always excluded - you don't need to
152+ /// explicitly include them if setting exclude paths.
118153 pub fn addPaths (
119154 self : * StepBuilder ,
120155 paths : struct {
@@ -132,17 +167,6 @@ const StepBuilder = struct {
132167
133168 pub fn build (self : * StepBuilder ) * std.Build.Step {
134169 const b = self .b ;
135- const arena = b .allocator ;
136-
137- const include_paths = include_paths : {
138- if (self .include_paths .items .len > 0 ) {
139- break :include_paths self .include_paths .items ;
140- } else {
141- var list = arena .alloc (std .Build .LazyPath , 1 ) catch @panic ("OOM" );
142- list [0 ] = b .path ("./" );
143- break :include_paths list ;
144- }
145- };
146170
147171 return buildStep (
148172 b ,
@@ -156,8 +180,9 @@ const StepBuilder = struct {
156180 .{},
157181 ),
158182 },
159- .include_paths = include_paths ,
183+ .include_paths = self . include_paths . items ,
160184 .exclude_paths = self .exclude_paths .items ,
185+ .sources = self .sources .items ,
161186 },
162187 );
163188 }
@@ -339,9 +364,17 @@ pub fn build(b: *std.Build) void {
339364
340365 const lint_cmd = b .step ("lint" , "Lint the linters own source code." );
341366 lint_cmd .dependOn (step : {
342- const include_paths = shims .ArrayList (std .Build .LazyPath ).empty ;
367+ var sources = shims .ArrayList (LintSource ).empty ;
368+ sources .append (b .allocator , .compiled (b .addLibrary (.{
369+ .name = "zlinter" ,
370+ .root_module = zlinter_lib_module ,
371+ }))) catch @panic ("OOM" );
372+
373+ var include_paths = shims .ArrayList (std .Build .LazyPath ).empty ;
343374 var exclude_paths = shims .ArrayList (std .Build .LazyPath ).empty ;
344375
376+ // Also lint all files within project, not just those resolved to our compiled source.
377+ include_paths .append (b .allocator , b .path ("./" )) catch @panic ("OOM" );
345378 exclude_paths .append (b .allocator , b .path ("integration_tests/test_cases" )) catch @panic ("OOM" );
346379 exclude_paths .append (b .allocator , b .path ("integration_tests/src/test_case_references.zig" )) catch @panic ("OOM" );
347380
@@ -398,6 +431,7 @@ pub fn build(b: *std.Build) void {
398431 ),
399432 },
400433 .{
434+ .sources = sources .items ,
401435 .target = target ,
402436 .optimize = optimize ,
403437 .include_paths = include_paths .items ,
@@ -469,6 +503,7 @@ fn buildStep(
469503 },
470504 include_paths : []const std.Build.LazyPath ,
471505 exclude_paths : []const std.Build.LazyPath ,
506+ sources : []const LintSource ,
472507 },
473508) * std.Build.Step {
474509 const zlinter_lib_module : * std.Build.Module , const exe_file : std.Build.LazyPath , const build_rules_exe_file : std.Build.LazyPath = switch (options .zlinter ) {
@@ -521,6 +556,7 @@ fn buildStep(
521556 zlinter_exe ,
522557 options .include_paths ,
523558 options .exclude_paths ,
559+ options .sources ,
524560 );
525561
526562 return & zlinter_run .step ;
@@ -727,6 +763,9 @@ const ZlinterRun = struct {
727763 /// Exclude paths confiured within the build file.
728764 exclude_paths : []const std.Build.LazyPath ,
729765
766+ /// The sources to lint (e.g., an executable or library).
767+ sources : []const LintSource ,
768+
730769 const Arg = union (enum ) {
731770 artifact : * std.Build.Step.Compile ,
732771 bytes : []const u8 ,
@@ -737,6 +776,7 @@ const ZlinterRun = struct {
737776 exe : * std.Build.Step.Compile ,
738777 include_paths : []const std.Build.LazyPath ,
739778 exclude_paths : []const std.Build.LazyPath ,
779+ sources : []const LintSource ,
740780 ) * ZlinterRun {
741781 const arena = owner .allocator ;
742782
@@ -751,6 +791,7 @@ const ZlinterRun = struct {
751791 .argv = .empty ,
752792 .exclude_paths = exclude_paths ,
753793 .include_paths = include_paths ,
794+ .sources = sources ,
754795 };
755796
756797 for (include_paths ) | path | {
@@ -772,6 +813,14 @@ const ZlinterRun = struct {
772813 const bin_file = exe .getEmittedBin ();
773814 bin_file .addStepDependencies (& self .step );
774815
816+ for (sources ) | s | {
817+ switch (s ) {
818+ .compiled_unit = > | info | {
819+ self .step .dependOn (& info .compile_step .step );
820+ },
821+ }
822+ }
823+
775824 return self ;
776825 }
777826
@@ -784,14 +833,16 @@ const ZlinterRun = struct {
784833 fn subPaths (
785834 step : * std.Build.Step ,
786835 paths : []const std.Build.LazyPath ,
787- ) error {OutOfMemory }! []const []const u8 {
836+ ) error {OutOfMemory }! ? []const []const u8 {
837+ if (paths .len == 0 ) return null ;
838+
788839 const b = step .owner ;
789840
790841 var list : shims .ArrayList ([]const u8 ) = try .initCapacity (
791842 b .allocator ,
792843 paths .len ,
793844 );
794- errdefer list .deinit (b .allocator );
845+ defer list .deinit (b .allocator );
795846
796847 for (paths ) | path | {
797848 list .appendAssumeCapacity (
@@ -807,9 +858,65 @@ const ZlinterRun = struct {
807858 const b = run .step .owner ;
808859 const arena = b .allocator ;
809860
861+ var cwd_buff : [std .fs .max_path_bytes ]u8 = undefined ;
862+ const cwd : BuildCwd = .init (& cwd_buff );
863+
864+ var includes : std .ArrayList (std .Build .LazyPath ) = try .initCapacity (
865+ b .allocator ,
866+ @max (1 , run .include_paths .len ),
867+ );
868+ defer includes .deinit (b .allocator );
869+
870+ includes .appendSliceAssumeCapacity (run .include_paths );
871+
872+ for (run .sources ) | source | {
873+ switch (source ) {
874+ .compiled_unit = > | info | {
875+ var exe = info .compile_step ;
876+
877+ // TODO: Use graph for import map.
878+ const graph = exe .root_module .getGraph ();
879+ _ = graph ;
880+
881+ var inputs = exe .step .inputs ;
882+ std .debug .assert (inputs .populated ());
883+
884+ var it = inputs .table .iterator ();
885+ while (it .next ()) | entry | {
886+ const p = entry .key_ptr .* ;
887+ sub_paths : for (entry .value_ptr .items ) | sub_path | {
888+ var buf : [std .fs .max_path_bytes ]u8 = undefined ;
889+ const joined_path = if (p .sub_path .len == 0 ) sub_path else p : {
890+ const fmt = "{s}" ++ std .fs .path .sep_str ++ "{s}" ;
891+ break :p std .fmt .bufPrint (
892+ & buf ,
893+ fmt ,
894+ .{ p .sub_path , sub_path },
895+ ) catch {
896+ std .debug .print (
897+ "Warning: Name too long - " ++ fmt ,
898+ .{ p .sub_path , sub_path },
899+ );
900+ continue :sub_paths ;
901+ };
902+ };
903+ std .debug .assert (joined_path .len > 0 );
904+
905+ if (cwd .relativePath (b , joined_path )) | path | {
906+ try includes .append (b .allocator , path );
907+ }
908+ }
909+ }
910+ },
911+ }
912+ }
913+ if (includes .items .len == 0 ) {
914+ includes .appendAssumeCapacity (b .path ("./" ));
915+ }
916+
810917 const build_info_zon_bytes : []const u8 = toZonString (BuildInfo {
811- .include_paths = if ( run . include_paths . len > 0 ) try subPaths (& run .step , run . include_paths ) else null ,
812- .exclude_paths = if ( run . exclude_paths . len > 0 ) try subPaths (& run .step , run .exclude_paths ) else null ,
918+ .include_paths = try subPaths (& run .step , includes . items ) ,
919+ .exclude_paths = try subPaths (& run .step , run .exclude_paths ),
813920 }, b .allocator );
814921
815922 const env_map = arena .create (std .process .EnvMap ) catch @panic ("OOM" );
@@ -990,6 +1097,68 @@ fn readHtmlTemplate(b: *std.Build, path: std.Build.LazyPath) ![]const u8 {
9901097 return buffer ;
9911098}
9921099
1100+ /// Normalised representation of the current working directory
1101+ const BuildCwd = struct {
1102+ path : []const u8 ,
1103+ dir : std.fs.Dir ,
1104+
1105+ pub fn init (buff : * [std .fs .max_path_bytes ]u8 ) BuildCwd {
1106+ return .{
1107+ .dir = std .fs .cwd (),
1108+ .path = std .process .getCwd (buff ) catch unreachable ,
1109+ };
1110+ }
1111+
1112+ /// Returns a path relative to the current working directory or null if the
1113+ /// path is not relative to the current working directory.
1114+ ///
1115+ /// If the path is a relative path, we check whether it exists with the
1116+ /// current working directory.
1117+ ///
1118+ /// If the path is absolute, we check whether it resolves to a readable
1119+ /// file within the current working directory.
1120+ pub fn relativePath (
1121+ self : * const BuildCwd ,
1122+ b : * std.Build ,
1123+ to : []const u8 ,
1124+ ) ? std.Build.LazyPath {
1125+ if (std .fs .path .isAbsolute (to )) {
1126+ const relative = std .fs .path .relative (
1127+ b .allocator ,
1128+ self .path ,
1129+ to ,
1130+ ) catch | e |
1131+ switch (e ) {
1132+ error .OutOfMemory = > @panic ("OOM" ),
1133+ error .Unexpected ,
1134+ error .CurrentWorkingDirectoryUnlinked ,
1135+ = > return null ,
1136+ };
1137+ errdefer b .allocator .free (relative );
1138+
1139+ if (relative .len != 0 and isReadable (& self .dir , relative )) {
1140+ return b .path (relative );
1141+ } else {
1142+ b .allocator .free (relative );
1143+ return null ;
1144+ }
1145+ } else {
1146+ if (isReadable (& self .dir , to )) {
1147+ return b .path (b .dupe (to ));
1148+ }
1149+ }
1150+ return null ;
1151+ }
1152+
1153+ fn isReadable (from : * const std.fs.Dir , sub_path : []const u8 ) bool {
1154+ _ = from .access (
1155+ sub_path ,
1156+ .{ .mode = .read_only },
1157+ ) catch return false ;
1158+ return true ;
1159+ }
1160+ };
1161+
9931162const BuildInfo = @import ("src/lib/BuildInfo.zig" );
9941163const std = @import ("std" );
9951164const isLintableFilePath = @import ("src/lib/files.zig" ).isLintableFilePath ;
0 commit comments