From 1a285efdab2e8b9d839c6debcb7335ca362b1fa5 Mon Sep 17 00:00:00 2001 From: Rory& Date: Wed, 17 Jun 2026 00:56:39 +0200 Subject: [PATCH] Add basic import validator scripts --- scripts/validateImports.cs | 200 +++++++++++++++++++++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100755 scripts/validateImports.cs diff --git a/scripts/validateImports.cs b/scripts/validateImports.cs new file mode 100755 index 000000000..cf3252bdc --- /dev/null +++ b/scripts/validateImports.cs @@ -0,0 +1,200 @@ +#!/usr/bin/env dotnet +#:property Nullable=enable +#:property PublishAOT=false +#:package ArcaneLibs@1.0.1-preview.20260616-220331 + +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; +using ArcaneLibs; +using ArcaneLibs.Attributes; +using ArcaneLibs.Extensions; + +var logJson = false; + +var importErrors = new Dictionary>(); + +void LogError(string fileName, ImportReference import) +{ + importErrors.GetOrCreate(fileName).Add(import); + Console.Write( + $"\n{ConsoleUtils.ColoredString("!", 255, 49, 49)} {ConsoleUtils.ColoredString(fileName + ":" + import.Line + ":" + import.Position, 49 + 128, 49 + 128, 255)}: {(logJson ? import.ToJson(indent: false) : import.ToColorizedString())}"); +} + +foreach (var f in ReadDirRecursive("./src")) +{ + if (f.EndsWith(".test.ts")) + { + } + else + await foreach (var import in GetImports(f)) + { + // if (import.ImportSourceType == ImportSourceType.Npm) + // Console.WriteLine(import.ToJson(indent: false)); + + // file level + if (f == "./src/schemas/Validator.ts") + { + if (import is not { ImportSource: "ajv" or "ajv-formats" or "node:fs" or "node:path" }) + LogError(f, import); + } + + //directory level + else if (f.StartsWith("./src/extensions")) + { + if (import.ImportSourceType == ImportSourceTypeValue.LocalPath) continue; + LogError(f, import); + } + else if (f.StartsWith("./src/database")) + { + if (import.ImportSource == "typeorm" + || import.ImportSourceType == ImportSourceTypeValue.LocalPath + || import.ImportSource == "node:crypto" + || (import.ImportSource == "schemas" && import.IsTypeImport) + || (import.ImportSource == "../Database") // maybe? + ) + continue; + LogError(f, import); + } + else if (f.StartsWith("./src/schemas")) + { + LogError(f, import); + } + else + { + } + } + + if (importErrors.ContainsKey(f)) Console.Write("\n"); +} + +if (importErrors.Any()) + Console.WriteLine($"\rFinished with {importErrors.Sum(x => x.Value.Count)} warnings...\e[K"); + +Console.WriteLine(); +// await foreach (var import in GetImports("./src/apply-migrations.ts")) +// { +// Console.WriteLine(import.ToJson(indent: false)); +// } + +IEnumerable ReadDirRecursive(string path) +{ + foreach (var f in Directory.GetFiles(path)) + yield return f; + + foreach (var d in Directory.GetDirectories(path)) + foreach (var f in ReadDirRecursive(d)) + yield return f; +} + +async IAsyncEnumerable GetImports(string path) +{ + var basicImportRegex = new Regex( + @"^import (?type )?{?(?[a-zA-Z ,\n]+)}? from ""(?.*)"";", + RegexOptions.Multiline + ); + var basicRequireRegex = new Regex( + @"require\(""(?.*)""\)", + RegexOptions.Multiline + ); + // Console.WriteLine(basicImportRegex); + + Console.Write($"\rReading imports for {path}: \e[K"); + var fileContents = await File.ReadAllTextAsync(path); + Console.Write($"{fileContents.Length} chars"); + + // if (basicImportRegex.IsMatch(fileContents)) Console.WriteLine("Match!"); + // else Console.WriteLine(fileContents + "\n^ Did not match regex " + basicImportRegex); + + ImportSourceTypeValue ClassifyImportSourceType(string p) + { + if (p.StartsWith("node:")) return ImportSourceTypeValue.Node; + if (p.StartsWith("@types/")) return ImportSourceTypeValue.Types; + if (p.StartsWith("@spacebar/")) return ImportSourceTypeValue.SpacebarAlias; + if (p.StartsWith("lambert-server")) return ImportSourceTypeValue.VendoredModule; + if (p.StartsWith("../")) return ImportSourceTypeValue.Path; + if (p.StartsWith("./")) return ImportSourceTypeValue.LocalPath; + + return ImportSourceTypeValue.Npm; + } + + ImportReference GetRef(Match m, bool isRequire) => new() + { + IsRequire = isRequire, + IsTypeImport = m.Groups.ContainsKey("typeSpecifier") && m.Groups["typeSpecifier"].Success, + ImportEntities = m.Groups["entities"].Value.ReplaceLineEndings("").Replace(" ", "").Split(","), + ImportSource = m.Groups["source"].Value, + ImportSourceType = ClassifyImportSourceType(m.Groups["source"].Value), + Line = fileContents[..(m.Index)].CountInstances("\n"), + Position = m.Index - fileContents[..(m.Index)].LastIndexOf('\n') + }; + + foreach (Match m in basicImportRegex.Matches(fileContents)) + yield return GetRef(m, false); + + foreach (Match m in basicRequireRegex.Matches(fileContents)) + yield return GetRef(m, true); +} + +struct ImportReference +{ + public int Line { get; set; } + public int Position { get; set; } + public bool IsTypeImport { get; set; } + public bool IsRequire { get; set; } + public string[] ImportEntities { get; set; } + public string ImportSource { get; set; } + public ImportSourceTypeValue ImportSourceType { get; set; } + + public override string ToString() + { + var sourceType = ImportSourceType; + return + $"{ImportSourceType.GetType().GetMember((sourceType.ToString())).First().GetFriendlyName()} {(IsRequire ? "require" : "import")} from {ImportSource}, obtaining {string.Join(", ", ImportEntities)}"; + } + + public string ToColorizedString() + { + var sourceType = ImportSourceType.GetType().GetMember((ImportSourceType.ToString())).First(); + var sourceTypeClr = sourceType.GetColorOrNull(); + return + $"{ConsoleUtils.ColoredString(sourceType.GetFriendlyName(), sourceTypeClr?.R ?? 255, sourceTypeClr?.G ?? 255, sourceTypeClr?.B ?? 255)} " + + $"{(IsRequire ? ConsoleUtils.ColoredString("require", 255, 255, 0) : ConsoleUtils.ColoredString("import", 0, 255, 0))} " + + $"from {ConsoleUtils.ColoredString(ImportSource, sourceTypeClr?.R ?? 255, sourceTypeClr?.G ?? 255, sourceTypeClr?.B ?? 255)}, " + + $"obtaining {(IsRequire ? "" : string.Join(", ", ImportEntities.Select(x => ConsoleUtils.ColoredString(x, 150, 150, 150))))}"; + } +} + +enum ImportType +{ + Unknown, + Import, + ImportType, + Require +} + +[JsonConverter(typeof(JsonStringEnumConverter))] +enum ImportSourceTypeValue +{ + Unknown, + + [FriendlyName(Name = "NPM")] [Color(203, 56, 55)] + Npm, + + [FriendlyName(Name = "NodeJS")] [Color(60, 135, 58)] + Node, + + [FriendlyName(Name = "Types")] [Color(49, 120, 198)] + Types, + + [FriendlyName(Name = "Spacebar internals")] [Color(11, 133, 255)] + SpacebarAlias, + + [FriendlyName(Name = "Vendored module")] [Color(127, 255, 0)] + VendoredModule, + + [FriendlyName(Name = "File path")] [Color(0, 255, 0)] + Path, + + [Color(255, 255, 0)] [FriendlyName(Name = "Local file path")] + LocalPath, +} \ No newline at end of file