Skip to content

Commit

Permalink
Merge pull request #13 from Ellerbach/improve-table-check
Browse files Browse the repository at this point in the history
Improvement for DocLinkChecker
  • Loading branch information
Ellerbach authored Nov 21, 2022
2 parents 63d37f5 + f4565b4 commit 745bc76
Show file tree
Hide file tree
Showing 3 changed files with 209 additions and 19 deletions.
64 changes: 64 additions & 0 deletions src/DocLinkChecker/Helpers/ExitCodeHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Licensed to DocFX Companion Tools and contributors under one or more agreements.
// DocFX Companion Tools and contributors licenses this file to you under the MIT license.

namespace DocLinkChecker.Helpers
{
using System;

/// <summary>
/// Helper methods to set the exit code.
/// </summary>
public static class ExitCodeHelper
{
private static int exitCode;

/// <summary>
/// Possible exit codes.
/// </summary>
public enum ExitCodes
{
/// <summary>
/// Exit code for seccessful execution.
/// </summary>
OK = 0,

/// <summary>
/// Exit code for parsing error.
/// </summary>
ParsingError = 1,

/// <summary>
/// Exit code for incorrect table format detected.
/// </summary>
TableFormatError = 2,

/// <summary>
/// Exit code for unkonown exception.
/// </summary>
UnknownExceptionError = 999,
}

/// <summary>
/// Gets or sets default exit code.
/// </summary>
public static ExitCodes ExitCode
{
get
{
return (ExitCodes)exitCode;
}

set
{
if (Enum.IsDefined(typeof(ExitCodes), value))
{
exitCode = (int)value;
}
else
{
exitCode = (int)ExitCodes.UnknownExceptionError;
}
}
}
}
}
144 changes: 128 additions & 16 deletions src/DocLinkChecker/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ namespace DocLinkChecker
public class Program
{
private static CommandlineOptions options;
private static int returnvalue;
private static MessageHelper message;

private static DirectoryInfo rootDir;
Expand All @@ -34,6 +33,9 @@ public class Program
/// <returns>0 if succesful, 1 on error.</returns>
private static int Main(string[] args)
{
// Set default exit code
ExitCodeHelper.ExitCode = ExitCodeHelper.ExitCodes.OK;

try
{
Parser.Default.ParseArguments<CommandlineOptions>(args)
Expand All @@ -43,12 +45,12 @@ private static int Main(string[] args)
catch (Exception ex)
{
Console.WriteLine($"ERROR: Parsing arguments threw an exception with message `{ex.Message}`");
returnvalue = 1;
ExitCodeHelper.ExitCode = ExitCodeHelper.ExitCodes.ParsingError;
}

Console.WriteLine($"Exit with return code {returnvalue}");
Console.WriteLine($"Exit with return code {(int)ExitCodeHelper.ExitCode}");

return returnvalue;
return (int)ExitCodeHelper.ExitCode;
}

/// <summary>
Expand All @@ -75,7 +77,7 @@ private static void RunLogic(CommandlineOptions o)
if (!Directory.Exists(options.DocFolder))
{
message.Error($"ERROR: Documentation folder '{options.DocFolder}' doesn't exist.");
returnvalue = 1;
ExitCodeHelper.ExitCode = ExitCodeHelper.ExitCodes.ParsingError;
return;
}

Expand All @@ -92,12 +94,12 @@ private static void RunLogic(CommandlineOptions o)
}

/// <summary>
/// On parameter errors, we set the returnvalue to 1 to indicated an error.
/// On parameter errors, we set the exit code to 1 to indicated a parsing error.
/// </summary>
/// <param name="errors">List or errors (ignored).</param>
private static void HandleErrors(IEnumerable<Error> errors)
{
returnvalue = 1;
ExitCodeHelper.ExitCode = ExitCodeHelper.ExitCodes.ParsingError;
}

/// <summary>
Expand Down Expand Up @@ -168,8 +170,8 @@ private static void CheckUnreferencedAttachments()

message.Error($"{attachment}");

// mark error in returnvalue of the tool
returnvalue = 1;
// mark error in exit code of the tool
ExitCodeHelper.ExitCode = ExitCodeHelper.ExitCodes.ParsingError;
}
}
}
Expand Down Expand Up @@ -231,20 +233,38 @@ private static void ProcessTable(string filepath, string[] content)
// The second line should contain at least 3 separators '-' between the |
// After each line, there should not be any text after the last |
Regex rxTable = new Regex(@"\|(?:.*)\|");
Regex rxTableFormatRow = new Regex(@"\s*(\|\s*:?-+:?\s*)+\|\s*");
Regex rxComments = new Regex(@"\`(?:[^`]*)\`");
int idxLine = 0;
bool isMatch = false;
int numCol = 0;
int initLine = 0;
bool isCodeBloc = false;
bool isCodeBlock = false;
char[] charsToTrim = { '\r', '\n' };
int minimalValidLineToTestIndex = 2;
for (int i = 0; i < content.Length; i++)
{
string line = content[i];
if (line.StartsWith("```"))
{
isCodeBloc = !isCodeBloc;
isCodeBlock = !isCodeBlock;
}

// Check if there is blank line before table (required by DocFX)
if (i >= minimalValidLineToTestIndex &&
rxTableFormatRow.Matches(line).Any() &&
(rxTableFormatRow.Match(line).Value.Length == line.Length) &&
!isCodeBlock)
{
string lineToTest = content[i - minimalValidLineToTestIndex];
if (lineToTest.Trim(charsToTrim) != string.Empty)
{
message.Error($"Malformed table in {filepath}, line {i - 1}. Blank line expected before table.");
ExitCodeHelper.ExitCode = ExitCodeHelper.ExitCodes.TableFormatError;
}
}

if (rxTable.Matches(line).Any() && !isCodeBloc)
if (rxTable.Matches(line).Any() && !isCodeBlock)
{
isMatch = true;
message.Verbose($"Table found line {i}.");
Expand All @@ -253,11 +273,13 @@ private static void ProcessTable(string filepath, string[] content)
if (!line.EndsWith('|') && line.Replace(" ", string.Empty).StartsWith('|'))
{
message.Error($"Malformed table in {filepath}, line {i + 1}. Table should finish by character '|'.");
ExitCodeHelper.ExitCode = ExitCodeHelper.ExitCodes.TableFormatError;
}

if (line.EndsWith('|') && !line.Replace(" ", string.Empty).StartsWith('|'))
{
message.Error($"Malformed table in {filepath}, line {i + 1}. Table should start by character '|'.");
ExitCodeHelper.ExitCode = ExitCodeHelper.ExitCodes.TableFormatError;
}

// Is it first line?
Expand All @@ -274,13 +296,21 @@ private static void ProcessTable(string filepath, string[] content)
if (i != initLine + idxLine)
{
message.Error($"Malformed table in {filepath}, line {i + 1}. Table should be continuous.");
ExitCodeHelper.ExitCode = ExitCodeHelper.ExitCodes.TableFormatError;
}

// Remove comments inside allowing something like `Spike \| data` to not count as a |
if (rxComments.Matches(line).Any())
{
line = rxComments.Replace(line, string.Empty);
}

// Count separators
string[] separators = line.Split('|');
if (separators.Length - 2 != numCol)
{
message.Error($"Malformed table in {filepath}, line {i + 1}. Different number of columns {separators.Length - 2} vs {numCol}.");
ExitCodeHelper.ExitCode = ExitCodeHelper.ExitCodes.TableFormatError;
}

if (idxLine == 1)
Expand All @@ -290,6 +320,7 @@ private static void ProcessTable(string filepath, string[] content)
if (separators[sep].Count(m => m == '-') < 3)
{
message.Error($"Malformed table in {filepath}, line {i + 1}. Second line should contains at least 3 characters '-' per column between the characters '|'.");
ExitCodeHelper.ExitCode = ExitCodeHelper.ExitCodes.TableFormatError;
}
}
}
Expand Down Expand Up @@ -331,6 +362,7 @@ private static void ProcessFile(DirectoryInfo folder, string filepath)
string relative = match.Value.Substring(start);
int end = relative.IndexOf(")");
relative = relative.Substring(0, end);
string afterSharp = string.Empty;

// relative string contain not only URL, but also "title", get rid of it
int positionOfLinkTitle = relative.IndexOf('\"');
Expand All @@ -342,6 +374,8 @@ private static void ProcessFile(DirectoryInfo folder, string filepath)
// strip in-doc references using a #
if (relative.Contains("#"))
{
// We keep the link after the sharp to check later on if it's a valid one
afterSharp = relative.Substring(relative.IndexOf("#") + 1);
relative = relative.Substring(0, relative.IndexOf("#"));
}

Expand All @@ -351,7 +385,6 @@ private static void ProcessFile(DirectoryInfo folder, string filepath)
// check link if not to a URL, in-doc link or e-mail address
if (!relative.StartsWith("http:") &&
!relative.StartsWith("https:") &&
!relative.StartsWith("#") &&
!relative.Contains("@") &&
!string.IsNullOrEmpty(Path.GetExtension(relative)) &&
!string.IsNullOrWhiteSpace(relative))
Expand All @@ -375,7 +408,7 @@ private static void ProcessFile(DirectoryInfo folder, string filepath)
// link is full path - not allowed
message.Output($"{filepath} {linenr}:{match.Index}");
message.Error($"Full path '{relative}' used. Use relative path.");
returnvalue = 1;
ExitCodeHelper.ExitCode = ExitCodeHelper.ExitCodes.ParsingError;
}

// don't need to check if reference is to a directory
Expand All @@ -388,8 +421,8 @@ private static void ProcessFile(DirectoryInfo folder, string filepath)
message.Output($"{filepath} {linenr}:{match.Index}");
message.Error($"Not found: {relative}");

// mark error in returnvalue of the tool
returnvalue = 1;
// mark error in exit code of the tool
ExitCodeHelper.ExitCode = ExitCodeHelper.ExitCodes.ParsingError;
}
else
{
Expand All @@ -398,6 +431,85 @@ private static void ProcessFile(DirectoryInfo folder, string filepath)
// register reference unique in list
allLinks.Add(absolute.ToLowerInvariant());
}

if (afterSharp != string.Empty)
{
// Time to check if the inside doc link is valid
if (afterSharp.ToLower() != afterSharp)
{
// link is full path - not allowed
message.Output($"{filepath} {linenr}:{match.Index}");
message.Error($"Inside doc path '{relative}#{afterSharp}' must be lower case.");
ExitCodeHelper.ExitCode = ExitCodeHelper.ExitCodes.ParsingError;
}

// We need to check if what is after the # is valid or not, first it must be lowercase.
var fileContent = File.ReadAllLines(absolute);

bool found = false;
foreach (var lineTitle in fileContent)
{
// Find titles
if (lineTitle.StartsWith('#'))
{
// Get rid of the title mark
var lineTitleLink = lineTitle.Replace("#", string.Empty);

// Remove the space
lineTitleLink = lineTitleLink.TrimStart();

// To lower
lineTitleLink = lineTitleLink.ToLower();

// Remove bold and italic as well as the . and few others
lineTitleLink = lineTitleLink.Replace("*", string.Empty).Replace(".", string.Empty).Replace("'", string.Empty)
.Replace("\"", string.Empty).Replace("_", string.Empty).Replace("/", string.Empty).Replace("&", string.Empty)
.Replace("(", string.Empty).Replace(")", string.Empty).Replace("`", string.Empty);

// Replave spaces by dash
lineTitleLink = lineTitleLink.Replace(" ", "-");

// If it our title?
if (afterSharp == lineTitleLink)
{
found = true;
break;
}
}
}

if (!found)
{
// Let's check if it's an absolute link or not
// Is it a link on a line number? Then pattern is 'l123456'
uint numLines;
Regex lineLinkPattern = new Regex(@"^l\d+");
if (lineLinkPattern.IsMatch(afterSharp))
{
if (!uint.TryParse(afterSharp.Substring(1), out numLines))
{
message.Output($"{filepath} {linenr}:{match.Index}");
message.Error($"In doc link was not found '{relative}#{afterSharp}'. Make sure you have all lowercase, remove '*' and replace spaces by '-'.");
ExitCodeHelper.ExitCode = ExitCodeHelper.ExitCodes.ParsingError;
}
else
{
if (fileContent.Length < numLines)
{
message.Output($"{filepath} {linenr}:{match.Index}");
message.Error($"Line link is invalid '{relative}#{afterSharp}' must be less than the number of lines in the target file.");
ExitCodeHelper.ExitCode = ExitCodeHelper.ExitCodes.ParsingError;
}
}
}
else
{
message.Output($"{filepath} {linenr}:{match.Index}");
message.Error($"In doc link was not found '{relative}#{afterSharp}'. Make sure you have all lowercase, remove '*' and replace spaces by '-'.");
ExitCodeHelper.ExitCode = ExitCodeHelper.ExitCodes.ParsingError;
}
}
}
}
}
}
Expand Down
Loading

0 comments on commit 745bc76

Please sign in to comment.