Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht
- Split the CLI build pipeline into named `ParseCommandLine`,
`ExecuteBuild`, and `RenderAndReport` phases, leaving `RunBuild` as a short
orchestrator with one owner for option objects and build state.
- Added a leaf `PasWeave.FS` unit for shared path and file helpers; discovery,
Lazarus, and output code now share one definition of path normalization,
source-file detection, readability, symlink checks, and whole-file reads.
Directory walkers remain specialized where their pruning and filtering
semantics differ.
- Replaced the repeated `--name` / `--name=value` option branches with a
shared `MatchValueOption` helper, roughly halving `ParseCommandLine` and
centralizing separated-versus-equals value handling.

## [0.7.0] - 2026-09-17

Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
FPC ?= fpc
UNIT_PATHS = -Fusrc/cli -Fusrc/diagnostics -Fusrc/incremental -Fusrc/model -Fusrc/parser -Fusrc/render -Fusrc/validation
UNIT_PATHS = -Fusrc/cli -Fusrc/diagnostics -Fusrc/incremental -Fusrc/model -Fusrc/parser -Fusrc/render -Fusrc/support -Fusrc/validation
FPC_FLAGS = -Mobjfpc -Sh $(UNIT_PATHS) -FUbuild/units

ifeq ($(OS),Windows_NT)
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ src/validation/ Authoring diagnostics and coverage gates
src/render/ HTML, Markdown, links, shared helpers, offline assets
src/diagnostics/ Stable diagnostic codes and severities
src/incremental/ Fingerprints, manifest.json, atomic writes
src/support/ Shared filesystem helpers (PasWeave.FS)
tests/ Fixtures, shared assertions, CLI + focused regression suites
examples/ Minimal documented-api first, rich scientific-api second
```
Expand Down
2 changes: 1 addition & 1 deletion scripts/build-portable-windows.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ try {

$unitPaths = @(
'-Fusrc/cli', '-Fusrc/diagnostics', '-Fusrc/incremental', '-Fusrc/model',
'-Fusrc/parser', '-Fusrc/render', '-Fusrc/validation'
'-Fusrc/parser', '-Fusrc/render', '-Fusrc/support', '-Fusrc/validation'
)
$testCompilerArguments = @(
'-Twin64', '-Px86_64', '-B', '-O2', '-Mobjfpc', '-Sh'
Expand Down
231 changes: 69 additions & 162 deletions src/cli/PasWeave.CLI.pas
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ interface

/// Runs PasWeave with the process command line.
///
/// @returns Process exit code (`0` for success, `1` for diagnostics failures).
/// @returns Process exit code: `0` success, `1` build diagnostics at or above
/// the `--fail-on` severity, `2` invalid command line or configuration,
/// `3` an unexpected internal error.
function RunPasWeave: Integer;

implementation
Expand Down Expand Up @@ -97,6 +99,25 @@ function RequireOptionValue(var AIndex: Integer; const AOption: string): string;
Result := ParamStr(AIndex);
end;

/// Matches `--name value` or `--name=value`, advancing AIndex for the
/// separated form. Returns False when the parameter is a different option.
function MatchValueOption(const AParam, AName: string; var AIndex: Integer;
out AValue: string): Boolean;
var
Prefix: string;
begin
AValue := '';
if AParam = AName then
begin
AValue := RequireOptionValue(AIndex, AName);
Exit(True);
end;
Prefix := AName + '=';
Result := Pos(Prefix, AParam) = 1;
if Result then
AValue := Copy(AParam, Length(Prefix) + 1, MaxInt);
end;

procedure PrintDiagnostic(ADiagnostic: TDiagnostic; AVerbose: Boolean);
var
Location: string;
Expand Down Expand Up @@ -134,7 +155,6 @@ function RunBuild: Integer;
DiagnosticOutputFile: string;
MarkdownOutputPath: string;
HTMLOutputPath: string;
CommentStyleValue: string;
BuildMode: string;
CommentStyles: TDocumentationCommentStyles;
DiscoveryOptions: TSourceDiscoveryOptions;
Expand All @@ -146,7 +166,6 @@ function RunBuild: Integer;
FailureSeverity: TDiagnosticSeverity;
MinimumCoverage: Integer;
HasMinimumCoverage: Boolean;
ThresholdValue: string;
RepositoryURL: string;
SourceLinkTemplate: string;
SourceLinkError: string;
Expand Down Expand Up @@ -250,207 +269,95 @@ function RunBuild: Integer;
end;

procedure ParseCommandLine;
var
OptionValue: string;
begin
I := 2;
while I <= ParamCount do
begin
if ParamStr(I) = '--output' then
OutputPath := RequireOptionValue(I, '--output')
else if Pos('--output=', ParamStr(I)) = 1 then
OutputPath := Copy(ParamStr(I), Length('--output=') + 1, MaxInt)
else if ParamStr(I) = '--project-name' then
begin
ProjectName := RequireOptionValue(I, '--project-name');
ProjectNameExplicit := True;
end
else if Pos('--project-name=', ParamStr(I)) = 1 then
if MatchValueOption(ParamStr(I), '--output', I, OptionValue) then
OutputPath := OptionValue
else if MatchValueOption(ParamStr(I), '--project-name', I, OptionValue) then
begin
ProjectName := Copy(ParamStr(I), Length('--project-name=') + 1,
MaxInt);
ProjectName := OptionValue;
ProjectNameExplicit := True;
end
else if ParamStr(I) = '--doc-comments' then
else if MatchValueOption(ParamStr(I), '--doc-comments', I, OptionValue) then
begin
CommentStyleValue := RequireOptionValue(I, '--doc-comments');
if not TryParseDocumentationCommentStyles(CommentStyleValue,
CommentStyles) then
if not TryParseDocumentationCommentStyles(OptionValue, CommentStyles) then
raise EPasWeaveInputError.CreateFmt(
'invalid documentation comment styles: %s ' +
'(expected slash (///), brace ({ ... }), paren ((* ... *)), ' +
'a comma-separated combination, or all)',
[CommentStyleValue]);
end
else if Pos('--doc-comments=', ParamStr(I)) = 1 then
begin
CommentStyleValue := Copy(ParamStr(I), Length('--doc-comments=') + 1,
MaxInt);
if not TryParseDocumentationCommentStyles(CommentStyleValue,
CommentStyles) then
raise EPasWeaveInputError.CreateFmt(
'invalid documentation comment styles: %s ' +
'(expected slash (///), brace ({ ... }), paren ((* ... *)), ' +
'a comma-separated combination, or all)',
[CommentStyleValue]);
[OptionValue]);
end
else if ParamStr(I) = '--recursive' then
DiscoveryOptions.Recursive := True
else if ParamStr(I) = '--include' then
DiscoveryOptions.AddIncludePattern(
RequireOptionValue(I, '--include'))
else if Pos('--include=', ParamStr(I)) = 1 then
DiscoveryOptions.AddIncludePattern(Copy(ParamStr(I),
Length('--include=') + 1, MaxInt))
else if ParamStr(I) = '--exclude' then
DiscoveryOptions.AddExcludePattern(
RequireOptionValue(I, '--exclude'))
else if Pos('--exclude=', ParamStr(I)) = 1 then
DiscoveryOptions.AddExcludePattern(Copy(ParamStr(I),
Length('--exclude=') + 1, MaxInt))
else if ParamStr(I) = '--unit-path' then
CompilerOptions.AddUnitPath(
RequireOptionValue(I, '--unit-path'))
else if Pos('--unit-path=', ParamStr(I)) = 1 then
CompilerOptions.AddUnitPath(Copy(ParamStr(I),
Length('--unit-path=') + 1, MaxInt))
else if ParamStr(I) = '--include-path' then
CompilerOptions.AddIncludePath(
RequireOptionValue(I, '--include-path'))
else if Pos('--include-path=', ParamStr(I)) = 1 then
CompilerOptions.AddIncludePath(Copy(ParamStr(I),
Length('--include-path=') + 1, MaxInt))
else if ParamStr(I) = '--define' then
CompilerOptions.AddDefine(RequireOptionValue(I, '--define'))
else if Pos('--define=', ParamStr(I)) = 1 then
CompilerOptions.AddDefine(Copy(ParamStr(I),
Length('--define=') + 1, MaxInt))
else if ParamStr(I) = '--target-os' then
CompilerOptions.SetTargetOS(
RequireOptionValue(I, '--target-os'))
else if Pos('--target-os=', ParamStr(I)) = 1 then
CompilerOptions.SetTargetOS(Copy(ParamStr(I),
Length('--target-os=') + 1, MaxInt))
else if ParamStr(I) = '--target-cpu' then
CompilerOptions.SetTargetCPU(
RequireOptionValue(I, '--target-cpu'))
else if Pos('--target-cpu=', ParamStr(I)) = 1 then
CompilerOptions.SetTargetCPU(Copy(ParamStr(I),
Length('--target-cpu=') + 1, MaxInt))
else if ParamStr(I) = '--build-mode' then
BuildMode := RequireOptionValue(I, '--build-mode')
else if Pos('--build-mode=', ParamStr(I)) = 1 then
BuildMode := Copy(ParamStr(I), Length('--build-mode=') + 1, MaxInt)
else if ParamStr(I) = '--package-path' then
PackagePaths.Add(RequireOptionValue(I, '--package-path'))
else if Pos('--package-path=', ParamStr(I)) = 1 then
PackagePaths.Add(Copy(ParamStr(I), Length('--package-path=') + 1,
MaxInt))
else if ParamStr(I) = '--repository-url' then
RepositoryURL := RequireOptionValue(I, '--repository-url')
else if Pos('--repository-url=', ParamStr(I)) = 1 then
RepositoryURL := Copy(ParamStr(I), Length('--repository-url=') + 1,
MaxInt)
else if ParamStr(I) = '--source-link-template' then
SourceLinkTemplate := RequireOptionValue(I, '--source-link-template')
else if Pos('--source-link-template=', ParamStr(I)) = 1 then
SourceLinkTemplate := Copy(ParamStr(I),
Length('--source-link-template=') + 1, MaxInt)
else if ParamStr(I) = '--project-mark' then
else if MatchValueOption(ParamStr(I), '--include', I, OptionValue) then
DiscoveryOptions.AddIncludePattern(OptionValue)
else if MatchValueOption(ParamStr(I), '--exclude', I, OptionValue) then
DiscoveryOptions.AddExcludePattern(OptionValue)
else if MatchValueOption(ParamStr(I), '--unit-path', I, OptionValue) then
CompilerOptions.AddUnitPath(OptionValue)
else if MatchValueOption(ParamStr(I), '--include-path', I, OptionValue) then
CompilerOptions.AddIncludePath(OptionValue)
else if MatchValueOption(ParamStr(I), '--define', I, OptionValue) then
CompilerOptions.AddDefine(OptionValue)
else if MatchValueOption(ParamStr(I), '--target-os', I, OptionValue) then
CompilerOptions.SetTargetOS(OptionValue)
else if MatchValueOption(ParamStr(I), '--target-cpu', I, OptionValue) then
CompilerOptions.SetTargetCPU(OptionValue)
else if MatchValueOption(ParamStr(I), '--build-mode', I, OptionValue) then
BuildMode := OptionValue
else if MatchValueOption(ParamStr(I), '--package-path', I, OptionValue) then
PackagePaths.Add(OptionValue)
else if MatchValueOption(ParamStr(I), '--repository-url', I, OptionValue) then
RepositoryURL := OptionValue
else if MatchValueOption(ParamStr(I), '--source-link-template', I, OptionValue) then
SourceLinkTemplate := OptionValue
else if MatchValueOption(ParamStr(I), '--project-mark', I, OptionValue) then
begin
ProjectMark := RequireOptionValue(I, '--project-mark');
if not IsValidProjectMark(ProjectMark) then
if not IsValidProjectMark(OptionValue) then
raise EPasWeaveInputError.Create(
'--project-mark must be 1 to 4 alphanumeric characters');
ProjectMark := OptionValue;
HasProjectMark := True;
end
else if Pos('--project-mark=', ParamStr(I)) = 1 then
begin
ProjectMark := Copy(ParamStr(I), Length('--project-mark=') + 1,
MaxInt);
if not IsValidProjectMark(ProjectMark) then
raise EPasWeaveInputError.Create(
'--project-mark must be 1 to 4 alphanumeric characters');
HasProjectMark := True;
end
else if ParamStr(I) = '--theme-accent' then
begin
ThemeAccent := RequireOptionValue(I, '--theme-accent');
if not IsValidThemeColor(ThemeAccent) then
raise EPasWeaveInputError.Create(
'--theme-accent must be a #RGB, #RRGGBB, or #RRGGBBAA color');
HasThemeAccent := True;
end
else if Pos('--theme-accent=', ParamStr(I)) = 1 then
else if MatchValueOption(ParamStr(I), '--theme-accent', I, OptionValue) then
begin
ThemeAccent := Copy(ParamStr(I), Length('--theme-accent=') + 1,
MaxInt);
if not IsValidThemeColor(ThemeAccent) then
if not IsValidThemeColor(OptionValue) then
raise EPasWeaveInputError.Create(
'--theme-accent must be a #RGB, #RRGGBB, or #RRGGBBAA color');
ThemeAccent := OptionValue;
HasThemeAccent := True;
end
else if ParamStr(I) = '--theme-accent-2' then
begin
ThemeAccentAlt := RequireOptionValue(I, '--theme-accent-2');
if not IsValidThemeColor(ThemeAccentAlt) then
raise EPasWeaveInputError.Create(
'--theme-accent-2 must be a #RGB, #RRGGBB, or #RRGGBBAA color');
HasThemeAccentAlt := True;
end
else if Pos('--theme-accent-2=', ParamStr(I)) = 1 then
else if MatchValueOption(ParamStr(I), '--theme-accent-2', I, OptionValue) then
begin
ThemeAccentAlt := Copy(ParamStr(I), Length('--theme-accent-2=') + 1,
MaxInt);
if not IsValidThemeColor(ThemeAccentAlt) then
if not IsValidThemeColor(OptionValue) then
raise EPasWeaveInputError.Create(
'--theme-accent-2 must be a #RGB, #RRGGBB, or #RRGGBBAA color');
ThemeAccentAlt := OptionValue;
HasThemeAccentAlt := True;
end
else if ParamStr(I) = '--theme-font' then
else if MatchValueOption(ParamStr(I), '--theme-font', I, OptionValue) then
begin
ThemeFont := RequireOptionValue(I, '--theme-font');
if not IsValidThemeFont(ThemeFont) then
if not IsValidThemeFont(OptionValue) then
raise EPasWeaveInputError.Create(
'--theme-font must be a safe font family name');
ThemeFont := OptionValue;
HasThemeFont := True;
end
else if Pos('--theme-font=', ParamStr(I)) = 1 then
else if MatchValueOption(ParamStr(I), '--min-documentation-coverage', I, OptionValue) then
begin
ThemeFont := Copy(ParamStr(I), Length('--theme-font=') + 1, MaxInt);
if not IsValidThemeFont(ThemeFont) then
raise EPasWeaveInputError.Create(
'--theme-font must be a safe font family name');
HasThemeFont := True;
end
else if ParamStr(I) = '--min-documentation-coverage' then
begin
ThresholdValue := RequireOptionValue(I, '--min-documentation-coverage');
if not TryStrToInt(ThresholdValue, MinimumCoverage) or
if not TryStrToInt(OptionValue, MinimumCoverage) or
(MinimumCoverage < 0) or (MinimumCoverage > 100) then
raise EPasWeaveInputError.Create(
'--min-documentation-coverage must be an integer from 0 to 100');
HasMinimumCoverage := True;
end
else if Pos('--min-documentation-coverage=', ParamStr(I)) = 1 then
begin
ThresholdValue := Copy(ParamStr(I),
Length('--min-documentation-coverage=') + 1, MaxInt);
if not TryStrToInt(ThresholdValue, MinimumCoverage) or
(MinimumCoverage < 0) or (MinimumCoverage > 100) then
raise EPasWeaveInputError.Create(
'--min-documentation-coverage must be an integer from 0 to 100');
HasMinimumCoverage := True;
end
else if ParamStr(I) = '--fail-on' then
begin
ThresholdValue := RequireOptionValue(I, '--fail-on');
if not TryParseDiagnosticSeverity(ThresholdValue, FailureSeverity) then
raise EPasWeaveInputError.Create(
'--fail-on must be warning or error');
end
else if Pos('--fail-on=', ParamStr(I)) = 1 then
else if MatchValueOption(ParamStr(I), '--fail-on', I, OptionValue) then
begin
ThresholdValue := Copy(ParamStr(I), Length('--fail-on=') + 1, MaxInt);
if not TryParseDiagnosticSeverity(ThresholdValue, FailureSeverity) then
if not TryParseDiagnosticSeverity(OptionValue, FailureSeverity) then
raise EPasWeaveInputError.Create(
'--fail-on must be warning or error');
end
Expand Down
22 changes: 3 additions & 19 deletions src/incremental/PasWeave.Incremental.pas
Original file line number Diff line number Diff line change
Expand Up @@ -46,28 +46,12 @@ function PeakHeapBytes: QWord;
implementation

uses
PasWeave.Hashing, PasWeave.Version, PasWeave.Output;
PasWeave.FS, PasWeave.Hashing, PasWeave.Version;

var
GPeakHeap: QWord;

function IsPascalLikeFilename(const AFilename: string): Boolean;
var
Extension: string;
begin
Extension := LowerCase(ExtractFileExt(AFilename));
Result := (Extension = '.pas') or (Extension = '.pp') or
(Extension = '.inc') or (Extension = '.lpi') or (Extension = '.lpk');
end;

function IsSymbolicLink(AAttributes: LongInt): Boolean;
begin
{$IFDEF UNIX}
Result := (AAttributes and faSymLink) <> 0;
{$ELSE}
Result := False;
{$ENDIF}
end;

procedure CollectDirectoryPascalFiles(const ARootDirectory,
ARelativeDirectory: string; ARecursive: Boolean;
Expand Down Expand Up @@ -105,7 +89,7 @@ procedure CollectDirectoryPascalFiles(const ARootDirectory,
CollectDirectoryPascalFiles(ARootDirectory, RelativePath, ARecursive,
ADiscovery, AFiles);
end
else if IsPascalLikeFilename(Search.Name) then
else if HasBuildInputExtension(Search.Name) then
begin
if Assigned(ADiscovery) and
(ADiscovery.IsExcluded(RelativePath) or
Expand Down Expand Up @@ -137,7 +121,7 @@ procedure CollectDirectoryListingFiles(const ADirectory: string;
FullPath := IncludeTrailingPathDelimiter(ADirectory) + Search.Name;
if (Search.Attr and faDirectory) = 0 then
begin
if IsPascalLikeFilename(Search.Name) then
if HasBuildInputExtension(Search.Name) then
AFiles.Add(NormalisePath(ExpandFileName(FullPath)));
end;
until FindNext(Search) <> 0;
Expand Down
Loading
Loading