diff --git a/README.md b/README.md index c99f058..8fb2473 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ $rule = $matcher->match($relative_path); ## TODO - [x] Add comments to the structure -- [ ] Complete phpdocs +- [x] Complete phpdocs - [ ] Add error checking on `match` to normalize or reject impossible patterns (i.e. "" and "/...") - [ ] Explain how `AutomataMatcher` works in this file. - [x] Should I support whitespace escaping in patterns? @@ -52,6 +52,7 @@ $rule = $matcher->match($relative_path); - [x] Remove ext-json from requires. - [x] Add ext-ctype to requires. - [x] Convert json tests to PHP arrays. +- [ ] Add a command line tool for dot-renderer ## Author(s) diff --git a/src/AutomataMatcher.php b/src/AutomataMatcher.php index a458c72..e9674bd 100644 --- a/src/AutomataMatcher.php +++ b/src/AutomataMatcher.php @@ -5,11 +5,23 @@ use Kellegous\CodeOwners\AutomataMatcher\State; use Kellegous\CodeOwners\AutomataMatcher\Token; +/** + * A RuleMatcher that combines all the patterns into a single automata. + */ final class AutomataMatcher implements RuleMatcher { + /** + * This is the start state for the automata. + * + * @var State + */ private State $start; /** + * The full collection of rules from the Owners instance. The NFA keeps the index of + * rules instead of a reference to the rules themselves since the index provides a straight-forward + * way to honor the last-match-wins rule (the largest of the matching indexes wins). + * * @var Rule[] */ private array $rules; @@ -27,6 +39,8 @@ private function __construct( } /** + * Builds a new AutomataMatcher from the given rules. + * * @param iterable $rules * @return self */ @@ -76,6 +90,14 @@ private static function parsePattern(Pattern $pattern): array return $tokens; } + /** + * Strangely, a pattern is absolute not only if it starts with a slash, + * but also if it contains a wildcard. + * + * @param string $pattern + * + * @return bool + */ private static function isAbsolute(string $pattern): bool { $ix = strpos($pattern, '/'); @@ -132,6 +154,8 @@ private static function parseToken( } /** + * @inerhitDoc + * @Override * @param string $path * @return Rule|null */ @@ -145,6 +169,8 @@ public function match(string $path): ?Rule } /** + * Used to return an internal representation of the automata for debugging purposes. + * * @return array{nodes: array, edges: array{from: string, to: string, label: string}[]} * * @internal diff --git a/src/AutomataMatcher/DotRenderer.php b/src/AutomataMatcher/DotRenderer.php index 9570810..42402fe 100644 --- a/src/AutomataMatcher/DotRenderer.php +++ b/src/AutomataMatcher/DotRenderer.php @@ -4,9 +4,16 @@ use Kellegous\CodeOwners\AutomataMatcher; +/** + * Intended for debugging, this class will emit a graphviz dot file to visualize + * the state machine used to match patterns in an AutomataMatcher. + */ final class DotRenderer { /** + * Generate a graphviz dot file from the given AutomataMatcher. This can be + * rendered using the dot command. + * * @param AutomataMatcher $matcher * @return string */ diff --git a/src/AutomataMatcher/State.php b/src/AutomataMatcher/State.php index ccc71bc..4a6f86a 100644 --- a/src/AutomataMatcher/State.php +++ b/src/AutomataMatcher/State.php @@ -1,27 +1,44 @@ */ private array $edges = []; /** + * A recursive state is a special ** state which is one that has a self-referencing + * epsilon state. + * * @var bool */ private bool $isRecursive; /** + * Create a new state. If the state is a terminating ** state, then it will + * be considered recursive. + * * @param bool $isRecursive */ public function __construct(bool $isRecursive = false) @@ -30,6 +47,8 @@ public function __construct(bool $isRecursive = false) } /** + * Add the states associated with the tokens of a parsed pattern. + * * @param Token[] $tokens * @param int $priority * @return void @@ -65,6 +84,9 @@ public function addTokens( } /** + * Find the highest priority match for the given path. Note that a return value + * of -1 indicates that no match was found. + * * @param string[] $path * @return int */ @@ -94,6 +116,8 @@ public function match(array $path): int } /** + * Used to return an internal representation of the automata for debugging purposes. + * * @param array $nodes * @param array{from:int, to: int, label:string}[] $edges * @return void diff --git a/src/AutomataMatcher/Token.php b/src/AutomataMatcher/Token.php index 6a220ab..ae92df6 100644 --- a/src/AutomataMatcher/Token.php +++ b/src/AutomataMatcher/Token.php @@ -4,12 +4,30 @@ namespace Kellegous\CodeOwners\AutomataMatcher; +/** + * Represents a "segment" of a pattern. Each pattern consists of a number of + * tokens separated by "/" characters. This class holds the raw string + * representation of that segment as well as the regular expression that matches + * that segment. + * + * @internal + */ final class Token { + /** + * @var string + */ private string $pattern; + /** + * @var string + */ private string $regex; + /** + * @param string $pattern + * @param string $regex + */ public function __construct( string $pattern, string $regex @@ -18,11 +36,17 @@ public function __construct( $this->regex = $regex; } + /** + * @return string + */ public function getPattern(): string { return $this->pattern; } + /** + * @return string + */ public function getRegex(): string { return $this->regex; diff --git a/src/Blank.php b/src/Blank.php index b957231..276d33d 100644 --- a/src/Blank.php +++ b/src/Blank.php @@ -4,6 +4,9 @@ namespace Kellegous\CodeOwners; +/** + * Represents a blank line in the code owners file. + */ final class Blank implements Entry { private SourceInfo $sourceInfo; diff --git a/src/Comment.php b/src/Comment.php index 26258c5..819e9ee 100644 --- a/src/Comment.php +++ b/src/Comment.php @@ -4,12 +4,26 @@ namespace Kellegous\CodeOwners; +/** + * Represents a comment in the code owners file. + */ final class Comment implements Entry { + /** + * The text of the comment including the leading `#`. + * @var string + */ private string $text; + /** + * @var SourceInfo + */ private SourceInfo $sourceInfo; + /** + * @param string $text + * @param SourceInfo $sourceInfo + */ public function __construct( string $text, SourceInfo $sourceInfo @@ -19,6 +33,7 @@ public function __construct( } /** + * @inheritDoc * @Override * @return SourceInfo */ @@ -28,6 +43,7 @@ public function getSourceInfo(): SourceInfo } /** + * #inheritDoc * @Override * @return string */ diff --git a/src/Owners.php b/src/Owners.php index 4a262c4..623fd40 100644 --- a/src/Owners.php +++ b/src/Owners.php @@ -6,6 +6,9 @@ use Iterator; +/** + * Represents the contents of a code owners file. + */ final class Owners { /** @@ -23,6 +26,8 @@ private function __construct( } /** + * Parse the contents of a code owners file. + * * @param string $filename * @return self * @throws ParseException @@ -109,6 +114,8 @@ private static function readLinesFrom(string $filename): iterable } /** + * Parse the contents of a code owners file as a string. + * * @param string $content * @param string|null $filename * @return self @@ -129,6 +136,9 @@ public static function fromString( } /** + * Get only the rules that are present in the code owners structure. This omits + * blank lines and comments. + * * @return iterable */ public function getRules(): iterable @@ -141,6 +151,8 @@ public function getRules(): iterable } /** + * Get all entries from the code owners file. + * * @return iterable */ public function getEntries(): iterable diff --git a/src/ParseException.php b/src/ParseException.php index df81efc..d232ca6 100644 --- a/src/ParseException.php +++ b/src/ParseException.php @@ -6,6 +6,9 @@ use Exception; +/** + * Thrown when the contents of an owners file cannot be parsed. + */ final class ParseException extends Exception { } \ No newline at end of file diff --git a/src/Pattern.php b/src/Pattern.php index 5b44bf6..514df43 100644 --- a/src/Pattern.php +++ b/src/Pattern.php @@ -6,6 +6,9 @@ use Closure; +/** + * Represents a file pattern part of a rule in a code owners file. + */ final class Pattern { /** @@ -13,11 +16,21 @@ final class Pattern */ private string $pattern; + /** + * @param string $pattern + */ private function __construct(string $pattern) { $this->pattern = $pattern; } + /** + * Parse the pattern from a string to a Pattern instance. + * + * @param string $pattern + * @return self + * @throws ParseException + */ public static function parse(string $pattern): self { if (strpos($pattern, '***') !== false || $pattern === '') { @@ -26,12 +39,19 @@ public static function parse(string $pattern): self return new self($pattern); } + /** + * Get the string representation of the pattern. + * + * @return string + */ public function toString(): string { return $this->pattern; } /** + * Get a matcher function for the pattern. + * * @return Closure(string):bool */ public function getMatcher(): Closure @@ -46,6 +66,12 @@ public function getMatcher(): Closure }; } + /** + * Create a regular expression from a pattern. + * + * @param string $pattern + * @return string + */ private static function toRegexp(string $pattern): string { if ($pattern === '/') { diff --git a/src/Rule.php b/src/Rule.php index 63c12b0..5a6ec0b 100644 --- a/src/Rule.php +++ b/src/Rule.php @@ -4,7 +4,10 @@ namespace Kellegous\CodeOwners; - +/** + * Represents a rule in the code owners file. A rule consists of a pattern, a list of zero or more + * owners and an optional trailing comment. + */ final class Rule implements Entry { /** @@ -17,8 +20,14 @@ final class Rule implements Entry */ private array $owners; + /** + * @var SourceInfo + */ private SourceInfo $sourceInfo; + /** + * @var string|null + */ private ?string $comment; /** @@ -39,6 +48,15 @@ public function __construct( $this->comment = $comment; } + /** + * Parse a rule from a line in the code owners file. + * + * @param string $line + * @param SourceInfo $sourceInfo + * @param string|null $comment + * @return Rule + * @throws ParseException + */ public static function parse( string $line, SourceInfo $sourceInfo, @@ -95,6 +113,8 @@ private static function cutAfterPattern(string $line): int } /** + * Get the pattern for the rule. + * * @return Pattern */ public function getPattern(): Pattern @@ -103,6 +123,8 @@ public function getPattern(): Pattern } /** + * Get the owners for the rule. + * * @return string[] */ public function getOwners(): array @@ -110,6 +132,11 @@ public function getOwners(): array return $this->owners; } + /** + * Get the trailing comment, if any. The comment will include the leading `#` character. + * + * @return string|null + */ public function getComment(): ?string { return $this->comment; diff --git a/src/RuleMatcher.php b/src/RuleMatcher.php index 976b542..9ec1cac 100644 --- a/src/RuleMatcher.php +++ b/src/RuleMatcher.php @@ -4,7 +4,18 @@ namespace Kellegous\CodeOwners; +/** + * A rule matcher is responsible for matching a path to a rule. + */ interface RuleMatcher { + /** + * Given the relative path to a file, this method returns the rule that matches the path, if such a rule exists. + * * Note that path is a relative path from the root of the repository. $path should not begin with a slash nor should + * * it end with a slash. + * + * @param string $path + * @return Rule|null + */ public function match(string $path): ?Rule; } \ No newline at end of file diff --git a/src/SimpleMatcher.php b/src/SimpleMatcher.php index ff39027..57d4c55 100644 --- a/src/SimpleMatcher.php +++ b/src/SimpleMatcher.php @@ -4,6 +4,11 @@ use Closure; +/** + * SimpleMatcher does a linear search of the rules present in the code owners + * file. This is a simpler matcher that uses less memory but is slower than an + * AutomatonMatcher.. + */ final class SimpleMatcher implements RuleMatcher { /** @@ -24,6 +29,7 @@ public function __construct(iterable $rules) } /** + * @inerhitDoc * @param string $path * @return Rule|null */ diff --git a/src/SourceInfo.php b/src/SourceInfo.php index b69d3c7..ed08af5 100644 --- a/src/SourceInfo.php +++ b/src/SourceInfo.php @@ -4,12 +4,26 @@ namespace Kellegous\CodeOwners; +/** + * Represents the source information, file and line number, for any entry + * in a code owners file. + */ final class SourceInfo { + /** + * @var string|null + */ private ?string $filename; + /** + * @var int + */ private int $lineNumber; + /** + * @param int $lineNumber + * @param string|null $filename + */ public function __construct( int $lineNumber, ?string $filename = null @@ -18,11 +32,21 @@ public function __construct( $this->filename = $filename; } + /** + * The filename from which the entry was parsed. If the entry was parsed + * as a string without a filename given, this will return `null`. + * @return string|null + */ public function getFilename(): ?string { return $this->filename; } + /** + * The line number from which the entry was parsed. + * + * @return int + */ public function getLineNumber(): int { return $this->lineNumber;