Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
feat(ruby): Add RubyVendorArtifactCollector to collect vendor artifacts test(deno): Add golden tests for Deno analyzer with various fixtures test(deno): Create Deno module and package files for testing test(deno): Implement Deno lock and import map for dependency management test(deno): Add FFI and worker scripts for Deno testing feat(ruby): Set up Ruby workspace with Gemfile and dependencies feat(ruby): Add expected output for Ruby workspace tests feat(signals): Introduce CallgraphManifest model for signal processing
268 lines
7.8 KiB
C#
268 lines
7.8 KiB
C#
using System.Collections.Immutable;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
|
|
namespace StellaOps.Scanner.Analyzers.Lang.Ruby.Internal;
|
|
|
|
internal static class RubyManifestParser
|
|
{
|
|
private static readonly Regex GemLineRegex = new(@"^\s*gem\s+(?<quote>['""])(?<name>[^'""]+)\k<quote>(?<args>.*)$", RegexOptions.Compiled);
|
|
private static readonly Regex GroupStartRegex = new(@"^\s*group\s+(?<args>.+?)\s+do\b", RegexOptions.Compiled);
|
|
private static readonly Regex InlineGroupRegex = new(@"(group|groups)\s*[:=]>\s*(?<value>\[.*?\]|:[A-Za-z0-9_]+)|(group|groups):\s*(?<typed>\[.*?\]|:[A-Za-z0-9_]+)", RegexOptions.Compiled);
|
|
private static readonly Regex SymbolRegex = new(@":(?<symbol>[A-Za-z0-9_]+)", RegexOptions.Compiled);
|
|
private static readonly Regex StringRegex = new(@"['""](?<value>[A-Za-z0-9_\-]+)['""]", RegexOptions.Compiled);
|
|
|
|
public static IReadOnlyDictionary<string, IReadOnlyCollection<string>> ParseGroups(string manifestPath)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(manifestPath) || !File.Exists(manifestPath))
|
|
{
|
|
return ImmutableDictionary<string, IReadOnlyCollection<string>>.Empty;
|
|
}
|
|
|
|
try
|
|
{
|
|
return ParseInternal(manifestPath);
|
|
}
|
|
catch (IOException)
|
|
{
|
|
return ImmutableDictionary<string, IReadOnlyCollection<string>>.Empty;
|
|
}
|
|
catch (UnauthorizedAccessException)
|
|
{
|
|
return ImmutableDictionary<string, IReadOnlyCollection<string>>.Empty;
|
|
}
|
|
}
|
|
|
|
private static IReadOnlyDictionary<string, IReadOnlyCollection<string>> ParseInternal(string manifestPath)
|
|
{
|
|
var groupStack = new Stack<HashSet<string>>();
|
|
var mapping = new Dictionary<string, HashSet<string>>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
foreach (var rawLine in File.ReadLines(manifestPath))
|
|
{
|
|
var line = StripComment(rawLine);
|
|
if (string.IsNullOrWhiteSpace(line))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (TryBeginGroup(line, groupStack))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (IsGroupEnd(line) && groupStack.Count > 0)
|
|
{
|
|
groupStack.Pop();
|
|
continue;
|
|
}
|
|
|
|
if (!TryParseGem(line, out var gemName, out var inlineGroups))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!mapping.TryGetValue(gemName, out var groups))
|
|
{
|
|
groups = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
mapping[gemName] = groups;
|
|
}
|
|
|
|
if (groupStack.Count > 0)
|
|
{
|
|
foreach (var group in groupStack.Peek())
|
|
{
|
|
groups.Add(group);
|
|
}
|
|
}
|
|
|
|
foreach (var inline in inlineGroups)
|
|
{
|
|
groups.Add(inline);
|
|
}
|
|
|
|
if (groups.Count == 0)
|
|
{
|
|
groups.Add("default");
|
|
}
|
|
}
|
|
|
|
return mapping.ToDictionary(
|
|
static pair => pair.Key,
|
|
static pair => (IReadOnlyCollection<string>)pair.Value
|
|
.OrderBy(static value => value, StringComparer.OrdinalIgnoreCase)
|
|
.ToArray(),
|
|
StringComparer.OrdinalIgnoreCase);
|
|
}
|
|
|
|
private static bool TryBeginGroup(string line, Stack<HashSet<string>> stack)
|
|
{
|
|
var match = GroupStartRegex.Match(line);
|
|
if (!match.Success)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var parsedGroups = ParseGroupTokens(match.Groups["args"].Value);
|
|
var inherited = stack.Count > 0
|
|
? new HashSet<string>(stack.Peek(), StringComparer.OrdinalIgnoreCase)
|
|
: new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
if (parsedGroups.Count == 0 && inherited.Count == 0)
|
|
{
|
|
inherited.Add("default");
|
|
}
|
|
|
|
foreach (var group in parsedGroups)
|
|
{
|
|
inherited.Add(group);
|
|
}
|
|
|
|
stack.Push(inherited);
|
|
return true;
|
|
}
|
|
|
|
private static IReadOnlyCollection<string> ParseGroupTokens(string tokens)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(tokens))
|
|
{
|
|
return Array.Empty<string>();
|
|
}
|
|
|
|
var normalized = tokens.Trim().Trim('(', ')');
|
|
var results = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
foreach (var segment in normalized.Split(',', StringSplitOptions.RemoveEmptyEntries))
|
|
{
|
|
var token = segment.Trim();
|
|
if (token.Length == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var symbolMatch = SymbolRegex.Match(token);
|
|
if (symbolMatch.Success)
|
|
{
|
|
results.Add(symbolMatch.Groups["symbol"].Value);
|
|
continue;
|
|
}
|
|
|
|
var stringMatch = StringRegex.Match(token);
|
|
if (stringMatch.Success)
|
|
{
|
|
results.Add(stringMatch.Groups["value"].Value);
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
private static bool TryParseGem(string line, out string name, out IReadOnlyCollection<string> inlineGroups)
|
|
{
|
|
var match = GemLineRegex.Match(line);
|
|
if (!match.Success)
|
|
{
|
|
name = string.Empty;
|
|
inlineGroups = Array.Empty<string>();
|
|
return false;
|
|
}
|
|
|
|
name = match.Groups["name"].Value.Trim();
|
|
inlineGroups = ExtractInlineGroups(match.Groups["args"].Value);
|
|
return !string.IsNullOrWhiteSpace(name);
|
|
}
|
|
|
|
private static IReadOnlyCollection<string> ExtractInlineGroups(string args)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(args))
|
|
{
|
|
return Array.Empty<string>();
|
|
}
|
|
|
|
var groups = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
foreach (Match match in InlineGroupRegex.Matches(args))
|
|
{
|
|
if (!match.Success)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var valueGroup = match.Groups["value"].Success ? match.Groups["value"] : match.Groups["typed"];
|
|
if (!valueGroup.Success)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var value = valueGroup.Value;
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
foreach (Match symbol in SymbolRegex.Matches(value))
|
|
{
|
|
if (symbol.Success)
|
|
{
|
|
groups.Add(symbol.Groups["symbol"].Value);
|
|
}
|
|
}
|
|
|
|
foreach (Match str in StringRegex.Matches(value))
|
|
{
|
|
if (str.Success)
|
|
{
|
|
groups.Add(str.Groups["value"].Value);
|
|
}
|
|
}
|
|
}
|
|
|
|
return groups;
|
|
}
|
|
|
|
private static bool IsGroupEnd(string line)
|
|
{
|
|
var trimmed = line.Trim();
|
|
if (!trimmed.StartsWith("end", StringComparison.Ordinal))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return trimmed.Length == 3 || char.IsWhiteSpace(trimmed[3]) || trimmed[3] == '#';
|
|
}
|
|
|
|
private static string StripComment(string line)
|
|
{
|
|
if (string.IsNullOrEmpty(line))
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
var builder = new StringBuilder(line.Length);
|
|
var inSingle = false;
|
|
var inDouble = false;
|
|
|
|
for (var i = 0; i < line.Length; i++)
|
|
{
|
|
var ch = line[i];
|
|
|
|
if (ch == '\'' && !inDouble)
|
|
{
|
|
inSingle = !inSingle;
|
|
}
|
|
else if (ch == '"' && !inSingle)
|
|
{
|
|
inDouble = !inDouble;
|
|
}
|
|
|
|
if (ch == '#' && !inSingle && !inDouble)
|
|
{
|
|
break;
|
|
}
|
|
|
|
builder.Append(ch);
|
|
}
|
|
|
|
return builder.ToString();
|
|
}
|
|
}
|