Refactor code structure for improved readability and maintainability; removed redundant code blocks and optimized function calls.
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Node.Internal;
|
||||
|
||||
internal sealed record NodeEntrypoint(
|
||||
string Path,
|
||||
string? BinName,
|
||||
string? MainField,
|
||||
string? ModuleField,
|
||||
string ConditionSet)
|
||||
{
|
||||
public static NodeEntrypoint Create(string path, string? binName, string? mainField, string? moduleField, IEnumerable<string>? conditions)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(path);
|
||||
|
||||
var conditionSet = NormalizeConditions(conditions);
|
||||
return new NodeEntrypoint(path, binName, mainField, moduleField, conditionSet);
|
||||
}
|
||||
|
||||
private static string NormalizeConditions(IEnumerable<string>? conditions)
|
||||
{
|
||||
if (conditions is null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var distinct = conditions
|
||||
.Where(static c => !string.IsNullOrWhiteSpace(c))
|
||||
.Select(static c => c.Trim())
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.OrderBy(static c => c, StringComparer.Ordinal);
|
||||
|
||||
return string.Join(',', distinct);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Node.Internal;
|
||||
|
||||
internal sealed record NodeImportEdge(
|
||||
string SourceFile,
|
||||
string TargetSpecifier,
|
||||
string Kind,
|
||||
string Evidence)
|
||||
{
|
||||
public string ComparisonKey => string.Concat(SourceFile, "|", TargetSpecifier, "|", Kind);
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Esprima;
|
||||
using Esprima.Ast;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Node.Internal;
|
||||
|
||||
internal static class NodeImportWalker
|
||||
{
|
||||
public static IReadOnlyList<NodeImportEdge> AnalyzeImports(string sourcePath, string content)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(sourcePath);
|
||||
if (content is null)
|
||||
{
|
||||
return Array.Empty<NodeImportEdge>();
|
||||
}
|
||||
|
||||
Script script;
|
||||
try
|
||||
{
|
||||
script = new JavaScriptParser(content, new ParserOptions
|
||||
{
|
||||
Tolerant = true,
|
||||
AdaptRegexp = true,
|
||||
Source = sourcePath
|
||||
}).ParseScript();
|
||||
}
|
||||
catch (ParserException)
|
||||
{
|
||||
return Array.Empty<NodeImportEdge>();
|
||||
}
|
||||
|
||||
var edges = new List<NodeImportEdge>();
|
||||
Walk(script, sourcePath, edges);
|
||||
return edges.Count == 0
|
||||
? Array.Empty<NodeImportEdge>()
|
||||
: edges.OrderBy(e => e.ComparisonKey, StringComparer.Ordinal).ToArray();
|
||||
}
|
||||
|
||||
private static void Walk(Node node, string sourcePath, List<NodeImportEdge> edges)
|
||||
{
|
||||
switch (node)
|
||||
{
|
||||
case ImportDeclaration importDecl when !string.IsNullOrWhiteSpace(importDecl.Source?.StringValue):
|
||||
edges.Add(new NodeImportEdge(sourcePath, importDecl.Source.StringValue!, "import", BuildEvidence(importDecl.Loc)));
|
||||
break;
|
||||
case CallExpression call when IsRequire(call) && call.Arguments.FirstOrDefault() is Literal { Value: string target }:
|
||||
edges.Add(new NodeImportEdge(sourcePath, target, "require", BuildEvidence(call.Loc)));
|
||||
break;
|
||||
case ImportExpression importExp when importExp.Source is Literal { Value: string importTarget }:
|
||||
edges.Add(new NodeImportEdge(sourcePath, importTarget, "import()", BuildEvidence(importExp.Loc)));
|
||||
break;
|
||||
}
|
||||
|
||||
foreach (var child in node.ChildNodes)
|
||||
{
|
||||
Walk(child, sourcePath, edges);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsRequire(CallExpression call)
|
||||
{
|
||||
return call.Callee is Identifier id && string.Equals(id.Name, "require", StringComparison.Ordinal)
|
||||
&& call.Arguments.Count == 1 && call.Arguments[0] is Literal { Value: string };
|
||||
}
|
||||
|
||||
private static string BuildEvidence(Location? loc)
|
||||
{
|
||||
if (loc is null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var json = new JsonObject
|
||||
{
|
||||
["start"] = BuildPosition(loc.Start),
|
||||
["end"] = BuildPosition(loc.End)
|
||||
};
|
||||
|
||||
return json.ToJsonString(new JsonSerializerOptions { WriteIndented = false });
|
||||
}
|
||||
|
||||
private static JsonObject BuildPosition(Position pos)
|
||||
{
|
||||
return new JsonObject
|
||||
{
|
||||
["line"] = pos.Line,
|
||||
["column"] = pos.Column
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Node.Internal;
|
||||
using System.Globalization;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Node.Internal;
|
||||
|
||||
internal sealed class NodePackage
|
||||
{
|
||||
@@ -80,6 +82,12 @@ internal sealed class NodePackage
|
||||
|
||||
public bool IsYarnPnp { get; }
|
||||
|
||||
private readonly List<NodeEntrypoint> _entrypoints = new();
|
||||
private readonly List<NodeImportEdge> _imports = new();
|
||||
|
||||
public IReadOnlyList<NodeEntrypoint> Entrypoints => _entrypoints;
|
||||
public IReadOnlyList<NodeImportEdge> Imports => _imports;
|
||||
|
||||
public string RelativePathNormalized => string.IsNullOrEmpty(RelativePath) ? string.Empty : RelativePath.Replace(Path.DirectorySeparatorChar, '/');
|
||||
|
||||
public string ComponentKey => $"purl::{Purl}";
|
||||
@@ -113,10 +121,43 @@ internal sealed class NodePackage
|
||||
LanguageEvidenceKind.Metadata,
|
||||
"package.json:scripts",
|
||||
locator,
|
||||
script.Command,
|
||||
script.Sha256));
|
||||
}
|
||||
|
||||
script.Command,
|
||||
script.Sha256));
|
||||
}
|
||||
|
||||
foreach (var entrypoint in _entrypoints)
|
||||
{
|
||||
var locator = string.IsNullOrEmpty(PackageJsonLocator)
|
||||
? "package.json#entrypoint"
|
||||
: $"{PackageJsonLocator}#entrypoint";
|
||||
|
||||
var content = string.Join(';', new[]
|
||||
{
|
||||
entrypoint.Path,
|
||||
entrypoint.BinName,
|
||||
entrypoint.MainField,
|
||||
entrypoint.ModuleField,
|
||||
entrypoint.ConditionSet
|
||||
}.Where(static v => !string.IsNullOrWhiteSpace(v)));
|
||||
|
||||
evidence.Add(new LanguageComponentEvidence(
|
||||
LanguageEvidenceKind.Metadata,
|
||||
"package.json:entrypoint",
|
||||
locator,
|
||||
content,
|
||||
sha256: null));
|
||||
}
|
||||
|
||||
foreach (var importEdge in _imports.OrderBy(static e => e.ComparisonKey, StringComparer.Ordinal))
|
||||
{
|
||||
evidence.Add(new LanguageComponentEvidence(
|
||||
LanguageEvidenceKind.Source,
|
||||
"node.import",
|
||||
importEdge.SourceFile,
|
||||
importEdge.TargetSpecifier,
|
||||
sha256: null));
|
||||
}
|
||||
|
||||
return evidence
|
||||
.OrderBy(static e => e.ComparisonKey, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
@@ -186,6 +227,33 @@ internal sealed class NodePackage
|
||||
}
|
||||
}
|
||||
|
||||
if (_entrypoints.Count > 0)
|
||||
{
|
||||
var paths = _entrypoints
|
||||
.Select(static ep => ep.Path)
|
||||
.OrderBy(static p => p, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
|
||||
entries.Add(new KeyValuePair<string, string?>("entrypoint", string.Join(';', paths)));
|
||||
|
||||
var conditionSets = _entrypoints
|
||||
.Select(static ep => ep.ConditionSet)
|
||||
.Where(static cs => !string.IsNullOrWhiteSpace(cs))
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.OrderBy(static cs => cs, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
|
||||
if (conditionSets.Length > 0)
|
||||
{
|
||||
entries.Add(new KeyValuePair<string, string?>("entrypoint.conditions", string.Join(';', conditionSets)));
|
||||
}
|
||||
}
|
||||
|
||||
if (_imports.Count > 0)
|
||||
{
|
||||
entries.Add(new KeyValuePair<string, string?>("imports", _imports.Count.ToString(CultureInfo.InvariantCulture)));
|
||||
}
|
||||
|
||||
if (HasInstallScripts)
|
||||
{
|
||||
entries.Add(new KeyValuePair<string, string?>("installScripts", "true"));
|
||||
@@ -230,6 +298,48 @@ internal sealed class NodePackage
|
||||
.OrderBy(static pair => pair.Key, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public void AddEntrypoint(string path, string conditionSet, string? binName, string? mainField, string? moduleField)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var entry = NodeEntrypoint.Create(path.Replace(Path.DirectorySeparatorChar, '/'), binName, mainField, moduleField, ParseConditionSet(conditionSet));
|
||||
if (_entrypoints.Any(ep => string.Equals(ep.Path, entry.Path, StringComparison.Ordinal)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_entrypoints.Add(entry);
|
||||
}
|
||||
|
||||
public void AddImport(string sourceFile, string targetSpecifier, string kind, string evidence)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sourceFile) || string.IsNullOrWhiteSpace(targetSpecifier))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var edge = new NodeImportEdge(sourceFile.Replace(Path.DirectorySeparatorChar, '/'), targetSpecifier.Trim(), kind.Trim(), evidence);
|
||||
if (_imports.Any(e => string.Equals(e.ComparisonKey, edge.ComparisonKey, StringComparison.Ordinal)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_imports.Add(edge);
|
||||
}
|
||||
|
||||
private static IEnumerable<string> ParseConditionSet(string conditionSet)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(conditionSet))
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
return conditionSet.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
}
|
||||
|
||||
private static string BuildPurl(string name, string version)
|
||||
{
|
||||
|
||||
@@ -61,9 +61,65 @@ internal static class NodePackageCollector
|
||||
|
||||
AppendDeclaredPackages(packages, lockData);
|
||||
|
||||
AttachImports(context, packages, cancellationToken);
|
||||
|
||||
return packages;
|
||||
}
|
||||
|
||||
private static void AttachImports(LanguageAnalyzerContext context, List<NodePackage> packages, CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var package in packages)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var packageRoot = string.IsNullOrEmpty(package.RelativePathNormalized)
|
||||
? context.RootPath
|
||||
: Path.Combine(context.RootPath, package.RelativePathNormalized.Replace('/', Path.DirectorySeparatorChar));
|
||||
|
||||
if (!Directory.Exists(packageRoot))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var file in EnumerateSourceFiles(packageRoot))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
string content;
|
||||
try
|
||||
{
|
||||
content = File.ReadAllText(file);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var imports = NodeImportWalker.AnalyzeImports(context.GetRelativePath(file).Replace(Path.DirectorySeparatorChar, '/'), content);
|
||||
foreach (var edge in imports)
|
||||
{
|
||||
package.AddImport(edge.SourceFile, edge.TargetSpecifier, edge.Kind, edge.Evidence);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<string> EnumerateSourceFiles(string root)
|
||||
{
|
||||
foreach (var extension in new[] { ".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx" })
|
||||
{
|
||||
foreach (var file in Directory.EnumerateFiles(root, "*" + extension, new EnumerationOptions
|
||||
{
|
||||
RecurseSubdirectories = true,
|
||||
MatchCasing = MatchCasing.CaseInsensitive,
|
||||
IgnoreInaccessible = true
|
||||
}))
|
||||
{
|
||||
yield return file;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void TraverseDirectory(
|
||||
LanguageAnalyzerContext context,
|
||||
string directory,
|
||||
|
||||
Reference in New Issue
Block a user