Files
git.stella-ops.org/src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Ruby/Internal/RubyManifestParser.cs
master 56c687253f
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
feat(ruby): Implement RubyManifestParser for parsing gem groups and dependencies
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
2025-11-10 09:27:03 +02:00

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();
}
}