From 1115ab0e0896ceb049ca1fd4d1258f2441360f9b Mon Sep 17 00:00:00 2001 From: bdovaz Date: Thu, 25 Sep 2025 11:58:36 +0200 Subject: [PATCH 01/29] feat: major improvements --- .config/dotnet-tools.json | 2 +- .editorconfig | 201 +++++++++++++++ .gitignore | 436 ++++++++++++++++++++++++++++++++- DocFxConfiguration.cs | 10 + DocFxConfigurationBuild.cs | 10 + Git.cs | 8 +- LICENSE => LICENSE.txt | 0 Program.cs | 259 +++++++++++++------- Properties/launchSettings.json | 9 + README.md | 2 +- RepositoryExtensions.cs | 25 +- UnityXrefMaps.csproj | 31 ++- UnityXrefMaps.sln | 25 ++ Utils.cs | 46 ++-- XrefMap.cs | 51 ++-- XrefMapReference.cs | 112 +++++---- docfx.json | 3 +- 17 files changed, 1012 insertions(+), 218 deletions(-) create mode 100644 .editorconfig create mode 100644 DocFxConfiguration.cs create mode 100644 DocFxConfigurationBuild.cs rename LICENSE => LICENSE.txt (100%) create mode 100644 Properties/launchSettings.json create mode 100644 UnityXrefMaps.sln diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index a873b6b..be28e25 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "docfx": { - "version": "2.67.5", + "version": "2.78.3", "commands": [ "docfx" ] diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..2ca9e89 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,201 @@ +# editorconfig.org + +# top-most EditorConfig file +root = true + +# Default settings: +# A newline ending every file +# Use 4 spaces as indentation +[*] +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +# Specify UTF-8 without byte-order mark +[*.{csproj,locproj,nativeproj,proj,resx,slnx,vbproj}] +charset = utf-8 + +# Generated code +[*{_AssemblyInfo.cs,.notsupported.cs,AsmOffsets.cs}] +generated_code = true + +# C# files +[*.cs] +# New line preferences +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = false +csharp_indent_switch_labels = true +csharp_indent_labels = one_less_than_current + +# Modifier preferences +csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async:suggestion + +# avoid this. unless absolutely necessary +dotnet_style_qualification_for_field = false:suggestion +dotnet_style_qualification_for_property = false:suggestion +dotnet_style_qualification_for_method = false:suggestion +dotnet_style_qualification_for_event = false:suggestion + +# Types: use keywords instead of BCL types, and permit var only when the type is clear +csharp_style_var_for_built_in_types = false:suggestion +csharp_style_var_when_type_is_apparent = false:none +csharp_style_var_elsewhere = false:suggestion +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion + +# name all constant fields using PascalCase +dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style +dotnet_naming_symbols.constant_fields.applicable_kinds = field +dotnet_naming_symbols.constant_fields.required_modifiers = const +dotnet_naming_style.pascal_case_style.capitalization = pascal_case + +# static fields should have s_ prefix +dotnet_naming_rule.static_fields_should_have_prefix.severity = suggestion +dotnet_naming_rule.static_fields_should_have_prefix.symbols = static_fields +dotnet_naming_rule.static_fields_should_have_prefix.style = static_prefix_style +dotnet_naming_symbols.static_fields.applicable_kinds = field +dotnet_naming_symbols.static_fields.required_modifiers = static +dotnet_naming_symbols.static_fields.applicable_accessibilities = private, internal, private_protected +dotnet_naming_style.static_prefix_style.required_prefix = s_ +dotnet_naming_style.static_prefix_style.capitalization = camel_case + +# internal and private fields should be _camelCase +dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion +dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields +dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style +dotnet_naming_symbols.private_internal_fields.applicable_kinds = field +dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal +dotnet_naming_style.camel_case_underscore_style.required_prefix = _ +dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case + +# Code style defaults +csharp_using_directive_placement = outside_namespace:suggestion +dotnet_sort_system_directives_first = true +csharp_prefer_braces = true:silent +csharp_preserve_single_line_blocks = true:none +csharp_preserve_single_line_statements = false:none +csharp_prefer_static_local_function = true:suggestion +csharp_prefer_simple_using_statement = false:none +csharp_style_prefer_switch_expression = true:suggestion +dotnet_style_readonly_field = true:suggestion + +# Expression-level preferences +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_collection_expression = when_types_exactly_match +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_auto_properties = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +csharp_prefer_simple_default_expression = true:suggestion + +# Expression-bodied members +csharp_style_expression_bodied_methods = true:silent +csharp_style_expression_bodied_constructors = true:silent +csharp_style_expression_bodied_operators = true:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = true:silent + +# Pattern matching +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion + +# Null checking preferences +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion + +# Other features +csharp_style_prefer_index_operator = false:none +csharp_style_prefer_range_operator = false:none +csharp_style_pattern_local_over_anonymous_function = false:none + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = do_not_ignore +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# License header +file_header_template = Licensed to the .NET Foundation under one or more agreements.\nThe .NET Foundation licenses this file to you under the MIT license. + +[src/libraries/System.Net.Http/src/System/Net/Http/{SocketsHttpHandler/Http3RequestStream.cs,BrowserHttpHandler/BrowserHttpHandler.cs}] +# disable CA2025, the analyzer throws a NullReferenceException when processing this file: https://github.com/dotnet/roslyn-analyzers/issues/7652 +dotnet_diagnostic.CA2025.severity = none + +# C++ Files +[*.{cpp,h,in}] +curly_bracket_next_line = true +indent_brace_style = Allman + +# Xml project files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}] +indent_size = 2 + +# Xml build files +[*.builds] +indent_size = 2 + +# Xml files +[*.{resx,ruleset,slnx,stylecop,xml}] +indent_size = 2 + +# Xml resource files +[*.resx] +# match Visual Studio behavior +insert_final_newline = false +trim_trailing_whitespace = false + +# Xml config files +[*.{props,targets,config,nuspec}] +indent_size = 2 + +# Data serialization +[*.{json,yaml,yml}] +indent_size = 2 + +# Shell scripts +[*.sh] +end_of_line = lf +[*.{cmd,bat}] +end_of_line = crlf \ No newline at end of file diff --git a/.gitignore b/.gitignore index c58b5fd..ba7a514 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,428 @@ -.vscode/ -_site/ -bin/ -gh-pages/ -obj/ -Temp/ -UnityCsReference/ -*.cache \ No newline at end of file +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates +*.env + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ + +[Dd]ebug/x64/ +[Dd]ebugPublic/x64/ +[Rr]elease/x64/ +[Rr]eleases/x64/ +bin/x64/ +obj/x64/ + +[Dd]ebug/x86/ +[Dd]ebugPublic/x86/ +[Rr]elease/x86/ +[Rr]eleases/x86/ +bin/x86/ +obj/x86/ + +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +[Aa][Rr][Mm]64[Ee][Cc]/ +bld/ +[Oo]bj/ +[Oo]ut/ +[Ll]og/ +[Ll]ogs/ + +# Build results on 'Bin' directories +**/[Bb]in/* +# Uncomment if you have tasks that rely on *.refresh files to move binaries +# (https://github.com/github/gitignore/pull/3736) +#!**/[Bb]in/*.refresh + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* +*.trx + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Approval Tests result files +*.received.* + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.idb +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +# but not Directory.Build.rsp, as it configures directory-level build defaults +!Directory.Build.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +**/.paket/paket.exe +paket-files/ + +# FAKE - F# Make +**/.fake/ + +# CodeRush personal settings +**/.cr/personal + +# Python Tools for Visual Studio (PTVS) +**/__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +#tools/** +#!tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog +MSBuild_Logs/ + +# AWS SAM Build and Temporary Artifacts folder +.aws-sam + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +**/.mfractor/ + +# Local History for Visual Studio +**/.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +**/.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp \ No newline at end of file diff --git a/DocFxConfiguration.cs b/DocFxConfiguration.cs new file mode 100644 index 0000000..9b65faa --- /dev/null +++ b/DocFxConfiguration.cs @@ -0,0 +1,10 @@ +using System.Text.Json.Serialization; + +namespace DocFxForUnity; + +// https://github.com/dotnet/docfx/blob/main/src/Docfx.App/Config/DocfxConfig.cs +public class DocFxConfiguration +{ + [JsonPropertyName("build")] + public DocFxConfigurationBuild? Build { get; set; } +} diff --git a/DocFxConfigurationBuild.cs b/DocFxConfigurationBuild.cs new file mode 100644 index 0000000..72a412a --- /dev/null +++ b/DocFxConfigurationBuild.cs @@ -0,0 +1,10 @@ +using System.Text.Json.Serialization; + +namespace DocFxForUnity; + +// https://github.com/dotnet/docfx/blob/main/src/Docfx.App/Config/BuildJsonConfig.cs +public class DocFxConfigurationBuild +{ + [JsonPropertyName("dest")] + public string? Destination { get; set; } +} diff --git a/Git.cs b/Git.cs index 628e837..8b4bfd1 100644 --- a/Git.cs +++ b/Git.cs @@ -4,7 +4,7 @@ namespace DocFxForUnity { - public sealed class Git + public static class Git { /// /// Fetches changes and hard resets the specified repository to the latest commit of a specified branch. If no @@ -36,11 +36,11 @@ public static Repository GetSyncRepository(string sourceUrl, string path, string repository.RemoveUntrackedFiles(); Console.WriteLine($"Fetching changes from 'origin' in '{path}'"); - var remote = repository.Network.Remotes["origin"]; - Commands.Fetch(repository, remote.Name, Array.Empty(), null, null); // WTF is this API libgit2sharp? + Remote remote = repository.Network.Remotes["origin"]; + Commands.Fetch(repository, remote.Name, [], null, null); // WTF is this API libgit2sharp? Console.WriteLine($"Checking out '{path}' to '{branch}' branch"); - var remoteBranch = $"origin/{branch}"; + string remoteBranch = $"origin/{branch}"; Commands.Checkout(repository, remoteBranch); } diff --git a/LICENSE b/LICENSE.txt similarity index 100% rename from LICENSE rename to LICENSE.txt diff --git a/Program.cs b/Program.cs index 66b2c59..33fc2bf 100644 --- a/Program.cs +++ b/Program.cs @@ -1,8 +1,9 @@ using System; -using System.Collections.Generic; +using System.CommandLine; using System.IO; -using System.Linq; +using System.Text.Json; using System.Text.RegularExpressions; +using System.Threading.Tasks; using LibGit2Sharp; namespace DocFxForUnity @@ -14,115 +15,207 @@ namespace DocFxForUnity /// /// /// - /// [.NET](https://dotnet.microsoft.com) >= 7.0 and [DocFX](https://dotnet.github.io/docfx/) must be installed + /// [.NET](https://dotnet.microsoft.com) >= 9.0 and [DocFX](https://dotnet.github.io/docfx/) must be installed /// on your system. /// partial class Program { /// - /// The path where the documentation of the Unity repository will be generated. + /// Gets the default URL of the online API documentation of Unity. /// - private const string GeneratedDocsPath = $"{UnityRepoPath}/_site"; + private const string DefaultUnityApiUrl = "https://docs.unity3d.com/{0}/Documentation/ScriptReference/"; /// - /// The path of the xref map generated by DocFX. + /// The default path of the Unity repository. /// - private static readonly string GeneratedXrefMapPath = Path.Combine(GeneratedDocsPath, XrefMapFileName); + private const string DefaultUnityRepositoryPath = "UnityCsReference"; /// - /// The path of the default xref map, pointing at . + /// The default URL of the Unity repository. /// - private static readonly string DefaultXrefMapPath = Path.Combine(XrefMapsPath, XrefMapFileName); + private const string DefaultUnityRepositoryUrl = "https://github.com/Unity-Technologies/UnityCsReference.git"; /// - /// Gets the URL of the online API documentation of Unity. + /// The default branch of the Unity repository. /// - private const string UnityApiUrl = "https://docs.unity3d.com/ScriptReference/"; - - [GeneratedRegex("\\d{4}\\.\\d")] - private static partial Regex UnityVersionRegex(); - - /// - /// The path of the Unity repository. - /// - private const string UnityRepoPath = "UnityCsReference"; - + private const string DefaultUnityRepositoryBranch = "master"; + + // https://github.com/dotnet/docfx/blob/1c4e9ff4a2d236206eee04066847a98343c6a3f7/src/Docfx.Build/XRefMaps/XRefArchive.cs#L14 /// - /// The URL of the Unity repository. + /// The default xref map filename. /// - private const string UnityRepoUrl = "https://github.com/Unity-Technologies/UnityCsReference.git"; + private const string DefaultXrefMapFileName = "xrefmap.yml"; /// - /// The xref map filename. + /// The default path where to copy the xref maps. /// - private const string XrefMapFileName = "xrefmap.yml"; + private const string DefaultXrefMapsPath = $"_site/{{0}}/{DefaultXrefMapFileName}"; /// - /// The path where to copy the xref maps. + /// The default DocFX config file path. /// - private const string XrefMapsPath = "_site"; + private const string DefaultDocFxConfigurationFilePath = "docfx.json"; /// /// Entry point of this program. /// - public static void Main() - { - Console.WriteLine($"Sync the Unity repository in '{UnityRepoPath}'"); - using var unityRepo = Git.GetSyncRepository(UnityRepoUrl, UnityRepoPath, branch: "master"); - - var versions = GetLatestVersions(unityRepo); - var latestVersion = versions - .OrderByDescending(version => version.name) - .First(version => version.release.Contains('f')); - - foreach (var version in versions) - { - Console.WriteLine($"Generating Unity '{version.name}' xref map"); - unityRepo.HardReset(version.release); - string xrefMapPath = Path.Combine(XrefMapsPath, version.name, XrefMapFileName); // .//xrefmap.yml - - Console.WriteLine($"Running DocFX on '{version.release}'"); - Utils.RunCommand("dotnet", "docfx", Console.WriteLine, Console.WriteLine); - - if (!File.Exists(GeneratedXrefMapPath)) - { - Console.WriteLine($"Error: '{GeneratedXrefMapPath}' for Unity '{version.name}' not generated"); - Console.WriteLine("\n"); - continue; - } - - Console.WriteLine($"Fixing hrefs in '{xrefMapPath}'"); - Utils.CopyFile(GeneratedXrefMapPath, xrefMapPath); - var xrefMap = XrefMap.Load(xrefMapPath); - xrefMap.FixHrefs(apiUrl: $"https://docs.unity3d.com/{version.name}/Documentation/ScriptReference/"); - xrefMap.Save(xrefMapPath); - - // Set the last version's xref map as the default one - if (version == latestVersion) - { - Console.WriteLine($"Fixing hrefs in '{DefaultXrefMapPath}'"); - Utils.CopyFile(GeneratedXrefMapPath, DefaultXrefMapPath); - xrefMap = XrefMap.Load(DefaultXrefMapPath); - xrefMap.FixHrefs(UnityApiUrl); - xrefMap.Save(DefaultXrefMapPath); - } - - Console.WriteLine("\n"); - } + public static async Task Main(string[] args) + { + RootCommand rootCommand = []; + + Option repositoryUrlOption = new("--repositoryUrl") + { + Description = "The Git repository url.", + DefaultValueFactory = _ => DefaultUnityRepositoryUrl + }; + Option repositoryBranchOption = new("--repositoryBranch") + { + Description = "The Git repository branch.", + DefaultValueFactory = _ => DefaultUnityRepositoryBranch + }; + Option repositoryPathOption = new("--repositoryPath") + { + Description = "The Git repository path to git clone. " + + "If the clone has already been made, it will reset so that the clone can be reused.", + DefaultValueFactory = _ => DefaultUnityRepositoryPath + }; + Option repositoryTagsOption = new("--repositoryTags") + { + Description = "The repository tags to use to generate the documentation. " + + "That is, the versions of the Unity editor (6000.0.1f1, 6000.1.1f1) or the versions of the Unity package (1.0.0, 1.1.0).", + Required = true + }; + Option apiUrlOption = new("--apiUrl") + { + Description = "The root path of the Unity editor API documentation or the Unity package. " + + "{0} is replaced with the Unity editor short version (example: https://docs.unity3d.com/6000.0/Documentation/ScriptReference/) or " + + "the Unity package short version (example: https://docs.unity3d.com/Packages/com.unity.inputsystem@1.0/api/)}) in the case of a package.", + DefaultValueFactory = _ => DefaultUnityApiUrl + }; + Option docFxConfigurationFilePathOption = new("--docFxConfigurationFilePath") + { + Description = "The path to the DocFX configuration file.", + DefaultValueFactory = _ => DefaultDocFxConfigurationFilePath + }; + Option xrefMapsPathOption = new("--xrefMapsPath") + { + Description = $"The path where the final {DefaultXrefMapFileName} files will be generated. " + + $"{{0}} is replaced with the Unity editor version (example: {string.Format(DefaultXrefMapsPath, "6000.0.1f1")}) or " + + $"the Unity package version (example: {string.Format(DefaultXrefMapsPath, "1.0.0")}) in the case of a package.", + DefaultValueFactory = _ => DefaultXrefMapsPath + }; + + rootCommand.Options.Add(repositoryUrlOption); + rootCommand.Options.Add(repositoryBranchOption); + rootCommand.Options.Add(repositoryPathOption); + rootCommand.Options.Add(repositoryTagsOption); + rootCommand.Options.Add(apiUrlOption); + rootCommand.Options.Add(docFxConfigurationFilePathOption); + rootCommand.Options.Add(xrefMapsPathOption); + + rootCommand.SetAction(async (parseResult, cancellationToken) => + { + bool result = true; + + string? repositoryUrl = parseResult.GetValue(repositoryUrlOption); + string? repositoryBranch = parseResult.GetValue(repositoryBranchOption); + string? repositoryPath = parseResult.GetValue(repositoryPathOption); + string[]? repositoryTags = parseResult.GetValue(repositoryTagsOption); + string? apiUrl = parseResult.GetValue(apiUrlOption); + string? docFxFilePath = parseResult.GetValue(docFxConfigurationFilePathOption); + string? xrefMapsPath = parseResult.GetValue(xrefMapsPathOption); + + using Stream docFxStream = File.OpenRead(docFxFilePath!); + + DocFxConfiguration? docFxConfiguration = await JsonSerializer.DeserializeAsync(docFxStream, cancellationToken: cancellationToken); + + string generatedDocsPath = docFxConfiguration!.Build!.Destination!; + string generatedXrefMapPath = Path.Combine(generatedDocsPath, DefaultXrefMapFileName!); + + Console.WriteLine($"Sync the Unity repository in '{Path.GetFullPath(repositoryPath!)}'"); + + using Repository repository = Git.GetSyncRepository(repositoryUrl!, repositoryPath!, repositoryBranch!); + + foreach (string repositoryTag in repositoryTags!) + { + Match versionMatch = VersionRegex().Match(repositoryTag); + + string shortVersion = $"{versionMatch.Groups["majorVersion"].Value}.{versionMatch.Groups["minorVersion"].Value}"; + + Console.WriteLine($"Generating Unity '{shortVersion}' xref map"); + + repository.HardReset(repositoryTag); + + string xrefMapPath = string.Format(xrefMapsPath!, repositoryTag); // .//xrefmap.yml + + Console.WriteLine($"Running DocFX on '{repositoryTag}'"); + + await Utils.RunCommand("dotnet", $"docfx {docFxFilePath}", Console.WriteLine, Console.WriteLine, cancellationToken); + + if (!File.Exists(generatedXrefMapPath)) + { + result = false; + + Console.Error.WriteLine($"Error: '{generatedXrefMapPath}' for Unity '{repositoryTag}' not generated"); + Console.Error.WriteLine("\n"); + + continue; + } + + Console.WriteLine($"Fixing hrefs in '{Path.GetFullPath(xrefMapPath)}'"); + + await Utils.CopyFile(generatedXrefMapPath, xrefMapPath, cancellationToken); + + XrefMap xrefMap = await XrefMap.Load(xrefMapPath, cancellationToken); + + xrefMap.FixHrefs(apiUrl: string.Format(apiUrl!, shortVersion)); + + await xrefMap.Save(xrefMapPath, cancellationToken); + + Console.WriteLine("\n"); + } + + return result ? 0 : 1; + }); + + Option xrefPathOption = new("--xrefPath") + { + Description = $"The path to the {DefaultXrefMapFileName} file.", + Required = true + }; + + Command testCommand = new("test", $"Check that the links in the {DefaultXrefMapFileName} file are valid.") + { + xrefPathOption + }; + + testCommand.SetAction(async (parseResult, cancellationToken) => + { + bool result = true; + + string? xrefPath = parseResult.GetValue(xrefPathOption); + + XrefMap xrefMap = await XrefMap.Load(xrefPath!, cancellationToken); + + foreach (XrefMapReference reference in xrefMap.References!) + { + if (!await Utils.TestUriExists(reference.Href, cancellationToken)) + { + result = false; + + Console.WriteLine($"Warning: invalid URL {reference.Href} for {reference.Uid} uid"); + } + } + + return result ? 0 : 1; + }); + + rootCommand.Subcommands.Add(testCommand); + + await rootCommand.Parse(args).InvokeAsync(); } - /// - /// Returns a collection of the latest versions of a specified repository of Unity. - /// - /// The repository of Unity to use. - /// The latest versions. - private static IEnumerable<(string name, string release)> GetLatestVersions(Repository unityRepository) - { - return unityRepository - .GetTags() - .Select(release => (name: UnityVersionRegex().Match(release).Value, release)) - .GroupBy(version => version.name) - .Select(version => version.First()); - } + [GeneratedRegex(@"(?\d+)\.(?\d+)\.(?\d+)")] + private static partial Regex VersionRegex(); } } diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json new file mode 100644 index 0000000..261a301 --- /dev/null +++ b/Properties/launchSettings.json @@ -0,0 +1,9 @@ +{ + "profiles": { + "DocFxForUnity": { + "commandName": "Project", + "commandLineArgs": "--repositoryTags 6000.0.1f1 --repositoryTags 6000.1.1f1" + } + }, + "$schema": "http://json.schemastore.org/launchsettings.json" +} diff --git a/README.md b/README.md index 75a83a0..9e2fe2c 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ DocFX will set clickable all the references of the Unity API on your documentati - To run this program: 1. Install Visual Studio 2022. - 2. Install [.NET 7.0](https://dotnet.microsoft.com/download/dotnet) SDK. + 2. Install [.NET 9.0](https://dotnet.microsoft.com/download/dotnet) SDK. 3. Clone this repository on your computer. 4. Open a terminal on the cloned repository and run: diff --git a/RepositoryExtensions.cs b/RepositoryExtensions.cs index 8eb8b10..72c0e2b 100644 --- a/RepositoryExtensions.cs +++ b/RepositoryExtensions.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using System.Linq; +using System; using LibGit2Sharp; namespace DocFxForUnity; @@ -9,31 +8,23 @@ namespace DocFxForUnity; /// public static class RepositoryExtensions { - /// - /// Returns a collection of the latest tags of a specified . - /// - /// The to use. - /// The collection of tags. - public static IEnumerable GetTags(this Repository repository) - { - return repository.Tags - .OrderByDescending(tag => (tag.Target as Commit).Author.When) - .Select(tag => tag.FriendlyName); - } - /// /// Hard resets the specified to the specified commit. /// /// The to hard reset to . /// The name of the commit where to reset . public static void HardReset(this Repository repository, string commit) - { + { + Console.WriteLine($"Hard reset to {commit}"); + repository.Reset(ResetMode.Hard, commit); try - { + { + Console.WriteLine($"Removing untracked files"); + repository.RemoveUntrackedFiles(); } - catch (System.Exception) { } + catch (Exception) { } } } \ No newline at end of file diff --git a/UnityXrefMaps.csproj b/UnityXrefMaps.csproj index 4dca7f2..6945a80 100644 --- a/UnityXrefMaps.csproj +++ b/UnityXrefMaps.csproj @@ -1,14 +1,37 @@ - + Exe - net7.0 + net9.0 + enable + DocFxForUnity + DocFxForUnity + true + docFxForUnity + LICENSE.txt + README.md + https://github.com/NormandErwan/UnityXrefMaps $(DefaultItemExcludes);UnityCsReference*\** - - + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + diff --git a/UnityXrefMaps.sln b/UnityXrefMaps.sln new file mode 100644 index 0000000..cc96728 --- /dev/null +++ b/UnityXrefMaps.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36511.14 d17.14 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnityXrefMaps", "UnityXrefMaps.csproj", "{97BE34CD-6988-1F05-D1B2-EBF906DAA098}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {97BE34CD-6988-1F05-D1B2-EBF906DAA098}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {97BE34CD-6988-1F05-D1B2-EBF906DAA098}.Debug|Any CPU.Build.0 = Debug|Any CPU + {97BE34CD-6988-1F05-D1B2-EBF906DAA098}.Release|Any CPU.ActiveCfg = Release|Any CPU + {97BE34CD-6988-1F05-D1B2-EBF906DAA098}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {FCDA8159-7FB5-4653-8EBE-A68917137BBC} + EndGlobalSection +EndGlobal diff --git a/Utils.cs b/Utils.cs index b4ba405..d8431d5 100644 --- a/Utils.cs +++ b/Utils.cs @@ -2,43 +2,33 @@ using System.Diagnostics; using System.IO; using System.Net.Http; +using System.Threading; using System.Threading.Tasks; namespace DocFxForUnity { - public sealed class Utils + public static class Utils { /// /// Client for send HTTP requests and receiving HTTP responses. /// - private static readonly HttpClient httpClient = new(); + private static readonly HttpClient s_httpClient = new(); /// /// Copy a source file to a destination file. Intermediate folders will be automatically created. /// /// The path of the source file to copy. /// The destination path of the copied file. - public static void CopyFile(string sourcePath, string destPath) + public static async Task CopyFile(string sourcePath, string destPath, CancellationToken cancellationToken = default) { - var destDirectoryPath = Path.GetDirectoryName(destPath); - Directory.CreateDirectory(destDirectoryPath); + string? destDirectoryPath = Path.GetDirectoryName(destPath); - File.Copy(sourcePath, destPath, overwrite: true); - } + Directory.CreateDirectory(destDirectoryPath!); - /// - /// Deletes the specified directories if they exist. - /// - /// The path of the directories to delete. - public static void DeleteDirectories(params string[] paths) - { - foreach (var path in paths) - { - if (Directory.Exists(path)) - { - Directory.Delete(path, recursive: true); - } - } + using Stream source = File.OpenRead(sourcePath); + using Stream destination = File.Create(destPath); + + await source.CopyToAsync(destination, cancellationToken); } /// @@ -48,7 +38,7 @@ public static void DeleteDirectories(params string[] paths) /// The arguments of the command. /// The function to call with the output data of the command. /// The function to call with the error data of the command. - public static void RunCommand(string command, string arguments, Action output, Action error) + public static async Task RunCommand(string command, string arguments, Action output, Action error, CancellationToken cancellationToken = default) { using var process = new Process(); process.StartInfo = new ProcessStartInfo(command, arguments) @@ -66,30 +56,32 @@ public static void RunCommand(string command, string arguments, Action o process.BeginOutputReadLine(); process.BeginErrorReadLine(); - process.WaitForExit(); + await process.WaitForExitAsync(cancellationToken); } /// - /// Requests the specified URI with and returns if the response status code is in the + /// Requests the specified URI with and returns if the response status code is in the /// range 200-299. /// /// The URI to request. /// true if the response status code is in the range 200-299. - public static async Task TestUriExists(string uri) + public static async Task TestUriExists(string? uri, CancellationToken cancellationToken = default) { try - { - var headRequest = new HttpRequestMessage(HttpMethod.Head, uri); - var response = await httpClient.SendAsync(headRequest); + { + HttpResponseMessage response = await s_httpClient.SendAsync(new(HttpMethod.Head, uri), cancellationToken); + if (!response.IsSuccessStatusCode && response.StatusCode != System.Net.HttpStatusCode.NotFound) { Console.Error.WriteLine($"Error: HTTP response code on {uri} is {response.StatusCode}"); } + return response.IsSuccessStatusCode; } catch (HttpRequestException e) { Console.WriteLine($"Exception on {uri}: {e.Message}"); + return false; } } diff --git a/XrefMap.cs b/XrefMap.cs index dfb6338..196c7af 100644 --- a/XrefMap.cs +++ b/XrefMap.cs @@ -1,7 +1,8 @@ -using System; using System.Collections.Generic; using System.IO; using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; using YamlDotNet.Serialization; namespace DocFxForUnity @@ -11,39 +12,39 @@ namespace DocFxForUnity /// public sealed partial class XrefMap { - private static readonly Deserializer Deserializer = new(); - private static readonly Serializer Serializer = new(); - - [GeneratedRegex("(\\d):")] - private static partial Regex ZeroStringsRegex(); - - public bool sorted { get; set; } - - public XrefMapReference[] references { get; set; } + private static readonly Deserializer s_deserializer = new(); + private static readonly Serializer s_serializer = new(); + + [YamlMember(Alias = "sorted")] + public bool Sorted { get; set; } + + [YamlMember(Alias = "references")] + public XrefMapReference[]? References { get; set; } /// /// Loads a from a file. /// /// The path of the file. /// The loaded from . - public static XrefMap Load(string filePath) + public static async Task Load(string filePath, CancellationToken cancellationToken = default) { - string xrefMapText = File.ReadAllText(filePath); + string xrefMapText = await File.ReadAllTextAsync(filePath, cancellationToken); // Remove `0:` strings on the xrefmap that make crash Deserializer xrefMapText = ZeroStringsRegex().Replace(xrefMapText, "$1"); - return Deserializer.Deserialize(xrefMapText); + return s_deserializer.Deserialize(xrefMapText); } /// - /// Fix the of of this . + /// Fix the of of this . /// /// The URL of the online API documentation of Unity. - public void FixHrefs(string apiUrl, bool testUrls = false) + public void FixHrefs(string apiUrl) { var fixedReferences = new List(); - foreach (var reference in references) + + foreach (XrefMapReference reference in References!) { if (!reference.IsValid) { @@ -52,23 +53,23 @@ public void FixHrefs(string apiUrl, bool testUrls = false) reference.FixHref(apiUrl); fixedReferences.Add(reference); - - if (testUrls && !Utils.TestUriExists(reference.href).Result) - { - Console.WriteLine("Warning: invalid URL " + reference.href + " for " + reference.uid + " uid"); - } } - references = fixedReferences.ToArray(); + + References = [.. fixedReferences]; } /// /// Saves this to a file. /// /// The path of the file. - public void Save(string filePath) + public async Task Save(string filePath, CancellationToken cancellationToken = default) { - string xrefMapText = "### YamlMime:XRefMap\n" + Serializer.Serialize(this); - File.WriteAllText(filePath, xrefMapText); + string xrefMapText = "### YamlMime:XRefMap\n" + s_serializer.Serialize(this); + + await File.WriteAllTextAsync(filePath, xrefMapText, cancellationToken); } + + [GeneratedRegex(@"(\d):")] + private static partial Regex ZeroStringsRegex(); } } \ No newline at end of file diff --git a/XrefMapReference.cs b/XrefMapReference.cs index 7f8acf0..2056c0a 100644 --- a/XrefMapReference.cs +++ b/XrefMapReference.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using System.Text.RegularExpressions; using YamlDotNet.Serialization; @@ -12,88 +11,109 @@ public sealed partial class XrefMapReference /// /// The online API documentation of Unity doesn't show some namespaces. /// - private static readonly List HrefNamespacesToTrim = new() { "UnityEditor", "UnityEngine" }; + private static readonly string[] s_hrefNamespacesToTrim = ["UnityEditor", "UnityEngine"]; - [GeneratedRegex("`{2}\\d")] - private static partial Regex GenericHrefRegex(); - - [GeneratedRegex("\\*$")] - private static partial Regex MethodHrefPointerRegex(); - - [GeneratedRegex("\\(.*\\)")] - private static partial Regex MethodHrefRegex(); - - [GeneratedRegex("\\.([a-z].*)$")] - private static partial Regex PropertyHrefRegex(); - - public string uid { get; set; } - - public string name { get; set; } + [YamlMember(Alias = "uid")] + public string? Uid { get; set; } + + [YamlMember(Alias = "name")] + public string? Name { get; set; } [YamlMember(Alias = "name.vb")] - public string nameVb { get; set; } - - public string href { get; set; } - - public string commentId { get; set; } - - public string isSpec { get; set; } - - public string fullName { get; set; } + public string? NameVb { get; set; } + + [YamlMember(Alias = "href")] + public string? Href { get; set; } + + [YamlMember(Alias = "commentId")] + public string? CommentId { get; set; } + + [YamlMember(Alias = "isSpec")] + public string? IsSpec { get; set; } + + [YamlMember(Alias = "fullName")] + public string? FullName { get; set; } [YamlMember(Alias = "fullName.vb")] - public string fullNameVb { get; set; } - - public string nameWithType { get; set; } + public string? FullNameVb { get; set; } + + [YamlMember(Alias = "nameWithType")] + public string? NameWithType { get; set; } [YamlMember(Alias = "nameWithType.vb")] - public string nameWithTypeVb { get; set; } + public string? NameWithTypeVb { get; set; } /// /// Gets if this is valid or not. /// - public bool IsValid => !commentId.Contains("Overload:"); + [YamlIgnore] + public bool IsValid => !CommentId!.Contains("Overload:"); /// - /// Sets to link to the online API documentation of Unity. + /// Sets to link to the online API documentation of Unity. /// /// The URL of the online API documentation of Unity. public void FixHref(string apiUrl) { // Namespaces point to documentation index - if (commentId.Contains("N:")) + if (CommentId!.StartsWith("N:")) { - href = "index"; + Href = "index"; } else { - href = uid; + Href = Uid; // Trim UnityEngine and UnityEditor namespaces from href - foreach (var hrefNamespaceToTrim in HrefNamespacesToTrim) + foreach (string hrefNamespaceToTrim in s_hrefNamespacesToTrim) { - href = href.Replace(hrefNamespaceToTrim + ".", ""); + Href = Href!.Replace(hrefNamespaceToTrim + ".", string.Empty); } // Fix href of constructors - href = href.Replace(".#ctor", "-ctor"); + Href = Href!.Replace(".#ctor", "-ctor"); // Fix href of generics - href = GenericHrefRegex().Replace(href, ""); - href = href.Replace("`", "_"); + Href = GenericHrefRegex().Replace(Href!, string.Empty); + Href = Href.Replace("`", "_"); // Fix href of methods - href = MethodHrefPointerRegex().Replace(href, ""); - href = MethodHrefRegex().Replace(href, ""); + Href = MethodHrefPointerRegex().Replace(Href, string.Empty); + Href = MethodHrefRegex().Replace(Href, string.Empty); + + // Fix href of operator + if (CommentId.StartsWith("M:") && CommentId.Contains(".op_")) + { + Href = Href.Replace(".op_", ".operator_"); + + Href = Href.Replace(".operator_Subtraction", ".operator_subtract"); + Href = Href.Replace(".operator_Multiply", ".operator_multiply"); + Href = Href.Replace(".operator_Division", ".operator_divide"); + Href = Href.Replace(".operator_Addition", ".operator_add"); + Href = Href.Replace(".operator_Equality", ".operator_eq"); + Href = Href.Replace(".operator_Implicit~", ".operator_"); + } // Fix href of properties - if (commentId.Contains("P:") || commentId.Contains("M:")) + if (CommentId.StartsWith("F:") || CommentId.StartsWith("M:") || CommentId.StartsWith("P:")) { - href = PropertyHrefRegex().Replace(href, "-$1"); + Href = PropertyHrefRegex().Replace(Href, "-$1"); } } - href = apiUrl + href + ".html"; + Href = apiUrl + Href + ".html"; } + + [GeneratedRegex(@"`{2}\d")] + private static partial Regex GenericHrefRegex(); + + [GeneratedRegex(@"\*$")] + private static partial Regex MethodHrefPointerRegex(); + + [GeneratedRegex(@"\(.*\)")] + private static partial Regex MethodHrefRegex(); + + [GeneratedRegex(@"\.([a-z].*)$")] + private static partial Regex PropertyHrefRegex(); } -} \ No newline at end of file +} diff --git a/docfx.json b/docfx.json index ca2924d..fc7acb9 100644 --- a/docfx.json +++ b/docfx.json @@ -11,8 +11,7 @@ "disableGitFeatures": true } ], - "build": - { + "build": { "xrefService": [ "https://xref.docs.microsoft.com/query?uid={uid}" ], "content": [ { From 03b08e440dd4bd1a99e6b4e940480ecdadd5941e Mon Sep 17 00:00:00 2001 From: bdovaz Date: Thu, 25 Sep 2025 15:53:37 +0200 Subject: [PATCH 02/29] wip --- .editorconfig | 4 +- .github/workflows/ci.yml | 23 ++ .github/workflows/unity-xref-maps.yml | 23 +- Git.cs | 52 ---- Program.cs | 221 ---------------- README.md | 2 +- UnityXrefMaps.Tests/CommandTests.cs | 236 ++++++++++++++++++ .../UnityXrefMaps.Tests.csproj | 39 +++ UnityXrefMaps.sln | 10 +- UnityXrefMaps/AssemblyInfo.cs | 3 + UnityXrefMaps/Commands/BuildCommand.cs | 146 +++++++++++ UnityXrefMaps/Commands/TestCommand.cs | 39 +++ UnityXrefMaps/Constants.cs | 40 +++ .../DocFX/DocFxConfiguration.cs | 2 +- .../DocFX/DocFxConfigurationBuild.cs | 2 +- UnityXrefMaps/Git.cs | 49 ++++ UnityXrefMaps/Program.cs | 13 + .../Properties}/launchSettings.json | 0 .../RepositoryExtensions.cs | 13 +- .../UnityXrefMaps.csproj | 9 +- UnityXrefMaps/Utils.cs | 89 +++++++ UnityXrefMaps/XrefMap.cs | 74 ++++++ UnityXrefMaps/XrefMapReference.cs | 118 +++++++++ docfx.json => UnityXrefMaps/docfx.json | 6 +- UnityXrefMaps/filterConfig.yml | 24 ++ Utils.cs | 89 ------- XrefMap.cs | 75 ------ XrefMapReference.cs | 119 --------- filterConfig.yml | 22 -- 29 files changed, 935 insertions(+), 607 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 Git.cs delete mode 100644 Program.cs create mode 100644 UnityXrefMaps.Tests/CommandTests.cs create mode 100644 UnityXrefMaps.Tests/UnityXrefMaps.Tests.csproj create mode 100644 UnityXrefMaps/AssemblyInfo.cs create mode 100644 UnityXrefMaps/Commands/BuildCommand.cs create mode 100644 UnityXrefMaps/Commands/TestCommand.cs create mode 100644 UnityXrefMaps/Constants.cs rename DocFxConfiguration.cs => UnityXrefMaps/DocFX/DocFxConfiguration.cs (85%) rename DocFxConfigurationBuild.cs => UnityXrefMaps/DocFX/DocFxConfigurationBuild.cs (85%) create mode 100644 UnityXrefMaps/Git.cs create mode 100644 UnityXrefMaps/Program.cs rename {Properties => UnityXrefMaps/Properties}/launchSettings.json (100%) rename RepositoryExtensions.cs => UnityXrefMaps/RepositoryExtensions.cs (72%) rename UnityXrefMaps.csproj => UnityXrefMaps/UnityXrefMaps.csproj (79%) create mode 100644 UnityXrefMaps/Utils.cs create mode 100644 UnityXrefMaps/XrefMap.cs create mode 100644 UnityXrefMaps/XrefMapReference.cs rename docfx.json => UnityXrefMaps/docfx.json (84%) create mode 100644 UnityXrefMaps/filterConfig.yml delete mode 100644 Utils.cs delete mode 100644 XrefMap.cs delete mode 100644 XrefMapReference.cs delete mode 100644 filterConfig.yml diff --git a/.editorconfig b/.editorconfig index 2ca9e89..16e1a86 100644 --- a/.editorconfig +++ b/.editorconfig @@ -157,7 +157,7 @@ csharp_space_between_parentheses = false csharp_space_between_square_brackets = false # License header -file_header_template = Licensed to the .NET Foundation under one or more agreements.\nThe .NET Foundation licenses this file to you under the MIT license. +file_header_template = [src/libraries/System.Net.Http/src/System/Net/Http/{SocketsHttpHandler/Http3RequestStream.cs,BrowserHttpHandler/BrowserHttpHandler.cs}] # disable CA2025, the analyzer throws a NullReferenceException when processing this file: https://github.com/dotnet/roslyn-analyzers/issues/7652 @@ -198,4 +198,4 @@ indent_size = 2 [*.sh] end_of_line = lf [*.{cmd,bat}] -end_of_line = crlf \ No newline at end of file +end_of_line = crlf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8daa862 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,23 @@ +--- +name: ci + +on: + workflow_dispatch: + push: + branches: + - main + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Install DocFX + run: dotnet tool restore + + - name: Build & test (Release) + run: dotnet test -c Release --logger "console;verbosity=normal" diff --git a/.github/workflows/unity-xref-maps.yml b/.github/workflows/unity-xref-maps.yml index 2dcda65..6f416f1 100644 --- a/.github/workflows/unity-xref-maps.yml +++ b/.github/workflows/unity-xref-maps.yml @@ -1,3 +1,4 @@ +--- name: Unity xref maps on: @@ -10,25 +11,29 @@ on: jobs: build: - runs-on: windows-latest + runs-on: ubuntu-latest + steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v5 - name: Install DocFX run: dotnet tool restore - - name: Cache UnityCsReference - uses: actions/cache@v3 - with: - path: UnityCsReference - key: unitycsreference + - name: Restore + run: dotnet restore + working-directory: UnityXrefMaps + + - name: Build + run: dotnet build --no-restore --configuration Release --output ${{ runner.temp }}/UnityXrefMaps + working-directory: UnityXrefMaps - name: Run - run: dotnet run + run: ${{ runner.temp }}/UnityXrefMaps/UnityXrefMaps.dll + working-directory: UnityXrefMaps - name: Deploy - uses: peaceiris/actions-gh-pages@v3 + uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: _site diff --git a/Git.cs b/Git.cs deleted file mode 100644 index 8b4bfd1..0000000 --- a/Git.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using System.IO; -using LibGit2Sharp; - -namespace DocFxForUnity -{ - public static class Git - { - /// - /// Fetches changes and hard resets the specified repository to the latest commit of a specified branch. If no - /// repository is found, it will be cloned before. - /// - /// The url of the repository. - /// The directory path where to find/clone the repository. - /// The branch use on the repository. - /// The synced repository on the latest commit of the specified branch. - public static Repository GetSyncRepository(string sourceUrl, string path, string branch = "main") - { - // Clone this repository to the specified branch if it doesn't exist - bool clone = !Directory.Exists(path); - if (clone) - { - Console.WriteLine($"Cloning {sourceUrl} to {path}"); - - var options = new CloneOptions() { BranchName = branch }; - Repository.Clone(sourceUrl, path, options); - } - - var repository = new Repository(path); - - // Otherwise fetch changes and checkout to the specified branch - if (!clone) - { - Console.WriteLine($"Hard reset '{path}' to HEAD"); - repository.Reset(ResetMode.Hard); - repository.RemoveUntrackedFiles(); - - Console.WriteLine($"Fetching changes from 'origin' in '{path}'"); - Remote remote = repository.Network.Remotes["origin"]; - Commands.Fetch(repository, remote.Name, [], null, null); // WTF is this API libgit2sharp? - - Console.WriteLine($"Checking out '{path}' to '{branch}' branch"); - string remoteBranch = $"origin/{branch}"; - Commands.Checkout(repository, remoteBranch); - } - - Console.WriteLine(); - - return repository; - } - } -} \ No newline at end of file diff --git a/Program.cs b/Program.cs deleted file mode 100644 index 33fc2bf..0000000 --- a/Program.cs +++ /dev/null @@ -1,221 +0,0 @@ -using System; -using System.CommandLine; -using System.IO; -using System.Text.Json; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using LibGit2Sharp; - -namespace DocFxForUnity -{ - /// - /// Generates the xref maps of the APIs of all the Unity versions. - /// - /// Usage: Generate - /// - /// - /// - /// [.NET](https://dotnet.microsoft.com) >= 9.0 and [DocFX](https://dotnet.github.io/docfx/) must be installed - /// on your system. - /// - partial class Program - { - /// - /// Gets the default URL of the online API documentation of Unity. - /// - private const string DefaultUnityApiUrl = "https://docs.unity3d.com/{0}/Documentation/ScriptReference/"; - - /// - /// The default path of the Unity repository. - /// - private const string DefaultUnityRepositoryPath = "UnityCsReference"; - - /// - /// The default URL of the Unity repository. - /// - private const string DefaultUnityRepositoryUrl = "https://github.com/Unity-Technologies/UnityCsReference.git"; - - /// - /// The default branch of the Unity repository. - /// - private const string DefaultUnityRepositoryBranch = "master"; - - // https://github.com/dotnet/docfx/blob/1c4e9ff4a2d236206eee04066847a98343c6a3f7/src/Docfx.Build/XRefMaps/XRefArchive.cs#L14 - /// - /// The default xref map filename. - /// - private const string DefaultXrefMapFileName = "xrefmap.yml"; - - /// - /// The default path where to copy the xref maps. - /// - private const string DefaultXrefMapsPath = $"_site/{{0}}/{DefaultXrefMapFileName}"; - - /// - /// The default DocFX config file path. - /// - private const string DefaultDocFxConfigurationFilePath = "docfx.json"; - - /// - /// Entry point of this program. - /// - public static async Task Main(string[] args) - { - RootCommand rootCommand = []; - - Option repositoryUrlOption = new("--repositoryUrl") - { - Description = "The Git repository url.", - DefaultValueFactory = _ => DefaultUnityRepositoryUrl - }; - Option repositoryBranchOption = new("--repositoryBranch") - { - Description = "The Git repository branch.", - DefaultValueFactory = _ => DefaultUnityRepositoryBranch - }; - Option repositoryPathOption = new("--repositoryPath") - { - Description = "The Git repository path to git clone. " + - "If the clone has already been made, it will reset so that the clone can be reused.", - DefaultValueFactory = _ => DefaultUnityRepositoryPath - }; - Option repositoryTagsOption = new("--repositoryTags") - { - Description = "The repository tags to use to generate the documentation. " + - "That is, the versions of the Unity editor (6000.0.1f1, 6000.1.1f1) or the versions of the Unity package (1.0.0, 1.1.0).", - Required = true - }; - Option apiUrlOption = new("--apiUrl") - { - Description = "The root path of the Unity editor API documentation or the Unity package. " + - "{0} is replaced with the Unity editor short version (example: https://docs.unity3d.com/6000.0/Documentation/ScriptReference/) or " + - "the Unity package short version (example: https://docs.unity3d.com/Packages/com.unity.inputsystem@1.0/api/)}) in the case of a package.", - DefaultValueFactory = _ => DefaultUnityApiUrl - }; - Option docFxConfigurationFilePathOption = new("--docFxConfigurationFilePath") - { - Description = "The path to the DocFX configuration file.", - DefaultValueFactory = _ => DefaultDocFxConfigurationFilePath - }; - Option xrefMapsPathOption = new("--xrefMapsPath") - { - Description = $"The path where the final {DefaultXrefMapFileName} files will be generated. " + - $"{{0}} is replaced with the Unity editor version (example: {string.Format(DefaultXrefMapsPath, "6000.0.1f1")}) or " + - $"the Unity package version (example: {string.Format(DefaultXrefMapsPath, "1.0.0")}) in the case of a package.", - DefaultValueFactory = _ => DefaultXrefMapsPath - }; - - rootCommand.Options.Add(repositoryUrlOption); - rootCommand.Options.Add(repositoryBranchOption); - rootCommand.Options.Add(repositoryPathOption); - rootCommand.Options.Add(repositoryTagsOption); - rootCommand.Options.Add(apiUrlOption); - rootCommand.Options.Add(docFxConfigurationFilePathOption); - rootCommand.Options.Add(xrefMapsPathOption); - - rootCommand.SetAction(async (parseResult, cancellationToken) => - { - bool result = true; - - string? repositoryUrl = parseResult.GetValue(repositoryUrlOption); - string? repositoryBranch = parseResult.GetValue(repositoryBranchOption); - string? repositoryPath = parseResult.GetValue(repositoryPathOption); - string[]? repositoryTags = parseResult.GetValue(repositoryTagsOption); - string? apiUrl = parseResult.GetValue(apiUrlOption); - string? docFxFilePath = parseResult.GetValue(docFxConfigurationFilePathOption); - string? xrefMapsPath = parseResult.GetValue(xrefMapsPathOption); - - using Stream docFxStream = File.OpenRead(docFxFilePath!); - - DocFxConfiguration? docFxConfiguration = await JsonSerializer.DeserializeAsync(docFxStream, cancellationToken: cancellationToken); - - string generatedDocsPath = docFxConfiguration!.Build!.Destination!; - string generatedXrefMapPath = Path.Combine(generatedDocsPath, DefaultXrefMapFileName!); - - Console.WriteLine($"Sync the Unity repository in '{Path.GetFullPath(repositoryPath!)}'"); - - using Repository repository = Git.GetSyncRepository(repositoryUrl!, repositoryPath!, repositoryBranch!); - - foreach (string repositoryTag in repositoryTags!) - { - Match versionMatch = VersionRegex().Match(repositoryTag); - - string shortVersion = $"{versionMatch.Groups["majorVersion"].Value}.{versionMatch.Groups["minorVersion"].Value}"; - - Console.WriteLine($"Generating Unity '{shortVersion}' xref map"); - - repository.HardReset(repositoryTag); - - string xrefMapPath = string.Format(xrefMapsPath!, repositoryTag); // .//xrefmap.yml - - Console.WriteLine($"Running DocFX on '{repositoryTag}'"); - - await Utils.RunCommand("dotnet", $"docfx {docFxFilePath}", Console.WriteLine, Console.WriteLine, cancellationToken); - - if (!File.Exists(generatedXrefMapPath)) - { - result = false; - - Console.Error.WriteLine($"Error: '{generatedXrefMapPath}' for Unity '{repositoryTag}' not generated"); - Console.Error.WriteLine("\n"); - - continue; - } - - Console.WriteLine($"Fixing hrefs in '{Path.GetFullPath(xrefMapPath)}'"); - - await Utils.CopyFile(generatedXrefMapPath, xrefMapPath, cancellationToken); - - XrefMap xrefMap = await XrefMap.Load(xrefMapPath, cancellationToken); - - xrefMap.FixHrefs(apiUrl: string.Format(apiUrl!, shortVersion)); - - await xrefMap.Save(xrefMapPath, cancellationToken); - - Console.WriteLine("\n"); - } - - return result ? 0 : 1; - }); - - Option xrefPathOption = new("--xrefPath") - { - Description = $"The path to the {DefaultXrefMapFileName} file.", - Required = true - }; - - Command testCommand = new("test", $"Check that the links in the {DefaultXrefMapFileName} file are valid.") - { - xrefPathOption - }; - - testCommand.SetAction(async (parseResult, cancellationToken) => - { - bool result = true; - - string? xrefPath = parseResult.GetValue(xrefPathOption); - - XrefMap xrefMap = await XrefMap.Load(xrefPath!, cancellationToken); - - foreach (XrefMapReference reference in xrefMap.References!) - { - if (!await Utils.TestUriExists(reference.Href, cancellationToken)) - { - result = false; - - Console.WriteLine($"Warning: invalid URL {reference.Href} for {reference.Uid} uid"); - } - } - - return result ? 0 : 1; - }); - - rootCommand.Subcommands.Add(testCommand); - - await rootCommand.Parse(args).InvokeAsync(); - } - - [GeneratedRegex(@"(?\d+)\.(?\d+)\.(?\d+)")] - private static partial Regex VersionRegex(); - } -} diff --git a/README.md b/README.md index 9e2fe2c..13c105c 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ > Automatically add clickable links to the Unity API on a DocFX documentation Generates references of the Unity API to use with DocFX (the -[cross reference maps](https://dotnet.github.io/docfx/tutorial/links_and_cross_references.html#cross-reference-between-projects)). +[cross reference maps](https://dotnet.github.io/docfx/docs/links-and-cross-references.html#cross-reference-to-net-basic-class-library)). DocFX will set clickable all the references of the Unity API on your documentation. ## Usage diff --git a/UnityXrefMaps.Tests/CommandTests.cs b/UnityXrefMaps.Tests/CommandTests.cs new file mode 100644 index 0000000..dc0ff55 --- /dev/null +++ b/UnityXrefMaps.Tests/CommandTests.cs @@ -0,0 +1,236 @@ +using Meziantou.Extensions.Logging.Xunit.v3; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; +using System; +using System.Collections.Generic; +using System.CommandLine; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using UnityXrefMaps.Commands; + +namespace UnityXrefMaps.Tests +{ + public class CommandTests : IAsyncDisposable + { + private static readonly string repositoryDirectoryPath = Guid.NewGuid().ToString(); + private static readonly string xrefDirectoryPath = Guid.NewGuid().ToString(); + private static readonly string docFxFilePath = Guid.NewGuid().ToString() + "_config.json"; + private static readonly string docFxFilterFilePath = Guid.NewGuid().ToString() + "_filter_config.yml"; + + private class CustomStringWriter : StringWriter + { + private readonly ILogger logger; + private readonly LogLevel logLevel; + + public CustomStringWriter(ILogger logger, LogLevel logLevel) + { + this.logger = logger; + this.logLevel = logLevel; + } + + public override void Write(string? value) + { + base.Write(value); + + logger.Log(logLevel, "{Message}", value); + } + } + + private readonly ITestOutputHelper output; + + public CommandTests(ITestOutputHelper output) + { + this.output = output; + } + + [Fact] + public async Task BuildTest_Success() + { + string docFxFileContent = await File.ReadAllTextAsync("docfx.json", TestContext.Current.CancellationToken); + + docFxFileContent = docFxFileContent.Replace("UnityCsReference/", repositoryDirectoryPath + '/'); + docFxFileContent = docFxFileContent.Replace("filterConfig.yml", docFxFilterFilePath); + + await File.WriteAllTextAsync(docFxFilePath, docFxFileContent, TestContext.Current.CancellationToken); + + string docFxFilterConfigContent = """ +### YamlMime:ManagedReference +--- +apiRules: + - include: + uidRegex: ^UnityEngine\.Vector2$ + - include: + uidRegex: ^UnityEngine\.Vector3$ + - exclude: + uidRegex: .* +"""; + + await File.WriteAllTextAsync(docFxFilterFilePath, docFxFilterConfigContent, TestContext.Current.CancellationToken); + + var serviceCollection = new ServiceCollection(); + + serviceCollection.AddLogging(builder => + { + builder.SetMinimumLevel(LogLevel.Trace); + builder.AddFakeLogging(); + builder.Services.AddSingleton(new XUnitLoggerProvider(output, appendScope: false)); + }); + + await using ServiceProvider serviceProvider = serviceCollection.BuildServiceProvider(); + + FakeLogCollector fakeLogCollector = serviceProvider.GetFakeLogCollector(); + + ILoggerFactory loggerFactory = serviceProvider.GetRequiredService(); + + ILogger logger = loggerFactory.CreateLogger(); + + InvocationConfiguration invocationConfiguration = new() + { + Error = new CustomStringWriter(logger, LogLevel.Error), + Output = new CustomStringWriter(logger, LogLevel.Information), + }; + + string[] testedVersions = ["6000.0.1f1", "6000.1.1f1"]; + + BuildCommand buildCommand = new(loggerFactory.CreateLogger()); + + string xrefDirectoryName = "test"; + string xrefFileName = "test2.yml"; + + string[] buildArgs = [ + "--repositoryPath", + repositoryDirectoryPath, + "--docFxConfigurationFilePath", + docFxFilePath, + "--xrefMapsPath", + $"{xrefDirectoryPath}/{xrefDirectoryName}/{{0}}/{xrefFileName}" + ]; + + buildArgs = [.. buildArgs, .. testedVersions.SelectMany(v => new string[] { "--repositoryTags", v })]; + + Assert.Equal( + 0, + await buildCommand + .Parse(buildArgs) + .InvokeAsync( + invocationConfiguration, + TestContext.Current.CancellationToken)); + + IReadOnlyList logRecords = fakeLogCollector.GetSnapshot(); + + Assert.Equal(2, logRecords.Count(l => l.Message.Equals("XRef map exported."))); + + foreach (string testedVersion in testedVersions) + { + string xrefFilePath = $"{xrefDirectoryPath}/{xrefDirectoryName}/{testedVersion}/{xrefFileName}"; + + Assert.True(File.Exists(xrefFilePath)); + + string xrefFileContent = await File.ReadAllTextAsync(xrefFilePath, TestContext.Current.CancellationToken); + + logger.LogInformation("{FilePath}:\n\n{FileContent}", xrefFilePath, xrefFileContent); + } + + TestCommand testCommand = new(loggerFactory.CreateLogger()); + + foreach (string testedVersion in testedVersions) + { + fakeLogCollector.Clear(); + + string[] testArgs = [ + "--xrefPath", + $"{xrefDirectoryPath}/{xrefDirectoryName}/{testedVersion}/{xrefFileName}" + ]; + + Assert.Equal( + 0, + await testCommand + .Parse(testArgs) + .InvokeAsync( + invocationConfiguration, + TestContext.Current.CancellationToken)); + + Assert.Equal(0, fakeLogCollector.Count); + } + } + + // https://stackoverflow.com/a/1702920 + private static void DeleteDirectory(string targetDir) + { + File.SetAttributes(targetDir, FileAttributes.Normal); + + string[] files = Directory.GetFiles(targetDir); + string[] dirs = Directory.GetDirectories(targetDir); + + foreach (string file in files) + { + File.SetAttributes(file, FileAttributes.Normal); + File.Delete(file); + } + + foreach (string dir in dirs) + { + DeleteDirectory(dir); + } + + Directory.Delete(targetDir, false); + } + + public async ValueTask DisposeAsync() + { + await DisposeAsyncCore().ConfigureAwait(false); + + GC.SuppressFinalize(this); + } + + protected virtual async ValueTask DisposeAsyncCore() + { + File.Delete(docFxFilePath); + File.Delete(docFxFilterFilePath); + + const int maxRetries = 3; + const int delay = 500; + + await DeleteDirectoryWithRetries(repositoryDirectoryPath, maxRetries, delay); + await DeleteDirectoryWithRetries(xrefDirectoryPath, maxRetries, delay); + } + + private async Task DeleteDirectoryWithRetries(string directoryPath, int maxRetries, int delay) + { + int retries = 0; + + while (retries < maxRetries) + { + try + { + output.WriteLine($"Trying to delete directory: {directoryPath}"); + + DeleteDirectory(directoryPath); + + output.WriteLine($"Directory deleted: {directoryPath}"); + + break; + } + catch (Exception e) + { + output.WriteLine($"Error deleting directory: {directoryPath}. Error: {e}"); + + retries++; + + if (retries < maxRetries) + { + output.WriteLine($"Retrying in {delay}ms..."); + + await Task.Delay(delay); + } + else + { + output.WriteLine("Retry limit reached."); + } + } + } + } + } +} diff --git a/UnityXrefMaps.Tests/UnityXrefMaps.Tests.csproj b/UnityXrefMaps.Tests/UnityXrefMaps.Tests.csproj new file mode 100644 index 0000000..0367ae8 --- /dev/null +++ b/UnityXrefMaps.Tests/UnityXrefMaps.Tests.csproj @@ -0,0 +1,39 @@ + + + + Exe + net9.0 + enable + false + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + PreserveNewest + + + + + + + + + + + + diff --git a/UnityXrefMaps.sln b/UnityXrefMaps.sln index cc96728..ae261f7 100644 --- a/UnityXrefMaps.sln +++ b/UnityXrefMaps.sln @@ -1,9 +1,11 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 -VisualStudioVersion = 17.14.36511.14 d17.14 +VisualStudioVersion = 17.14.36511.14 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnityXrefMaps", "UnityXrefMaps.csproj", "{97BE34CD-6988-1F05-D1B2-EBF906DAA098}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnityXrefMaps", "UnityXrefMaps\UnityXrefMaps.csproj", "{97BE34CD-6988-1F05-D1B2-EBF906DAA098}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnityXrefMaps.Tests", "UnityXrefMaps.Tests\UnityXrefMaps.Tests.csproj", "{2A7A5BA2-A0E4-D775-C22B-0C5A6B68260B}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -15,6 +17,10 @@ Global {97BE34CD-6988-1F05-D1B2-EBF906DAA098}.Debug|Any CPU.Build.0 = Debug|Any CPU {97BE34CD-6988-1F05-D1B2-EBF906DAA098}.Release|Any CPU.ActiveCfg = Release|Any CPU {97BE34CD-6988-1F05-D1B2-EBF906DAA098}.Release|Any CPU.Build.0 = Release|Any CPU + {2A7A5BA2-A0E4-D775-C22B-0C5A6B68260B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2A7A5BA2-A0E4-D775-C22B-0C5A6B68260B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2A7A5BA2-A0E4-D775-C22B-0C5A6B68260B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2A7A5BA2-A0E4-D775-C22B-0C5A6B68260B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/UnityXrefMaps/AssemblyInfo.cs b/UnityXrefMaps/AssemblyInfo.cs new file mode 100644 index 0000000..153d6b8 --- /dev/null +++ b/UnityXrefMaps/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("UnityXrefMaps.Tests")] diff --git a/UnityXrefMaps/Commands/BuildCommand.cs b/UnityXrefMaps/Commands/BuildCommand.cs new file mode 100644 index 0000000..943076d --- /dev/null +++ b/UnityXrefMaps/Commands/BuildCommand.cs @@ -0,0 +1,146 @@ +using System.CommandLine; +using System.IO; +using System.Text.Json; +using System.Text.RegularExpressions; +using LibGit2Sharp; +using Microsoft.Extensions.Logging; +using UnityXrefMaps.DocFX; + +namespace UnityXrefMaps.Commands; + +internal sealed partial class BuildCommand : RootCommand +{ + public BuildCommand(ILogger logger) + { + Option repositoryUrlOption = new("--repositoryUrl") + { + Description = "The Git repository url.", + DefaultValueFactory = _ => Constants.DefaultUnityRepositoryUrl + }; + Option repositoryBranchOption = new("--repositoryBranch") + { + Description = "The Git repository branch.", + DefaultValueFactory = _ => Constants.DefaultUnityRepositoryBranch + }; + Option repositoryPathOption = new("--repositoryPath") + { + Description = "The Git repository path to git clone. " + + "If the clone has already been made, it will reset so that the clone can be reused.", + DefaultValueFactory = _ => Constants.DefaultUnityRepositoryPath + }; + Option repositoryTagsOption = new("--repositoryTags") + { + Description = "The repository tags to use to generate the documentation. " + + "That is, the versions of the Unity editor (6000.0.1f1, 6000.1.1f1) or the versions of the Unity package (1.0.0, 1.1.0).", + Required = true + }; + Option apiUrlOption = new("--apiUrl") + { + Description = "The root path of the Unity editor API documentation or the Unity package. " + + "{0} is replaced with the Unity editor short version (example: https://docs.unity3d.com/6000.0/Documentation/ScriptReference/) or " + + "the Unity package short version (example: https://docs.unity3d.com/Packages/com.unity.inputsystem@1.0/api/)}) in the case of a package.", + DefaultValueFactory = _ => Constants.DefaultUnityApiUrl + }; + Option docFxConfigurationFilePathOption = new("--docFxConfigurationFilePath") + { + Description = "The path to the DocFX configuration file.", + DefaultValueFactory = _ => Constants.DefaultDocFxConfigurationFilePath + }; + Option xrefMapsPathOption = new("--xrefMapsPath") + { + Description = $"The path where the final {Constants.DefaultXrefMapFileName} files will be generated. " + + $"{{0}} is replaced with the Unity editor version (example: {string.Format(Constants.DefaultXrefMapsPath, "6000.0.1f1")}) or " + + $"the Unity package version (example: {string.Format(Constants.DefaultXrefMapsPath, "1.0.0")}) in the case of a package.", + DefaultValueFactory = _ => Constants.DefaultXrefMapsPath + }; + + Options.Add(repositoryUrlOption); + Options.Add(repositoryBranchOption); + Options.Add(repositoryPathOption); + Options.Add(repositoryTagsOption); + Options.Add(apiUrlOption); + Options.Add(docFxConfigurationFilePathOption); + Options.Add(xrefMapsPathOption); + + SetAction(async (parseResult, cancellationToken) => + { + bool result = true; + + string? repositoryUrl = parseResult.GetValue(repositoryUrlOption); + string? repositoryBranch = parseResult.GetValue(repositoryBranchOption); + string? repositoryPath = parseResult.GetValue(repositoryPathOption); + string[]? repositoryTags = parseResult.GetValue(repositoryTagsOption); + string? apiUrl = parseResult.GetValue(apiUrlOption); + string? docFxFilePath = parseResult.GetValue(docFxConfigurationFilePathOption); + string? xrefMapsPath = parseResult.GetValue(xrefMapsPathOption); + + using Stream docFxStream = File.OpenRead(docFxFilePath!); + + DocFxConfiguration? docFxConfiguration = await JsonSerializer.DeserializeAsync(docFxStream, cancellationToken: cancellationToken); + + string generatedDocsPath = docFxConfiguration!.Build!.Destination!; + string generatedXrefMapPath = Path.Combine(generatedDocsPath, Constants.DefaultXrefMapFileName!); + + logger.LogInformation("Sync the Unity repository in '{RepositoryPath}'", Path.GetFullPath(repositoryPath!)); + + using Repository repository = Git.GetSyncRepository(repositoryUrl!, repositoryPath!, repositoryBranch!, logger); + + foreach (string repositoryTag in repositoryTags!) + { + Match versionMatch = VersionRegex().Match(repositoryTag); + + string shortVersion = $"{versionMatch.Groups["majorVersion"].Value}.{versionMatch.Groups["minorVersion"].Value}"; + + logger.LogInformation("Generating Unity '{ShortVersion}' xref map", shortVersion); + + repository.HardReset(repositoryTag, logger); + + string xrefMapPath = string.Format(xrefMapsPath!, repositoryTag); // .//xrefmap.yml + + logger.LogInformation("Running DocFX on '{RepositoryTag}'", repositoryTag); + + await Utils.RunCommand( + "dotnet", $"docfx {docFxFilePath}", + value => + { + if (!string.IsNullOrEmpty(value)) + { + logger.LogInformation("{Message}", value); + } + }, + value => + { + if (!string.IsNullOrEmpty(value)) + { + logger.LogError("{Message}", value); + } + }, + cancellationToken); + + if (!File.Exists(generatedXrefMapPath)) + { + result = false; + + logger.LogError("Error: '{XrefMapFilePath}' for Unity '{RepositoryTag}' not generated", generatedXrefMapPath, repositoryTag); + + continue; + } + + logger.LogInformation("Fixing hrefs in '{XrefMapFilePath}'", Path.GetFullPath(xrefMapPath)); + + await Utils.CopyFile(generatedXrefMapPath, xrefMapPath, cancellationToken); + + XrefMap xrefMap = await XrefMap.Load(xrefMapPath, cancellationToken); + + xrefMap.FixHrefs(apiUrl: string.Format(apiUrl!, shortVersion)); + + await xrefMap.Save(xrefMapPath, cancellationToken); + } + + return result ? 0 : 1; + }); + } + + [GeneratedRegex(@"(?\d+)\.(?\d+)\.(?\d+)")] + private static partial Regex VersionRegex(); +} diff --git a/UnityXrefMaps/Commands/TestCommand.cs b/UnityXrefMaps/Commands/TestCommand.cs new file mode 100644 index 0000000..958374d --- /dev/null +++ b/UnityXrefMaps/Commands/TestCommand.cs @@ -0,0 +1,39 @@ +using System.CommandLine; +using Microsoft.Extensions.Logging; + +namespace UnityXrefMaps.Commands; + +internal sealed class TestCommand : Command +{ + public TestCommand(ILogger logger) : base("test", $"Check that the links in the {Constants.DefaultXrefMapFileName} file are valid.") + { + Option xrefPathOption = new("--xrefPath") + { + Description = $"The path to the {Constants.DefaultXrefMapFileName} file.", + Required = true + }; + + Options.Add(xrefPathOption); + + SetAction(async (parseResult, cancellationToken) => + { + bool result = true; + + string? xrefPath = parseResult.GetValue(xrefPathOption); + + XrefMap xrefMap = await XrefMap.Load(xrefPath!, cancellationToken); + + foreach (XrefMapReference reference in xrefMap.References!) + { + if (!await Utils.TestUriExists(reference.Href, logger, cancellationToken)) + { + result = false; + + logger.LogWarning("Invalid URL {Href} for {Uid} uid", reference.Href, reference.Uid); + } + } + + return result ? 0 : 1; + }); + } +} diff --git a/UnityXrefMaps/Constants.cs b/UnityXrefMaps/Constants.cs new file mode 100644 index 0000000..da11fe6 --- /dev/null +++ b/UnityXrefMaps/Constants.cs @@ -0,0 +1,40 @@ +namespace UnityXrefMaps; + +internal static class Constants +{ + /// + /// Gets the default URL of the online API documentation of Unity. + /// + public const string DefaultUnityApiUrl = "https://docs.unity3d.com/{0}/Documentation/ScriptReference/"; + + /// + /// The default path of the Unity repository. + /// + public const string DefaultUnityRepositoryPath = "UnityCsReference"; + + /// + /// The default URL of the Unity repository. + /// + public const string DefaultUnityRepositoryUrl = "https://github.com/Unity-Technologies/UnityCsReference.git"; + + /// + /// The default branch of the Unity repository. + /// + public const string DefaultUnityRepositoryBranch = "master"; + + // https://github.com/dotnet/docfx/blob/1c4e9ff4a2d236206eee04066847a98343c6a3f7/src/Docfx.Build/XRefMaps/XRefArchive.cs#L14 + /// + /// The default xref map filename. + /// + public const string DefaultXrefMapFileName = "xrefmap.yml"; + + /// + /// The default path where to copy the xref maps. + /// + public const string DefaultXrefMapsPath = $"_site/{{0}}/{DefaultXrefMapFileName}"; + + /// + /// The default DocFX config file path. + /// + public const string DefaultDocFxConfigurationFilePath = "docfx.json"; +} diff --git a/DocFxConfiguration.cs b/UnityXrefMaps/DocFX/DocFxConfiguration.cs similarity index 85% rename from DocFxConfiguration.cs rename to UnityXrefMaps/DocFX/DocFxConfiguration.cs index 9b65faa..ca957b6 100644 --- a/DocFxConfiguration.cs +++ b/UnityXrefMaps/DocFX/DocFxConfiguration.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace DocFxForUnity; +namespace UnityXrefMaps.DocFX; // https://github.com/dotnet/docfx/blob/main/src/Docfx.App/Config/DocfxConfig.cs public class DocFxConfiguration diff --git a/DocFxConfigurationBuild.cs b/UnityXrefMaps/DocFX/DocFxConfigurationBuild.cs similarity index 85% rename from DocFxConfigurationBuild.cs rename to UnityXrefMaps/DocFX/DocFxConfigurationBuild.cs index 72a412a..8895399 100644 --- a/DocFxConfigurationBuild.cs +++ b/UnityXrefMaps/DocFX/DocFxConfigurationBuild.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace DocFxForUnity; +namespace UnityXrefMaps.DocFX; // https://github.com/dotnet/docfx/blob/main/src/Docfx.App/Config/BuildJsonConfig.cs public class DocFxConfigurationBuild diff --git a/UnityXrefMaps/Git.cs b/UnityXrefMaps/Git.cs new file mode 100644 index 0000000..1852aa6 --- /dev/null +++ b/UnityXrefMaps/Git.cs @@ -0,0 +1,49 @@ +using System.IO; +using LibGit2Sharp; +using Microsoft.Extensions.Logging; + +namespace UnityXrefMaps; + +internal static class Git +{ + /// + /// Fetches changes and hard resets the specified repository to the latest commit of a specified branch. If no + /// repository is found, it will be cloned before. + /// + /// The url of the repository. + /// The directory path where to find/clone the repository. + /// The branch use on the repository. + /// The synced repository on the latest commit of the specified branch. + public static Repository GetSyncRepository(string sourceUrl, string path, string branch, ILogger logger) + { + // Clone this repository to the specified branch if it doesn't exist + bool clone = !Directory.Exists(path); + if (clone) + { + logger.LogInformation("Cloning {SourceUrl} to {Path}", sourceUrl, path); + + var options = new CloneOptions() { BranchName = branch }; + Repository.Clone(sourceUrl, path, options); + } + + var repository = new Repository(path); + + // Otherwise fetch changes and checkout to the specified branch + if (!clone) + { + logger.LogInformation("Hard reset '{Path}' to HEAD", path); + repository.Reset(ResetMode.Hard); + repository.RemoveUntrackedFiles(); + + logger.LogInformation("Fetching changes from 'origin' in '{Path}'", path); + Remote remote = repository.Network.Remotes["origin"]; + LibGit2Sharp.Commands.Fetch(repository, remote.Name, [], null, null); // WTF is this API libgit2sharp? + + logger.LogInformation("Checking out '{Path}' to '{Branch}' branch", path, branch); + string remoteBranch = $"origin/{branch}"; + LibGit2Sharp.Commands.Checkout(repository, remoteBranch); + } + + return repository; + } +} diff --git a/UnityXrefMaps/Program.cs b/UnityXrefMaps/Program.cs new file mode 100644 index 0000000..8f50a9f --- /dev/null +++ b/UnityXrefMaps/Program.cs @@ -0,0 +1,13 @@ +using System.CommandLine; +using Microsoft.Extensions.Logging; +using UnityXrefMaps.Commands; + +ILoggerFactory factory = LoggerFactory.Create(builder => +{ + builder.AddConsole(); +}); + +RootCommand rootCommand = new BuildCommand(factory.CreateLogger()); +rootCommand.Subcommands.Add(new TestCommand(factory.CreateLogger())); + +await rootCommand.Parse(args).InvokeAsync(); diff --git a/Properties/launchSettings.json b/UnityXrefMaps/Properties/launchSettings.json similarity index 100% rename from Properties/launchSettings.json rename to UnityXrefMaps/Properties/launchSettings.json diff --git a/RepositoryExtensions.cs b/UnityXrefMaps/RepositoryExtensions.cs similarity index 72% rename from RepositoryExtensions.cs rename to UnityXrefMaps/RepositoryExtensions.cs index 72c0e2b..eb597ef 100644 --- a/RepositoryExtensions.cs +++ b/UnityXrefMaps/RepositoryExtensions.cs @@ -1,30 +1,31 @@ using System; using LibGit2Sharp; +using Microsoft.Extensions.Logging; -namespace DocFxForUnity; +namespace UnityXrefMaps; /// /// Extension methods for . /// -public static class RepositoryExtensions +internal static class RepositoryExtensions { /// /// Hard resets the specified to the specified commit. /// /// The to hard reset to . /// The name of the commit where to reset . - public static void HardReset(this Repository repository, string commit) + public static void HardReset(this Repository repository, string commit, ILogger logger) { - Console.WriteLine($"Hard reset to {commit}"); + logger.LogInformation("Hard reset to {Commit}", commit); repository.Reset(ResetMode.Hard, commit); try { - Console.WriteLine($"Removing untracked files"); + logger.LogInformation($"Removing untracked files"); repository.RemoveUntrackedFiles(); } catch (Exception) { } } -} \ No newline at end of file +} diff --git a/UnityXrefMaps.csproj b/UnityXrefMaps/UnityXrefMaps.csproj similarity index 79% rename from UnityXrefMaps.csproj rename to UnityXrefMaps/UnityXrefMaps.csproj index 6945a80..fd9fb89 100644 --- a/UnityXrefMaps.csproj +++ b/UnityXrefMaps/UnityXrefMaps.csproj @@ -4,10 +4,8 @@ Exe net9.0 enable - DocFxForUnity - DocFxForUnity true - docFxForUnity + unityXrefMaps LICENSE.txt README.md https://github.com/NormandErwan/UnityXrefMaps @@ -16,13 +14,14 @@ + - - + + diff --git a/UnityXrefMaps/Utils.cs b/UnityXrefMaps/Utils.cs new file mode 100644 index 0000000..39c5dc7 --- /dev/null +++ b/UnityXrefMaps/Utils.cs @@ -0,0 +1,89 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace UnityXrefMaps; + +internal static class Utils +{ + /// + /// Client for send HTTP requests and receiving HTTP responses. + /// + private static readonly HttpClient s_httpClient = new(); + + /// + /// Copy a source file to a destination file. Intermediate folders will be automatically created. + /// + /// The path of the source file to copy. + /// The destination path of the copied file. + public static async Task CopyFile(string sourcePath, string destPath, CancellationToken cancellationToken = default) + { + string? destDirectoryPath = Path.GetDirectoryName(destPath); + + Directory.CreateDirectory(destDirectoryPath!); + + using Stream source = File.OpenRead(sourcePath); + using Stream destination = File.Create(destPath); + + await source.CopyToAsync(destination, cancellationToken); + } + + /// + /// Run a command in a hidden window and returns its output. + /// + /// The command to run. + /// The arguments of the command. + /// The function to call with the output data of the command. + /// The function to call with the error data of the command. + public static async Task RunCommand(string command, string arguments, Action output, Action error, CancellationToken cancellationToken = default) + { + using var process = new Process(); + process.StartInfo = new ProcessStartInfo(command, arguments) + { + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true + }; + + process.OutputDataReceived += (sender, args) => output(args.Data); + process.ErrorDataReceived += (sender, args) => error(args.Data); + + process.Start(); + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + + await process.WaitForExitAsync(cancellationToken); + } + + /// + /// Requests the specified URI with and returns if the response status code is in the + /// range 200-299. + /// + /// The URI to request. + /// true if the response status code is in the range 200-299. + public static async Task TestUriExists(string? uri, ILogger logger, CancellationToken cancellationToken = default) + { + try + { + HttpResponseMessage response = await s_httpClient.SendAsync(new(HttpMethod.Head, uri), cancellationToken); + + if (!response.IsSuccessStatusCode && response.StatusCode != System.Net.HttpStatusCode.NotFound) + { + logger.LogError("HTTP response code on {Uri} is {StatusCode}", uri, response.StatusCode); + } + + return response.IsSuccessStatusCode; + } + catch (HttpRequestException e) + { + logger.LogError(e, "Exception on {Uri}", uri); + + return false; + } + } +} diff --git a/UnityXrefMaps/XrefMap.cs b/UnityXrefMaps/XrefMap.cs new file mode 100644 index 0000000..784b563 --- /dev/null +++ b/UnityXrefMaps/XrefMap.cs @@ -0,0 +1,74 @@ +using System.Collections.Generic; +using System.IO; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using YamlDotNet.Serialization; + +namespace UnityXrefMaps; + +/// +/// Represents a xref map file of Unity. +/// +public sealed partial class XrefMap +{ + private static readonly Deserializer s_deserializer = new(); + private static readonly Serializer s_serializer = new(); + + [YamlMember(Alias = "sorted")] + public bool Sorted { get; set; } + + [YamlMember(Alias = "references")] + public XrefMapReference[]? References { get; set; } + + /// + /// Loads a from a file. + /// + /// The path of the file. + /// The loaded from . + public static async Task Load(string filePath, CancellationToken cancellationToken = default) + { + string xrefMapText = await File.ReadAllTextAsync(filePath, cancellationToken); + + // Remove `0:` strings on the xrefmap that make crash Deserializer + xrefMapText = ZeroStringsRegex().Replace(xrefMapText, "$1"); + + return s_deserializer.Deserialize(xrefMapText); + } + + /// + /// Fix the of of this . + /// + /// The URL of the online API documentation of Unity. + public void FixHrefs(string apiUrl) + { + var fixedReferences = new List(); + + foreach (XrefMapReference reference in References!) + { + if (!reference.IsValid) + { + continue; + } + + reference.FixHref(apiUrl); + fixedReferences.Add(reference); + } + + References = [.. fixedReferences]; + } + + /// + /// Saves this to a file. + /// + /// The path of the file. + public async Task Save(string filePath, CancellationToken cancellationToken = default) + { + string xrefMapText = "### YamlMime:XRefMap\n" + s_serializer.Serialize(this); + + await File.WriteAllTextAsync(filePath, xrefMapText, cancellationToken); + } + + [GeneratedRegex(@"(\d):")] + private static partial Regex ZeroStringsRegex(); +} diff --git a/UnityXrefMaps/XrefMapReference.cs b/UnityXrefMaps/XrefMapReference.cs new file mode 100644 index 0000000..2726aff --- /dev/null +++ b/UnityXrefMaps/XrefMapReference.cs @@ -0,0 +1,118 @@ +using System.Text.RegularExpressions; +using YamlDotNet.Serialization; + +namespace UnityXrefMaps; + +/// +/// Represents a reference item on a . +/// +public sealed partial class XrefMapReference +{ + /// + /// The online API documentation of Unity doesn't show some namespaces. + /// + private static readonly string[] s_hrefNamespacesToTrim = ["UnityEditor", "UnityEngine"]; + + [YamlMember(Alias = "uid")] + public string? Uid { get; set; } + + [YamlMember(Alias = "name")] + public string? Name { get; set; } + + [YamlMember(Alias = "name.vb")] + public string? NameVb { get; set; } + + [YamlMember(Alias = "href")] + public string? Href { get; set; } + + [YamlMember(Alias = "commentId")] + public string? CommentId { get; set; } + + [YamlMember(Alias = "isSpec")] + public string? IsSpec { get; set; } + + [YamlMember(Alias = "fullName")] + public string? FullName { get; set; } + + [YamlMember(Alias = "fullName.vb")] + public string? FullNameVb { get; set; } + + [YamlMember(Alias = "nameWithType")] + public string? NameWithType { get; set; } + + [YamlMember(Alias = "nameWithType.vb")] + public string? NameWithTypeVb { get; set; } + + /// + /// Gets if this is valid or not. + /// + [YamlIgnore] + public bool IsValid => !CommentId!.Contains("Overload:"); + + /// + /// Sets to link to the online API documentation of Unity. + /// + /// The URL of the online API documentation of Unity. + public void FixHref(string apiUrl) + { + // Namespaces point to documentation index + if (CommentId!.StartsWith("N:")) + { + Href = "index"; + } + else + { + Href = Uid; + + // Trim UnityEngine and UnityEditor namespaces from href + foreach (string hrefNamespaceToTrim in s_hrefNamespacesToTrim) + { + Href = Href!.Replace(hrefNamespaceToTrim + ".", string.Empty); + } + + // Fix href of constructors + Href = Href!.Replace(".#ctor", "-ctor"); + + // Fix href of generics + Href = GenericHrefRegex().Replace(Href!, string.Empty); + Href = Href.Replace("`", "_"); + + // Fix href of methods + Href = MethodHrefPointerRegex().Replace(Href, string.Empty); + Href = MethodHrefRegex().Replace(Href, string.Empty); + + // Fix href of operator + if (CommentId.StartsWith("M:") && CommentId.Contains(".op_")) + { + Href = Href.Replace(".op_", ".operator_"); + + Href = Href.Replace(".operator_Subtraction", ".operator_subtract"); + Href = Href.Replace(".operator_Multiply", ".operator_multiply"); + Href = Href.Replace(".operator_Division", ".operator_divide"); + Href = Href.Replace(".operator_Addition", ".operator_add"); + Href = Href.Replace(".operator_Equality", ".operator_eq"); + Href = Href.Replace(".operator_Implicit~", ".operator_"); + } + + // Fix href of properties + if (CommentId.StartsWith("F:") || CommentId.StartsWith("M:") || CommentId.StartsWith("P:")) + { + Href = PropertyHrefRegex().Replace(Href, "-$1"); + } + } + + Href = apiUrl + Href + ".html"; + } + + [GeneratedRegex(@"`{2}\d")] + private static partial Regex GenericHrefRegex(); + + [GeneratedRegex(@"\*$")] + private static partial Regex MethodHrefPointerRegex(); + + [GeneratedRegex(@"\(.*\)")] + private static partial Regex MethodHrefRegex(); + + [GeneratedRegex(@"\.([a-z].*)$")] + private static partial Regex PropertyHrefRegex(); +} diff --git a/docfx.json b/UnityXrefMaps/docfx.json similarity index 84% rename from docfx.json rename to UnityXrefMaps/docfx.json index fc7acb9..824a1e5 100644 --- a/docfx.json +++ b/UnityXrefMaps/docfx.json @@ -12,7 +12,9 @@ } ], "build": { - "xrefService": [ "https://xref.docs.microsoft.com/query?uid={uid}" ], + "xref": [ + "https://learn.microsoft.com/en-us/dotnet/.xrefmap.json" + ], "content": [ { "src": "UnityCsReference/_api", @@ -22,4 +24,4 @@ ], "dest": "UnityCsReference/_site" } -} \ No newline at end of file +} diff --git a/UnityXrefMaps/filterConfig.yml b/UnityXrefMaps/filterConfig.yml new file mode 100644 index 0000000..b70045a --- /dev/null +++ b/UnityXrefMaps/filterConfig.yml @@ -0,0 +1,24 @@ +### YamlMime:ManagedReference +--- +apiRules: + - exclude: + uidRegex: ^AOT + - exclude: + uidRegex: ^JetBrains + - exclude: + uidRegex: ^TreeEditor + - exclude: + uidRegex: ^Unity\.CodeEditor + - exclude: + uidRegex: ^UnityEditorInternal + - exclude: + uidRegex: ^UnityEditor\.InspectorMode + - exclude: + uidRegex: ^UnityEngineInternal + - exclude: + uidRegex: ^UnityEngine.Internal + - exclude: + uidRegex: Finalize$ + - exclude: + hasAttribute: + uid: UnityEngine.Internal.ExcludeFromDocsAttribute diff --git a/Utils.cs b/Utils.cs deleted file mode 100644 index d8431d5..0000000 --- a/Utils.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; - -namespace DocFxForUnity -{ - public static class Utils - { - /// - /// Client for send HTTP requests and receiving HTTP responses. - /// - private static readonly HttpClient s_httpClient = new(); - - /// - /// Copy a source file to a destination file. Intermediate folders will be automatically created. - /// - /// The path of the source file to copy. - /// The destination path of the copied file. - public static async Task CopyFile(string sourcePath, string destPath, CancellationToken cancellationToken = default) - { - string? destDirectoryPath = Path.GetDirectoryName(destPath); - - Directory.CreateDirectory(destDirectoryPath!); - - using Stream source = File.OpenRead(sourcePath); - using Stream destination = File.Create(destPath); - - await source.CopyToAsync(destination, cancellationToken); - } - - /// - /// Run a command in a hidden window and returns its output. - /// - /// The command to run. - /// The arguments of the command. - /// The function to call with the output data of the command. - /// The function to call with the error data of the command. - public static async Task RunCommand(string command, string arguments, Action output, Action error, CancellationToken cancellationToken = default) - { - using var process = new Process(); - process.StartInfo = new ProcessStartInfo(command, arguments) - { - UseShellExecute = false, - CreateNoWindow = true, - RedirectStandardOutput = true, - RedirectStandardError = true - }; - - process.OutputDataReceived += (sender, args) => output(args.Data); - process.ErrorDataReceived += (sender, args) => error(args.Data); - - process.Start(); - process.BeginOutputReadLine(); - process.BeginErrorReadLine(); - - await process.WaitForExitAsync(cancellationToken); - } - - /// - /// Requests the specified URI with and returns if the response status code is in the - /// range 200-299. - /// - /// The URI to request. - /// true if the response status code is in the range 200-299. - public static async Task TestUriExists(string? uri, CancellationToken cancellationToken = default) - { - try - { - HttpResponseMessage response = await s_httpClient.SendAsync(new(HttpMethod.Head, uri), cancellationToken); - - if (!response.IsSuccessStatusCode && response.StatusCode != System.Net.HttpStatusCode.NotFound) - { - Console.Error.WriteLine($"Error: HTTP response code on {uri} is {response.StatusCode}"); - } - - return response.IsSuccessStatusCode; - } - catch (HttpRequestException e) - { - Console.WriteLine($"Exception on {uri}: {e.Message}"); - - return false; - } - } - } -} \ No newline at end of file diff --git a/XrefMap.cs b/XrefMap.cs deleted file mode 100644 index 196c7af..0000000 --- a/XrefMap.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; -using YamlDotNet.Serialization; - -namespace DocFxForUnity -{ - /// - /// Represents a xref map file of Unity. - /// - public sealed partial class XrefMap - { - private static readonly Deserializer s_deserializer = new(); - private static readonly Serializer s_serializer = new(); - - [YamlMember(Alias = "sorted")] - public bool Sorted { get; set; } - - [YamlMember(Alias = "references")] - public XrefMapReference[]? References { get; set; } - - /// - /// Loads a from a file. - /// - /// The path of the file. - /// The loaded from . - public static async Task Load(string filePath, CancellationToken cancellationToken = default) - { - string xrefMapText = await File.ReadAllTextAsync(filePath, cancellationToken); - - // Remove `0:` strings on the xrefmap that make crash Deserializer - xrefMapText = ZeroStringsRegex().Replace(xrefMapText, "$1"); - - return s_deserializer.Deserialize(xrefMapText); - } - - /// - /// Fix the of of this . - /// - /// The URL of the online API documentation of Unity. - public void FixHrefs(string apiUrl) - { - var fixedReferences = new List(); - - foreach (XrefMapReference reference in References!) - { - if (!reference.IsValid) - { - continue; - } - - reference.FixHref(apiUrl); - fixedReferences.Add(reference); - } - - References = [.. fixedReferences]; - } - - /// - /// Saves this to a file. - /// - /// The path of the file. - public async Task Save(string filePath, CancellationToken cancellationToken = default) - { - string xrefMapText = "### YamlMime:XRefMap\n" + s_serializer.Serialize(this); - - await File.WriteAllTextAsync(filePath, xrefMapText, cancellationToken); - } - - [GeneratedRegex(@"(\d):")] - private static partial Regex ZeroStringsRegex(); - } -} \ No newline at end of file diff --git a/XrefMapReference.cs b/XrefMapReference.cs deleted file mode 100644 index 2056c0a..0000000 --- a/XrefMapReference.cs +++ /dev/null @@ -1,119 +0,0 @@ -using System.Text.RegularExpressions; -using YamlDotNet.Serialization; - -namespace DocFxForUnity -{ - /// - /// Represents a reference item on a . - /// - public sealed partial class XrefMapReference - { - /// - /// The online API documentation of Unity doesn't show some namespaces. - /// - private static readonly string[] s_hrefNamespacesToTrim = ["UnityEditor", "UnityEngine"]; - - [YamlMember(Alias = "uid")] - public string? Uid { get; set; } - - [YamlMember(Alias = "name")] - public string? Name { get; set; } - - [YamlMember(Alias = "name.vb")] - public string? NameVb { get; set; } - - [YamlMember(Alias = "href")] - public string? Href { get; set; } - - [YamlMember(Alias = "commentId")] - public string? CommentId { get; set; } - - [YamlMember(Alias = "isSpec")] - public string? IsSpec { get; set; } - - [YamlMember(Alias = "fullName")] - public string? FullName { get; set; } - - [YamlMember(Alias = "fullName.vb")] - public string? FullNameVb { get; set; } - - [YamlMember(Alias = "nameWithType")] - public string? NameWithType { get; set; } - - [YamlMember(Alias = "nameWithType.vb")] - public string? NameWithTypeVb { get; set; } - - /// - /// Gets if this is valid or not. - /// - [YamlIgnore] - public bool IsValid => !CommentId!.Contains("Overload:"); - - /// - /// Sets to link to the online API documentation of Unity. - /// - /// The URL of the online API documentation of Unity. - public void FixHref(string apiUrl) - { - // Namespaces point to documentation index - if (CommentId!.StartsWith("N:")) - { - Href = "index"; - } - else - { - Href = Uid; - - // Trim UnityEngine and UnityEditor namespaces from href - foreach (string hrefNamespaceToTrim in s_hrefNamespacesToTrim) - { - Href = Href!.Replace(hrefNamespaceToTrim + ".", string.Empty); - } - - // Fix href of constructors - Href = Href!.Replace(".#ctor", "-ctor"); - - // Fix href of generics - Href = GenericHrefRegex().Replace(Href!, string.Empty); - Href = Href.Replace("`", "_"); - - // Fix href of methods - Href = MethodHrefPointerRegex().Replace(Href, string.Empty); - Href = MethodHrefRegex().Replace(Href, string.Empty); - - // Fix href of operator - if (CommentId.StartsWith("M:") && CommentId.Contains(".op_")) - { - Href = Href.Replace(".op_", ".operator_"); - - Href = Href.Replace(".operator_Subtraction", ".operator_subtract"); - Href = Href.Replace(".operator_Multiply", ".operator_multiply"); - Href = Href.Replace(".operator_Division", ".operator_divide"); - Href = Href.Replace(".operator_Addition", ".operator_add"); - Href = Href.Replace(".operator_Equality", ".operator_eq"); - Href = Href.Replace(".operator_Implicit~", ".operator_"); - } - - // Fix href of properties - if (CommentId.StartsWith("F:") || CommentId.StartsWith("M:") || CommentId.StartsWith("P:")) - { - Href = PropertyHrefRegex().Replace(Href, "-$1"); - } - } - - Href = apiUrl + Href + ".html"; - } - - [GeneratedRegex(@"`{2}\d")] - private static partial Regex GenericHrefRegex(); - - [GeneratedRegex(@"\*$")] - private static partial Regex MethodHrefPointerRegex(); - - [GeneratedRegex(@"\(.*\)")] - private static partial Regex MethodHrefRegex(); - - [GeneratedRegex(@"\.([a-z].*)$")] - private static partial Regex PropertyHrefRegex(); - } -} diff --git a/filterConfig.yml b/filterConfig.yml deleted file mode 100644 index e4aef4c..0000000 --- a/filterConfig.yml +++ /dev/null @@ -1,22 +0,0 @@ -apiRules: -- exclude: - uidRegex: ^AOT -- exclude: - uidRegex: ^JetBrains -- exclude: - uidRegex: ^TreeEditor -- exclude: - uidRegex: ^Unity\.CodeEditor -- exclude: - uidRegex: ^UnityEditorInternal -- exclude: - uidRegex: ^UnityEditor\.InspectorMode -- exclude: - uidRegex: ^UnityEngineInternal -- exclude: - uidRegex: ^UnityEngine.Internal -- exclude: - uidRegex: Finalize$ -- exclude: - hasAttribute: - uid: UnityEngine.Internal.ExcludeFromDocsAttribute \ No newline at end of file From cce605ea5db338fedcdd91cc2385dd7f964268ba Mon Sep 17 00:00:00 2001 From: bdovaz Date: Thu, 25 Sep 2025 16:15:33 +0200 Subject: [PATCH 03/29] wip --- .github/workflows/ci.yml | 1 - .github/workflows/tool.yml | 45 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/tool.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8daa862..7f75509 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,6 @@ name: ci on: - workflow_dispatch: push: branches: - main diff --git a/.github/workflows/tool.yml b/.github/workflows/tool.yml new file mode 100644 index 0000000..3c57e34 --- /dev/null +++ b/.github/workflows/tool.yml @@ -0,0 +1,45 @@ +--- +name: tool + +on: + push: + branches: + - main + tags: + - '*' + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Should Deploy? + if: ${{ success() && github.event_name == 'push' }} + shell: pwsh + run: | + if ("${{github.ref}}" -match "^refs/tags/([0-9]+\.[0-9]+\.[0-9]+)") + { + $version = $Matches[1] + + echo "UNITY_XREF_MAPS_DEPLOY=1" >> $env:GITHUB_ENV + echo "UNITY_XREF_MAPS_VERSION=$version" >> $env:GITHUB_ENV + } + + - name: Pack + if: ${{ success() && env.UNITY_XREF_MAPS_DEPLOY != 1 }} + run: dotnet pack UnityXrefMaps/UnityXrefMaps.csproj -c Release -o ${{ runner.temp }} + + - name: Pack (Release) + if: ${{ success() && env.UNITY_XREF_MAPS_DEPLOY == 1 }} + run: dotnet pack UnityXrefMaps/UnityXrefMaps.csproj -c Release -p:PackageVersion=$UNITY_XREF_MAPS_VERSION -o ${{ runner.temp }} + + - name: Publish (Release) + if: ${{ success() && env.UNITY_XREF_MAPS_DEPLOY == 1 }} + run: | + dotnet nuget push ${{ runner.temp }}/UnityXrefMaps.${{ env.UNITY_XREF_MAPS_VERSION }}.nupkg \ + --source https://api.nuget.org/v3/index.json \ + --api-key ${{ secrets.NUGET_API_KEY }} From 444bfa509fa8f4a53448fe9ae950d388326bf961 Mon Sep 17 00:00:00 2001 From: bdovaz Date: Thu, 25 Sep 2025 20:31:47 +0200 Subject: [PATCH 04/29] wip --- UnityXrefMaps.Tests/CommandTests.cs | 472 +++++++++--------- UnityXrefMaps/AssemblyInfo.cs | 6 +- UnityXrefMaps/Commands/BuildCommand.cs | 292 +++++------ UnityXrefMaps/Commands/TestCommand.cs | 78 +-- UnityXrefMaps/Constants.cs | 80 +-- UnityXrefMaps/DocFX/DocFxConfiguration.cs | 20 +- .../DocFX/DocFxConfigurationBuild.cs | 20 +- UnityXrefMaps/Program.cs | 26 +- UnityXrefMaps/RepositoryExtensions.cs | 4 +- UnityXrefMaps/Utils.cs | 10 +- UnityXrefMaps/XrefMap.cs | 8 +- UnityXrefMaps/XrefMapReference.cs | 40 +- 12 files changed, 528 insertions(+), 528 deletions(-) diff --git a/UnityXrefMaps.Tests/CommandTests.cs b/UnityXrefMaps.Tests/CommandTests.cs index dc0ff55..2723b23 100644 --- a/UnityXrefMaps.Tests/CommandTests.cs +++ b/UnityXrefMaps.Tests/CommandTests.cs @@ -1,236 +1,236 @@ -using Meziantou.Extensions.Logging.Xunit.v3; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Testing; -using System; -using System.Collections.Generic; -using System.CommandLine; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using UnityXrefMaps.Commands; - -namespace UnityXrefMaps.Tests -{ - public class CommandTests : IAsyncDisposable - { - private static readonly string repositoryDirectoryPath = Guid.NewGuid().ToString(); - private static readonly string xrefDirectoryPath = Guid.NewGuid().ToString(); - private static readonly string docFxFilePath = Guid.NewGuid().ToString() + "_config.json"; - private static readonly string docFxFilterFilePath = Guid.NewGuid().ToString() + "_filter_config.yml"; - - private class CustomStringWriter : StringWriter - { - private readonly ILogger logger; - private readonly LogLevel logLevel; - - public CustomStringWriter(ILogger logger, LogLevel logLevel) - { - this.logger = logger; - this.logLevel = logLevel; - } - - public override void Write(string? value) - { - base.Write(value); - - logger.Log(logLevel, "{Message}", value); - } - } - - private readonly ITestOutputHelper output; - - public CommandTests(ITestOutputHelper output) - { - this.output = output; - } - - [Fact] - public async Task BuildTest_Success() - { - string docFxFileContent = await File.ReadAllTextAsync("docfx.json", TestContext.Current.CancellationToken); - - docFxFileContent = docFxFileContent.Replace("UnityCsReference/", repositoryDirectoryPath + '/'); - docFxFileContent = docFxFileContent.Replace("filterConfig.yml", docFxFilterFilePath); - - await File.WriteAllTextAsync(docFxFilePath, docFxFileContent, TestContext.Current.CancellationToken); - - string docFxFilterConfigContent = """ -### YamlMime:ManagedReference ---- -apiRules: - - include: - uidRegex: ^UnityEngine\.Vector2$ - - include: - uidRegex: ^UnityEngine\.Vector3$ - - exclude: - uidRegex: .* -"""; - - await File.WriteAllTextAsync(docFxFilterFilePath, docFxFilterConfigContent, TestContext.Current.CancellationToken); - - var serviceCollection = new ServiceCollection(); - - serviceCollection.AddLogging(builder => - { - builder.SetMinimumLevel(LogLevel.Trace); - builder.AddFakeLogging(); - builder.Services.AddSingleton(new XUnitLoggerProvider(output, appendScope: false)); - }); - - await using ServiceProvider serviceProvider = serviceCollection.BuildServiceProvider(); - - FakeLogCollector fakeLogCollector = serviceProvider.GetFakeLogCollector(); - - ILoggerFactory loggerFactory = serviceProvider.GetRequiredService(); - - ILogger logger = loggerFactory.CreateLogger(); - - InvocationConfiguration invocationConfiguration = new() - { - Error = new CustomStringWriter(logger, LogLevel.Error), - Output = new CustomStringWriter(logger, LogLevel.Information), - }; - - string[] testedVersions = ["6000.0.1f1", "6000.1.1f1"]; - - BuildCommand buildCommand = new(loggerFactory.CreateLogger()); - - string xrefDirectoryName = "test"; - string xrefFileName = "test2.yml"; - - string[] buildArgs = [ - "--repositoryPath", - repositoryDirectoryPath, - "--docFxConfigurationFilePath", - docFxFilePath, - "--xrefMapsPath", - $"{xrefDirectoryPath}/{xrefDirectoryName}/{{0}}/{xrefFileName}" - ]; - - buildArgs = [.. buildArgs, .. testedVersions.SelectMany(v => new string[] { "--repositoryTags", v })]; - - Assert.Equal( - 0, - await buildCommand - .Parse(buildArgs) - .InvokeAsync( - invocationConfiguration, - TestContext.Current.CancellationToken)); - - IReadOnlyList logRecords = fakeLogCollector.GetSnapshot(); - - Assert.Equal(2, logRecords.Count(l => l.Message.Equals("XRef map exported."))); - - foreach (string testedVersion in testedVersions) - { - string xrefFilePath = $"{xrefDirectoryPath}/{xrefDirectoryName}/{testedVersion}/{xrefFileName}"; - - Assert.True(File.Exists(xrefFilePath)); - - string xrefFileContent = await File.ReadAllTextAsync(xrefFilePath, TestContext.Current.CancellationToken); - - logger.LogInformation("{FilePath}:\n\n{FileContent}", xrefFilePath, xrefFileContent); - } - - TestCommand testCommand = new(loggerFactory.CreateLogger()); - - foreach (string testedVersion in testedVersions) - { - fakeLogCollector.Clear(); - - string[] testArgs = [ - "--xrefPath", - $"{xrefDirectoryPath}/{xrefDirectoryName}/{testedVersion}/{xrefFileName}" - ]; - - Assert.Equal( - 0, - await testCommand - .Parse(testArgs) - .InvokeAsync( - invocationConfiguration, - TestContext.Current.CancellationToken)); - - Assert.Equal(0, fakeLogCollector.Count); - } - } - - // https://stackoverflow.com/a/1702920 - private static void DeleteDirectory(string targetDir) - { - File.SetAttributes(targetDir, FileAttributes.Normal); - - string[] files = Directory.GetFiles(targetDir); - string[] dirs = Directory.GetDirectories(targetDir); - - foreach (string file in files) - { - File.SetAttributes(file, FileAttributes.Normal); - File.Delete(file); - } - - foreach (string dir in dirs) - { - DeleteDirectory(dir); - } - - Directory.Delete(targetDir, false); - } - - public async ValueTask DisposeAsync() - { - await DisposeAsyncCore().ConfigureAwait(false); - - GC.SuppressFinalize(this); - } - - protected virtual async ValueTask DisposeAsyncCore() - { - File.Delete(docFxFilePath); - File.Delete(docFxFilterFilePath); - - const int maxRetries = 3; - const int delay = 500; - - await DeleteDirectoryWithRetries(repositoryDirectoryPath, maxRetries, delay); - await DeleteDirectoryWithRetries(xrefDirectoryPath, maxRetries, delay); - } - - private async Task DeleteDirectoryWithRetries(string directoryPath, int maxRetries, int delay) - { - int retries = 0; - - while (retries < maxRetries) - { - try - { - output.WriteLine($"Trying to delete directory: {directoryPath}"); - - DeleteDirectory(directoryPath); - - output.WriteLine($"Directory deleted: {directoryPath}"); - - break; - } - catch (Exception e) - { - output.WriteLine($"Error deleting directory: {directoryPath}. Error: {e}"); - - retries++; - - if (retries < maxRetries) - { - output.WriteLine($"Retrying in {delay}ms..."); - - await Task.Delay(delay); - } - else - { - output.WriteLine("Retry limit reached."); - } - } - } - } - } -} +using Meziantou.Extensions.Logging.Xunit.v3; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; +using System; +using System.Collections.Generic; +using System.CommandLine; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using UnityXrefMaps.Commands; + +namespace UnityXrefMaps.Tests +{ + public class CommandTests : IAsyncDisposable + { + private static readonly string repositoryDirectoryPath = Guid.NewGuid().ToString(); + private static readonly string xrefDirectoryPath = Guid.NewGuid().ToString(); + private static readonly string docFxFilePath = Guid.NewGuid().ToString() + "_config.json"; + private static readonly string docFxFilterFilePath = Guid.NewGuid().ToString() + "_filter_config.yml"; + + private class CustomStringWriter : StringWriter + { + private readonly ILogger logger; + private readonly LogLevel logLevel; + + public CustomStringWriter(ILogger logger, LogLevel logLevel) + { + this.logger = logger; + this.logLevel = logLevel; + } + + public override void Write(string? value) + { + base.Write(value); + + logger.Log(logLevel, "{Message}", value); + } + } + + private readonly ITestOutputHelper output; + + public CommandTests(ITestOutputHelper output) + { + this.output = output; + } + + [Fact] + public async Task BuildTest_Success() + { + string docFxFileContent = await File.ReadAllTextAsync("docfx.json", TestContext.Current.CancellationToken); + + docFxFileContent = docFxFileContent.Replace("UnityCsReference/", repositoryDirectoryPath + '/'); + docFxFileContent = docFxFileContent.Replace("filterConfig.yml", docFxFilterFilePath); + + await File.WriteAllTextAsync(docFxFilePath, docFxFileContent, TestContext.Current.CancellationToken); + + string docFxFilterConfigContent = """ +### YamlMime:ManagedReference +--- +apiRules: + - include: + uidRegex: ^UnityEngine\.Vector2$ + - include: + uidRegex: ^UnityEngine\.Vector3$ + - exclude: + uidRegex: .* +"""; + + await File.WriteAllTextAsync(docFxFilterFilePath, docFxFilterConfigContent, TestContext.Current.CancellationToken); + + var serviceCollection = new ServiceCollection(); + + serviceCollection.AddLogging(builder => + { + builder.SetMinimumLevel(LogLevel.Trace); + builder.AddFakeLogging(); + builder.Services.AddSingleton(new XUnitLoggerProvider(output, appendScope: false)); + }); + + await using ServiceProvider serviceProvider = serviceCollection.BuildServiceProvider(); + + FakeLogCollector fakeLogCollector = serviceProvider.GetFakeLogCollector(); + + ILoggerFactory loggerFactory = serviceProvider.GetRequiredService(); + + ILogger logger = loggerFactory.CreateLogger(); + + InvocationConfiguration invocationConfiguration = new() + { + Error = new CustomStringWriter(logger, LogLevel.Error), + Output = new CustomStringWriter(logger, LogLevel.Information), + }; + + string[] testedVersions = ["6000.0.1f1", "6000.1.1f1"]; + + BuildCommand buildCommand = new(loggerFactory.CreateLogger()); + + string xrefDirectoryName = "test"; + string xrefFileName = "test2.yml"; + + string[] buildArgs = [ + "--repositoryPath", + repositoryDirectoryPath, + "--docFxConfigurationFilePath", + docFxFilePath, + "--xrefMapsPath", + $"{xrefDirectoryPath}/{xrefDirectoryName}/{{0}}/{xrefFileName}" + ]; + + buildArgs = [.. buildArgs, .. testedVersions.SelectMany(v => new string[] { "--repositoryTags", v })]; + + Assert.Equal( + 0, + await buildCommand + .Parse(buildArgs) + .InvokeAsync( + invocationConfiguration, + TestContext.Current.CancellationToken)); + + IReadOnlyList logRecords = fakeLogCollector.GetSnapshot(); + + Assert.Equal(2, logRecords.Count(l => l.Message.Equals("XRef map exported."))); + + foreach (string testedVersion in testedVersions) + { + string xrefFilePath = $"{xrefDirectoryPath}/{xrefDirectoryName}/{testedVersion}/{xrefFileName}"; + + Assert.True(File.Exists(xrefFilePath)); + + string xrefFileContent = await File.ReadAllTextAsync(xrefFilePath, TestContext.Current.CancellationToken); + + logger.LogInformation("{FilePath}:\n\n{FileContent}", xrefFilePath, xrefFileContent); + } + + TestCommand testCommand = new(loggerFactory.CreateLogger()); + + foreach (string testedVersion in testedVersions) + { + fakeLogCollector.Clear(); + + string[] testArgs = [ + "--xrefPath", + $"{xrefDirectoryPath}/{xrefDirectoryName}/{testedVersion}/{xrefFileName}" + ]; + + Assert.Equal( + 0, + await testCommand + .Parse(testArgs) + .InvokeAsync( + invocationConfiguration, + TestContext.Current.CancellationToken)); + + Assert.Equal(0, fakeLogCollector.Count); + } + } + + // https://stackoverflow.com/a/1702920 + private static void DeleteDirectory(string targetDir) + { + File.SetAttributes(targetDir, FileAttributes.Normal); + + string[] files = Directory.GetFiles(targetDir); + string[] dirs = Directory.GetDirectories(targetDir); + + foreach (string file in files) + { + File.SetAttributes(file, FileAttributes.Normal); + File.Delete(file); + } + + foreach (string dir in dirs) + { + DeleteDirectory(dir); + } + + Directory.Delete(targetDir, false); + } + + public async ValueTask DisposeAsync() + { + await DisposeAsyncCore().ConfigureAwait(false); + + GC.SuppressFinalize(this); + } + + protected virtual async ValueTask DisposeAsyncCore() + { + File.Delete(docFxFilePath); + File.Delete(docFxFilterFilePath); + + const int maxRetries = 3; + const int delay = 500; + + await DeleteDirectoryWithRetries(repositoryDirectoryPath, maxRetries, delay); + await DeleteDirectoryWithRetries(xrefDirectoryPath, maxRetries, delay); + } + + private async Task DeleteDirectoryWithRetries(string directoryPath, int maxRetries, int delay) + { + int retries = 0; + + while (retries < maxRetries) + { + try + { + output.WriteLine($"Trying to delete directory: {directoryPath}"); + + DeleteDirectory(directoryPath); + + output.WriteLine($"Directory deleted: {directoryPath}"); + + break; + } + catch (Exception e) + { + output.WriteLine($"Error deleting directory: {directoryPath}. Error: {e}"); + + retries++; + + if (retries < maxRetries) + { + output.WriteLine($"Retrying in {delay}ms..."); + + await Task.Delay(delay); + } + else + { + output.WriteLine("Retry limit reached."); + } + } + } + } + } +} diff --git a/UnityXrefMaps/AssemblyInfo.cs b/UnityXrefMaps/AssemblyInfo.cs index 153d6b8..08d26de 100644 --- a/UnityXrefMaps/AssemblyInfo.cs +++ b/UnityXrefMaps/AssemblyInfo.cs @@ -1,3 +1,3 @@ -using System.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("UnityXrefMaps.Tests")] +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("UnityXrefMaps.Tests")] diff --git a/UnityXrefMaps/Commands/BuildCommand.cs b/UnityXrefMaps/Commands/BuildCommand.cs index 943076d..77dff8b 100644 --- a/UnityXrefMaps/Commands/BuildCommand.cs +++ b/UnityXrefMaps/Commands/BuildCommand.cs @@ -1,146 +1,146 @@ -using System.CommandLine; -using System.IO; -using System.Text.Json; -using System.Text.RegularExpressions; -using LibGit2Sharp; -using Microsoft.Extensions.Logging; -using UnityXrefMaps.DocFX; - -namespace UnityXrefMaps.Commands; - -internal sealed partial class BuildCommand : RootCommand -{ - public BuildCommand(ILogger logger) - { - Option repositoryUrlOption = new("--repositoryUrl") - { - Description = "The Git repository url.", - DefaultValueFactory = _ => Constants.DefaultUnityRepositoryUrl - }; - Option repositoryBranchOption = new("--repositoryBranch") - { - Description = "The Git repository branch.", - DefaultValueFactory = _ => Constants.DefaultUnityRepositoryBranch - }; - Option repositoryPathOption = new("--repositoryPath") - { - Description = "The Git repository path to git clone. " + - "If the clone has already been made, it will reset so that the clone can be reused.", - DefaultValueFactory = _ => Constants.DefaultUnityRepositoryPath - }; - Option repositoryTagsOption = new("--repositoryTags") - { - Description = "The repository tags to use to generate the documentation. " + - "That is, the versions of the Unity editor (6000.0.1f1, 6000.1.1f1) or the versions of the Unity package (1.0.0, 1.1.0).", - Required = true - }; - Option apiUrlOption = new("--apiUrl") - { - Description = "The root path of the Unity editor API documentation or the Unity package. " + - "{0} is replaced with the Unity editor short version (example: https://docs.unity3d.com/6000.0/Documentation/ScriptReference/) or " + - "the Unity package short version (example: https://docs.unity3d.com/Packages/com.unity.inputsystem@1.0/api/)}) in the case of a package.", - DefaultValueFactory = _ => Constants.DefaultUnityApiUrl - }; - Option docFxConfigurationFilePathOption = new("--docFxConfigurationFilePath") - { - Description = "The path to the DocFX configuration file.", - DefaultValueFactory = _ => Constants.DefaultDocFxConfigurationFilePath - }; - Option xrefMapsPathOption = new("--xrefMapsPath") - { - Description = $"The path where the final {Constants.DefaultXrefMapFileName} files will be generated. " + - $"{{0}} is replaced with the Unity editor version (example: {string.Format(Constants.DefaultXrefMapsPath, "6000.0.1f1")}) or " + - $"the Unity package version (example: {string.Format(Constants.DefaultXrefMapsPath, "1.0.0")}) in the case of a package.", - DefaultValueFactory = _ => Constants.DefaultXrefMapsPath - }; - - Options.Add(repositoryUrlOption); - Options.Add(repositoryBranchOption); - Options.Add(repositoryPathOption); - Options.Add(repositoryTagsOption); - Options.Add(apiUrlOption); - Options.Add(docFxConfigurationFilePathOption); - Options.Add(xrefMapsPathOption); - - SetAction(async (parseResult, cancellationToken) => - { - bool result = true; - - string? repositoryUrl = parseResult.GetValue(repositoryUrlOption); - string? repositoryBranch = parseResult.GetValue(repositoryBranchOption); - string? repositoryPath = parseResult.GetValue(repositoryPathOption); - string[]? repositoryTags = parseResult.GetValue(repositoryTagsOption); - string? apiUrl = parseResult.GetValue(apiUrlOption); - string? docFxFilePath = parseResult.GetValue(docFxConfigurationFilePathOption); - string? xrefMapsPath = parseResult.GetValue(xrefMapsPathOption); - - using Stream docFxStream = File.OpenRead(docFxFilePath!); - - DocFxConfiguration? docFxConfiguration = await JsonSerializer.DeserializeAsync(docFxStream, cancellationToken: cancellationToken); - - string generatedDocsPath = docFxConfiguration!.Build!.Destination!; - string generatedXrefMapPath = Path.Combine(generatedDocsPath, Constants.DefaultXrefMapFileName!); - - logger.LogInformation("Sync the Unity repository in '{RepositoryPath}'", Path.GetFullPath(repositoryPath!)); - - using Repository repository = Git.GetSyncRepository(repositoryUrl!, repositoryPath!, repositoryBranch!, logger); - - foreach (string repositoryTag in repositoryTags!) - { - Match versionMatch = VersionRegex().Match(repositoryTag); - - string shortVersion = $"{versionMatch.Groups["majorVersion"].Value}.{versionMatch.Groups["minorVersion"].Value}"; - - logger.LogInformation("Generating Unity '{ShortVersion}' xref map", shortVersion); - - repository.HardReset(repositoryTag, logger); - - string xrefMapPath = string.Format(xrefMapsPath!, repositoryTag); // .//xrefmap.yml - - logger.LogInformation("Running DocFX on '{RepositoryTag}'", repositoryTag); - - await Utils.RunCommand( - "dotnet", $"docfx {docFxFilePath}", - value => - { - if (!string.IsNullOrEmpty(value)) - { - logger.LogInformation("{Message}", value); - } - }, - value => - { - if (!string.IsNullOrEmpty(value)) - { - logger.LogError("{Message}", value); - } - }, - cancellationToken); - - if (!File.Exists(generatedXrefMapPath)) - { - result = false; - - logger.LogError("Error: '{XrefMapFilePath}' for Unity '{RepositoryTag}' not generated", generatedXrefMapPath, repositoryTag); - - continue; - } - - logger.LogInformation("Fixing hrefs in '{XrefMapFilePath}'", Path.GetFullPath(xrefMapPath)); - - await Utils.CopyFile(generatedXrefMapPath, xrefMapPath, cancellationToken); - - XrefMap xrefMap = await XrefMap.Load(xrefMapPath, cancellationToken); - - xrefMap.FixHrefs(apiUrl: string.Format(apiUrl!, shortVersion)); - - await xrefMap.Save(xrefMapPath, cancellationToken); - } - - return result ? 0 : 1; - }); - } - - [GeneratedRegex(@"(?\d+)\.(?\d+)\.(?\d+)")] - private static partial Regex VersionRegex(); -} +using System.CommandLine; +using System.IO; +using System.Text.Json; +using System.Text.RegularExpressions; +using LibGit2Sharp; +using Microsoft.Extensions.Logging; +using UnityXrefMaps.DocFX; + +namespace UnityXrefMaps.Commands; + +internal sealed partial class BuildCommand : RootCommand +{ + public BuildCommand(ILogger logger) + { + Option repositoryUrlOption = new("--repositoryUrl") + { + Description = "The Git repository url.", + DefaultValueFactory = _ => Constants.DefaultUnityRepositoryUrl + }; + Option repositoryBranchOption = new("--repositoryBranch") + { + Description = "The Git repository branch.", + DefaultValueFactory = _ => Constants.DefaultUnityRepositoryBranch + }; + Option repositoryPathOption = new("--repositoryPath") + { + Description = "The Git repository path to git clone. " + + "If the clone has already been made, it will reset so that the clone can be reused.", + DefaultValueFactory = _ => Constants.DefaultUnityRepositoryPath + }; + Option repositoryTagsOption = new("--repositoryTags") + { + Description = "The repository tags to use to generate the documentation. " + + "That is, the versions of the Unity editor (6000.0.1f1, 6000.1.1f1) or the versions of the Unity package (1.0.0, 1.1.0).", + Required = true + }; + Option apiUrlOption = new("--apiUrl") + { + Description = "The root path of the Unity editor API documentation or the Unity package. " + + "{0} is replaced with the Unity editor short version (example: https://docs.unity3d.com/6000.0/Documentation/ScriptReference/) or " + + "the Unity package short version (example: https://docs.unity3d.com/Packages/com.unity.inputsystem@1.0/api/)}) in the case of a package.", + DefaultValueFactory = _ => Constants.DefaultUnityApiUrl + }; + Option docFxConfigurationFilePathOption = new("--docFxConfigurationFilePath") + { + Description = "The path to the DocFX configuration file.", + DefaultValueFactory = _ => Constants.DefaultDocFxConfigurationFilePath + }; + Option xrefMapsPathOption = new("--xrefMapsPath") + { + Description = $"The path where the final {Constants.DefaultXrefMapFileName} files will be generated. " + + $"{{0}} is replaced with the Unity editor version (example: {string.Format(Constants.DefaultXrefMapsPath, "6000.0.1f1")}) or " + + $"the Unity package version (example: {string.Format(Constants.DefaultXrefMapsPath, "1.0.0")}) in the case of a package.", + DefaultValueFactory = _ => Constants.DefaultXrefMapsPath + }; + + Options.Add(repositoryUrlOption); + Options.Add(repositoryBranchOption); + Options.Add(repositoryPathOption); + Options.Add(repositoryTagsOption); + Options.Add(apiUrlOption); + Options.Add(docFxConfigurationFilePathOption); + Options.Add(xrefMapsPathOption); + + SetAction(async (parseResult, cancellationToken) => + { + bool result = true; + + string? repositoryUrl = parseResult.GetValue(repositoryUrlOption); + string? repositoryBranch = parseResult.GetValue(repositoryBranchOption); + string? repositoryPath = parseResult.GetValue(repositoryPathOption); + string[]? repositoryTags = parseResult.GetValue(repositoryTagsOption); + string? apiUrl = parseResult.GetValue(apiUrlOption); + string? docFxFilePath = parseResult.GetValue(docFxConfigurationFilePathOption); + string? xrefMapsPath = parseResult.GetValue(xrefMapsPathOption); + + using Stream docFxStream = File.OpenRead(docFxFilePath!); + + DocFxConfiguration? docFxConfiguration = await JsonSerializer.DeserializeAsync(docFxStream, cancellationToken: cancellationToken); + + string generatedDocsPath = docFxConfiguration!.Build!.Destination!; + string generatedXrefMapPath = Path.Combine(generatedDocsPath, Constants.DefaultXrefMapFileName!); + + logger.LogInformation("Sync the Unity repository in '{RepositoryPath}'", Path.GetFullPath(repositoryPath!)); + + using Repository repository = Git.GetSyncRepository(repositoryUrl!, repositoryPath!, repositoryBranch!, logger); + + foreach (string repositoryTag in repositoryTags!) + { + Match versionMatch = VersionRegex().Match(repositoryTag); + + string shortVersion = $"{versionMatch.Groups["majorVersion"].Value}.{versionMatch.Groups["minorVersion"].Value}"; + + logger.LogInformation("Generating Unity '{ShortVersion}' xref map", shortVersion); + + repository.HardReset(repositoryTag, logger); + + string xrefMapPath = string.Format(xrefMapsPath!, repositoryTag); // .//xrefmap.yml + + logger.LogInformation("Running DocFX on '{RepositoryTag}'", repositoryTag); + + await Utils.RunCommand( + "dotnet", $"docfx {docFxFilePath}", + value => + { + if (!string.IsNullOrEmpty(value)) + { + logger.LogInformation("{Message}", value); + } + }, + value => + { + if (!string.IsNullOrEmpty(value)) + { + logger.LogError("{Message}", value); + } + }, + cancellationToken); + + if (!File.Exists(generatedXrefMapPath)) + { + result = false; + + logger.LogError("Error: '{XrefMapFilePath}' for Unity '{RepositoryTag}' not generated", generatedXrefMapPath, repositoryTag); + + continue; + } + + logger.LogInformation("Fixing hrefs in '{XrefMapFilePath}'", Path.GetFullPath(xrefMapPath)); + + await Utils.CopyFile(generatedXrefMapPath, xrefMapPath, cancellationToken); + + XrefMap xrefMap = await XrefMap.Load(xrefMapPath, cancellationToken); + + xrefMap.FixHrefs(apiUrl: string.Format(apiUrl!, shortVersion)); + + await xrefMap.Save(xrefMapPath, cancellationToken); + } + + return result ? 0 : 1; + }); + } + + [GeneratedRegex(@"(?\d+)\.(?\d+)\.(?\d+)")] + private static partial Regex VersionRegex(); +} diff --git a/UnityXrefMaps/Commands/TestCommand.cs b/UnityXrefMaps/Commands/TestCommand.cs index 958374d..0ffe91d 100644 --- a/UnityXrefMaps/Commands/TestCommand.cs +++ b/UnityXrefMaps/Commands/TestCommand.cs @@ -1,39 +1,39 @@ -using System.CommandLine; -using Microsoft.Extensions.Logging; - -namespace UnityXrefMaps.Commands; - -internal sealed class TestCommand : Command -{ - public TestCommand(ILogger logger) : base("test", $"Check that the links in the {Constants.DefaultXrefMapFileName} file are valid.") - { - Option xrefPathOption = new("--xrefPath") - { - Description = $"The path to the {Constants.DefaultXrefMapFileName} file.", - Required = true - }; - - Options.Add(xrefPathOption); - - SetAction(async (parseResult, cancellationToken) => - { - bool result = true; - - string? xrefPath = parseResult.GetValue(xrefPathOption); - - XrefMap xrefMap = await XrefMap.Load(xrefPath!, cancellationToken); - - foreach (XrefMapReference reference in xrefMap.References!) - { - if (!await Utils.TestUriExists(reference.Href, logger, cancellationToken)) - { - result = false; - - logger.LogWarning("Invalid URL {Href} for {Uid} uid", reference.Href, reference.Uid); - } - } - - return result ? 0 : 1; - }); - } -} +using System.CommandLine; +using Microsoft.Extensions.Logging; + +namespace UnityXrefMaps.Commands; + +internal sealed class TestCommand : Command +{ + public TestCommand(ILogger logger) : base("test", $"Check that the links in the {Constants.DefaultXrefMapFileName} file are valid.") + { + Option xrefPathOption = new("--xrefPath") + { + Description = $"The path to the {Constants.DefaultXrefMapFileName} file.", + Required = true + }; + + Options.Add(xrefPathOption); + + SetAction(async (parseResult, cancellationToken) => + { + bool result = true; + + string? xrefPath = parseResult.GetValue(xrefPathOption); + + XrefMap xrefMap = await XrefMap.Load(xrefPath!, cancellationToken); + + foreach (XrefMapReference reference in xrefMap.References!) + { + if (!await Utils.TestUriExists(reference.Href, logger, cancellationToken)) + { + result = false; + + logger.LogWarning("Invalid URL {Href} for {Uid} uid", reference.Href, reference.Uid); + } + } + + return result ? 0 : 1; + }); + } +} diff --git a/UnityXrefMaps/Constants.cs b/UnityXrefMaps/Constants.cs index da11fe6..d6af748 100644 --- a/UnityXrefMaps/Constants.cs +++ b/UnityXrefMaps/Constants.cs @@ -1,40 +1,40 @@ -namespace UnityXrefMaps; - -internal static class Constants -{ - /// - /// Gets the default URL of the online API documentation of Unity. - /// - public const string DefaultUnityApiUrl = "https://docs.unity3d.com/{0}/Documentation/ScriptReference/"; - - /// - /// The default path of the Unity repository. - /// - public const string DefaultUnityRepositoryPath = "UnityCsReference"; - - /// - /// The default URL of the Unity repository. - /// - public const string DefaultUnityRepositoryUrl = "https://github.com/Unity-Technologies/UnityCsReference.git"; - - /// - /// The default branch of the Unity repository. - /// - public const string DefaultUnityRepositoryBranch = "master"; - - // https://github.com/dotnet/docfx/blob/1c4e9ff4a2d236206eee04066847a98343c6a3f7/src/Docfx.Build/XRefMaps/XRefArchive.cs#L14 - /// - /// The default xref map filename. - /// - public const string DefaultXrefMapFileName = "xrefmap.yml"; - - /// - /// The default path where to copy the xref maps. - /// - public const string DefaultXrefMapsPath = $"_site/{{0}}/{DefaultXrefMapFileName}"; - - /// - /// The default DocFX config file path. - /// - public const string DefaultDocFxConfigurationFilePath = "docfx.json"; -} +namespace UnityXrefMaps; + +internal static class Constants +{ + /// + /// Gets the default URL of the online API documentation of Unity. + /// + public const string DefaultUnityApiUrl = "https://docs.unity3d.com/{0}/Documentation/ScriptReference/"; + + /// + /// The default path of the Unity repository. + /// + public const string DefaultUnityRepositoryPath = "UnityCsReference"; + + /// + /// The default URL of the Unity repository. + /// + public const string DefaultUnityRepositoryUrl = "https://github.com/Unity-Technologies/UnityCsReference.git"; + + /// + /// The default branch of the Unity repository. + /// + public const string DefaultUnityRepositoryBranch = "master"; + + // https://github.com/dotnet/docfx/blob/1c4e9ff4a2d236206eee04066847a98343c6a3f7/src/Docfx.Build/XRefMaps/XRefArchive.cs#L14 + /// + /// The default xref map filename. + /// + public const string DefaultXrefMapFileName = "xrefmap.yml"; + + /// + /// The default path where to copy the xref maps. + /// + public const string DefaultXrefMapsPath = $"_site/{{0}}/{DefaultXrefMapFileName}"; + + /// + /// The default DocFX config file path. + /// + public const string DefaultDocFxConfigurationFilePath = "docfx.json"; +} diff --git a/UnityXrefMaps/DocFX/DocFxConfiguration.cs b/UnityXrefMaps/DocFX/DocFxConfiguration.cs index ca957b6..8ce9db5 100644 --- a/UnityXrefMaps/DocFX/DocFxConfiguration.cs +++ b/UnityXrefMaps/DocFX/DocFxConfiguration.cs @@ -1,10 +1,10 @@ -using System.Text.Json.Serialization; - -namespace UnityXrefMaps.DocFX; - -// https://github.com/dotnet/docfx/blob/main/src/Docfx.App/Config/DocfxConfig.cs -public class DocFxConfiguration -{ - [JsonPropertyName("build")] - public DocFxConfigurationBuild? Build { get; set; } -} +using System.Text.Json.Serialization; + +namespace UnityXrefMaps.DocFX; + +// https://github.com/dotnet/docfx/blob/main/src/Docfx.App/Config/DocfxConfig.cs +public class DocFxConfiguration +{ + [JsonPropertyName("build")] + public DocFxConfigurationBuild? Build { get; set; } +} diff --git a/UnityXrefMaps/DocFX/DocFxConfigurationBuild.cs b/UnityXrefMaps/DocFX/DocFxConfigurationBuild.cs index 8895399..455e495 100644 --- a/UnityXrefMaps/DocFX/DocFxConfigurationBuild.cs +++ b/UnityXrefMaps/DocFX/DocFxConfigurationBuild.cs @@ -1,10 +1,10 @@ -using System.Text.Json.Serialization; - -namespace UnityXrefMaps.DocFX; - -// https://github.com/dotnet/docfx/blob/main/src/Docfx.App/Config/BuildJsonConfig.cs -public class DocFxConfigurationBuild -{ - [JsonPropertyName("dest")] - public string? Destination { get; set; } -} +using System.Text.Json.Serialization; + +namespace UnityXrefMaps.DocFX; + +// https://github.com/dotnet/docfx/blob/main/src/Docfx.App/Config/BuildJsonConfig.cs +public class DocFxConfigurationBuild +{ + [JsonPropertyName("dest")] + public string? Destination { get; set; } +} diff --git a/UnityXrefMaps/Program.cs b/UnityXrefMaps/Program.cs index 8f50a9f..d1d809a 100644 --- a/UnityXrefMaps/Program.cs +++ b/UnityXrefMaps/Program.cs @@ -1,13 +1,13 @@ -using System.CommandLine; -using Microsoft.Extensions.Logging; -using UnityXrefMaps.Commands; - -ILoggerFactory factory = LoggerFactory.Create(builder => -{ - builder.AddConsole(); -}); - -RootCommand rootCommand = new BuildCommand(factory.CreateLogger()); -rootCommand.Subcommands.Add(new TestCommand(factory.CreateLogger())); - -await rootCommand.Parse(args).InvokeAsync(); +using System.CommandLine; +using Microsoft.Extensions.Logging; +using UnityXrefMaps.Commands; + +ILoggerFactory factory = LoggerFactory.Create(builder => +{ + builder.AddConsole(); +}); + +RootCommand rootCommand = new BuildCommand(factory.CreateLogger()); +rootCommand.Subcommands.Add(new TestCommand(factory.CreateLogger())); + +await rootCommand.Parse(args).InvokeAsync(); diff --git a/UnityXrefMaps/RepositoryExtensions.cs b/UnityXrefMaps/RepositoryExtensions.cs index eb597ef..67a42ab 100644 --- a/UnityXrefMaps/RepositoryExtensions.cs +++ b/UnityXrefMaps/RepositoryExtensions.cs @@ -15,13 +15,13 @@ internal static class RepositoryExtensions /// The to hard reset to . /// The name of the commit where to reset . public static void HardReset(this Repository repository, string commit, ILogger logger) - { + { logger.LogInformation("Hard reset to {Commit}", commit); repository.Reset(ResetMode.Hard, commit); try - { + { logger.LogInformation($"Removing untracked files"); repository.RemoveUntrackedFiles(); diff --git a/UnityXrefMaps/Utils.cs b/UnityXrefMaps/Utils.cs index 39c5dc7..8b88a27 100644 --- a/UnityXrefMaps/Utils.cs +++ b/UnityXrefMaps/Utils.cs @@ -26,10 +26,10 @@ public static async Task CopyFile(string sourcePath, string destPath, Cancellati Directory.CreateDirectory(destDirectoryPath!); - using Stream source = File.OpenRead(sourcePath); - using Stream destination = File.Create(destPath); - - await source.CopyToAsync(destination, cancellationToken); + using Stream source = File.OpenRead(sourcePath); + using Stream destination = File.Create(destPath); + + await source.CopyToAsync(destination, cancellationToken); } /// @@ -69,7 +69,7 @@ public static async Task RunCommand(string command, string arguments, Action TestUriExists(string? uri, ILogger logger, CancellationToken cancellationToken = default) { try - { + { HttpResponseMessage response = await s_httpClient.SendAsync(new(HttpMethod.Head, uri), cancellationToken); if (!response.IsSuccessStatusCode && response.StatusCode != System.Net.HttpStatusCode.NotFound) diff --git a/UnityXrefMaps/XrefMap.cs b/UnityXrefMaps/XrefMap.cs index 784b563..1be0b71 100644 --- a/UnityXrefMaps/XrefMap.cs +++ b/UnityXrefMaps/XrefMap.cs @@ -13,11 +13,11 @@ namespace UnityXrefMaps; public sealed partial class XrefMap { private static readonly Deserializer s_deserializer = new(); - private static readonly Serializer s_serializer = new(); - + private static readonly Serializer s_serializer = new(); + [YamlMember(Alias = "sorted")] - public bool Sorted { get; set; } - + public bool Sorted { get; set; } + [YamlMember(Alias = "references")] public XrefMapReference[]? References { get; set; } diff --git a/UnityXrefMaps/XrefMapReference.cs b/UnityXrefMaps/XrefMapReference.cs index 2726aff..a4cc3f5 100644 --- a/UnityXrefMaps/XrefMapReference.cs +++ b/UnityXrefMaps/XrefMapReference.cs @@ -14,29 +14,29 @@ public sealed partial class XrefMapReference private static readonly string[] s_hrefNamespacesToTrim = ["UnityEditor", "UnityEngine"]; [YamlMember(Alias = "uid")] - public string? Uid { get; set; } - + public string? Uid { get; set; } + [YamlMember(Alias = "name")] public string? Name { get; set; } [YamlMember(Alias = "name.vb")] - public string? NameVb { get; set; } - + public string? NameVb { get; set; } + [YamlMember(Alias = "href")] - public string? Href { get; set; } - + public string? Href { get; set; } + [YamlMember(Alias = "commentId")] - public string? CommentId { get; set; } - + public string? CommentId { get; set; } + [YamlMember(Alias = "isSpec")] - public string? IsSpec { get; set; } - + public string? IsSpec { get; set; } + [YamlMember(Alias = "fullName")] public string? FullName { get; set; } [YamlMember(Alias = "fullName.vb")] - public string? FullNameVb { get; set; } - + public string? FullNameVb { get; set; } + [YamlMember(Alias = "nameWithType")] public string? NameWithType { get; set; } @@ -83,14 +83,14 @@ public void FixHref(string apiUrl) // Fix href of operator if (CommentId.StartsWith("M:") && CommentId.Contains(".op_")) - { - Href = Href.Replace(".op_", ".operator_"); - - Href = Href.Replace(".operator_Subtraction", ".operator_subtract"); - Href = Href.Replace(".operator_Multiply", ".operator_multiply"); - Href = Href.Replace(".operator_Division", ".operator_divide"); - Href = Href.Replace(".operator_Addition", ".operator_add"); - Href = Href.Replace(".operator_Equality", ".operator_eq"); + { + Href = Href.Replace(".op_", ".operator_"); + + Href = Href.Replace(".operator_Subtraction", ".operator_subtract"); + Href = Href.Replace(".operator_Multiply", ".operator_multiply"); + Href = Href.Replace(".operator_Division", ".operator_divide"); + Href = Href.Replace(".operator_Addition", ".operator_add"); + Href = Href.Replace(".operator_Equality", ".operator_eq"); Href = Href.Replace(".operator_Implicit~", ".operator_"); } From 7bfe07506f09fea027dce5af9e52b38930e7de4a Mon Sep 17 00:00:00 2001 From: bdovaz Date: Thu, 25 Sep 2025 21:34:22 +0200 Subject: [PATCH 05/29] wip --- .github/workflows/ci.yml | 2 +- .github/workflows/tool.yml | 9 ++++++++- UnityXrefMaps.Tests/CommandTests.cs | 14 ++++++++++++-- UnityXrefMaps/Properties/launchSettings.json | 2 +- UnityXrefMaps/UnityXrefMaps.csproj | 4 ++-- 5 files changed, 24 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7f75509..ab74a5b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,4 +19,4 @@ jobs: run: dotnet tool restore - name: Build & test (Release) - run: dotnet test -c Release --logger "console;verbosity=normal" + run: dotnet test -c Release diff --git a/.github/workflows/tool.yml b/.github/workflows/tool.yml index 3c57e34..bb18d7a 100644 --- a/.github/workflows/tool.yml +++ b/.github/workflows/tool.yml @@ -37,9 +37,16 @@ jobs: if: ${{ success() && env.UNITY_XREF_MAPS_DEPLOY == 1 }} run: dotnet pack UnityXrefMaps/UnityXrefMaps.csproj -c Release -p:PackageVersion=$UNITY_XREF_MAPS_VERSION -o ${{ runner.temp }} + - name: NuGet login + if: ${{ success() && env.UNITY_XREF_MAPS_DEPLOY == 1 }} + uses: NuGet/login@v1 + id: login + with: + user: ${{ secrets.NUGET_USER }} + - name: Publish (Release) if: ${{ success() && env.UNITY_XREF_MAPS_DEPLOY == 1 }} run: | dotnet nuget push ${{ runner.temp }}/UnityXrefMaps.${{ env.UNITY_XREF_MAPS_VERSION }}.nupkg \ --source https://api.nuget.org/v3/index.json \ - --api-key ${{ secrets.NUGET_API_KEY }} + --api-key ${{ steps.login.outputs.NUGET_API_KEY }} diff --git a/UnityXrefMaps.Tests/CommandTests.cs b/UnityXrefMaps.Tests/CommandTests.cs index 2723b23..4d837f3 100644 --- a/UnityXrefMaps.Tests/CommandTests.cs +++ b/UnityXrefMaps.Tests/CommandTests.cs @@ -120,7 +120,7 @@ await buildCommand IReadOnlyList logRecords = fakeLogCollector.GetSnapshot(); - Assert.Equal(2, logRecords.Count(l => l.Message.Equals("XRef map exported."))); + Assert.Equal(2, logRecords.Count(l => l.Message.Contains("XRef map exported.", StringComparison.OrdinalIgnoreCase))); foreach (string testedVersion in testedVersions) { @@ -139,9 +139,19 @@ await buildCommand { fakeLogCollector.Clear(); + string xrefFilePath = $"{xrefDirectoryPath}/{xrefDirectoryName}/{testedVersion}/{xrefFileName}"; + + XrefMap xrefMap = await XrefMap.Load(xrefFilePath, TestContext.Current.CancellationToken); + + Assert.Equal(3, xrefMap.References!.Length); + + Assert.Equal("UnityEngine", xrefMap.References[0].Uid); + Assert.Equal("UnityEngine.Vector2", xrefMap.References[1].Uid); + Assert.Equal("UnityEngine.Vector3", xrefMap.References[2].Uid); + string[] testArgs = [ "--xrefPath", - $"{xrefDirectoryPath}/{xrefDirectoryName}/{testedVersion}/{xrefFileName}" + xrefFilePath ]; Assert.Equal( diff --git a/UnityXrefMaps/Properties/launchSettings.json b/UnityXrefMaps/Properties/launchSettings.json index 261a301..43e9b4d 100644 --- a/UnityXrefMaps/Properties/launchSettings.json +++ b/UnityXrefMaps/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "DocFxForUnity": { "commandName": "Project", - "commandLineArgs": "--repositoryTags 6000.0.1f1 --repositoryTags 6000.1.1f1" + "commandLineArgs": "--repositoryTags 6000.0.1f1" } }, "$schema": "http://json.schemastore.org/launchsettings.json" diff --git a/UnityXrefMaps/UnityXrefMaps.csproj b/UnityXrefMaps/UnityXrefMaps.csproj index fd9fb89..66a129f 100644 --- a/UnityXrefMaps/UnityXrefMaps.csproj +++ b/UnityXrefMaps/UnityXrefMaps.csproj @@ -20,8 +20,8 @@ - - + + From 6162fb2ccac1d66aef2f0d6f67d6c29c501af6ba Mon Sep 17 00:00:00 2001 From: bdovaz Date: Thu, 25 Sep 2025 22:39:12 +0200 Subject: [PATCH 06/29] wip --- UnityXrefMaps.Tests/CommandTests.cs | 178 ++++++++++++++++--- UnityXrefMaps/Commands/BuildCommand.cs | 8 +- UnityXrefMaps/Properties/launchSettings.json | 2 +- UnityXrefMaps/XrefMap.cs | 4 +- UnityXrefMaps/XrefMapReference.cs | 11 +- UnityXrefMaps/docfx.json | 3 +- 6 files changed, 173 insertions(+), 33 deletions(-) diff --git a/UnityXrefMaps.Tests/CommandTests.cs b/UnityXrefMaps.Tests/CommandTests.cs index 4d837f3..cb91fcd 100644 --- a/UnityXrefMaps.Tests/CommandTests.cs +++ b/UnityXrefMaps.Tests/CommandTests.cs @@ -12,12 +12,12 @@ namespace UnityXrefMaps.Tests { - public class CommandTests : IAsyncDisposable + public class CommandTests : IAsyncLifetime, IAsyncDisposable { - private static readonly string repositoryDirectoryPath = Guid.NewGuid().ToString(); - private static readonly string xrefDirectoryPath = Guid.NewGuid().ToString(); - private static readonly string docFxFilePath = Guid.NewGuid().ToString() + "_config.json"; - private static readonly string docFxFilterFilePath = Guid.NewGuid().ToString() + "_filter_config.yml"; + private string? _repositoryDirectoryPath; + private string? _xrefDirectoryPath; + private string? _docFxFilePath; + private string? _docFxFilterFilePath; private class CustomStringWriter : StringWriter { @@ -46,14 +46,14 @@ public CommandTests(ITestOutputHelper output) } [Fact] - public async Task BuildTest_Success() + public async Task UnityEditor_BuildTest_Success() { string docFxFileContent = await File.ReadAllTextAsync("docfx.json", TestContext.Current.CancellationToken); - docFxFileContent = docFxFileContent.Replace("UnityCsReference/", repositoryDirectoryPath + '/'); - docFxFileContent = docFxFileContent.Replace("filterConfig.yml", docFxFilterFilePath); + docFxFileContent = docFxFileContent.Replace("UnityCsReference/", _repositoryDirectoryPath + '/'); + docFxFileContent = docFxFileContent.Replace("filterConfig.yml", _docFxFilterFilePath); - await File.WriteAllTextAsync(docFxFilePath, docFxFileContent, TestContext.Current.CancellationToken); + await File.WriteAllTextAsync(_docFxFilePath!, docFxFileContent, TestContext.Current.CancellationToken); string docFxFilterConfigContent = """ ### YamlMime:ManagedReference @@ -67,7 +67,7 @@ public async Task BuildTest_Success() uidRegex: .* """; - await File.WriteAllTextAsync(docFxFilterFilePath, docFxFilterConfigContent, TestContext.Current.CancellationToken); + await File.WriteAllTextAsync(_docFxFilterFilePath!, docFxFilterConfigContent, TestContext.Current.CancellationToken); var serviceCollection = new ServiceCollection(); @@ -101,11 +101,13 @@ public async Task BuildTest_Success() string[] buildArgs = [ "--repositoryPath", - repositoryDirectoryPath, + _repositoryDirectoryPath!, "--docFxConfigurationFilePath", - docFxFilePath, + _docFxFilePath!, "--xrefMapsPath", - $"{xrefDirectoryPath}/{xrefDirectoryName}/{{0}}/{xrefFileName}" + $"{_xrefDirectoryPath}/{xrefDirectoryName}/{{0}}/{xrefFileName}", + "--trimNamespaces", + "UnityEngine" ]; buildArgs = [.. buildArgs, .. testedVersions.SelectMany(v => new string[] { "--repositoryTags", v })]; @@ -124,7 +126,7 @@ await buildCommand foreach (string testedVersion in testedVersions) { - string xrefFilePath = $"{xrefDirectoryPath}/{xrefDirectoryName}/{testedVersion}/{xrefFileName}"; + string xrefFilePath = $"{_xrefDirectoryPath}/{xrefDirectoryName}/{testedVersion}/{xrefFileName}"; Assert.True(File.Exists(xrefFilePath)); @@ -139,7 +141,7 @@ await buildCommand { fakeLogCollector.Clear(); - string xrefFilePath = $"{xrefDirectoryPath}/{xrefDirectoryName}/{testedVersion}/{xrefFileName}"; + string xrefFilePath = $"{_xrefDirectoryPath}/{xrefDirectoryName}/{testedVersion}/{xrefFileName}"; XrefMap xrefMap = await XrefMap.Load(xrefFilePath, TestContext.Current.CancellationToken); @@ -166,6 +168,132 @@ await testCommand } } + [Fact] + public async Task UnityPackage_BuildTest_Success() + { + string docFxFileContent = await File.ReadAllTextAsync("docfx.json", TestContext.Current.CancellationToken); + + docFxFileContent = docFxFileContent.Replace("UnityCsReference/Projects/CSharp/*.csproj", "UnityCsReference/InputSystem/**/*.cs"); + docFxFileContent = docFxFileContent.Replace("UnityCsReference/", _repositoryDirectoryPath + '/'); + docFxFileContent = docFxFileContent.Replace("filterConfig.yml", _docFxFilterFilePath); + + await File.WriteAllTextAsync(_docFxFilePath!, docFxFileContent, TestContext.Current.CancellationToken); + + string docFxFilterConfigContent = """ +### YamlMime:ManagedReference +--- +apiRules: + - include: + uidRegex: ^UnityEngine\.InputSystem\.InputSystem$ + - include: + uidRegex: ^UnityEngine\.InputSystem\.InputActionAsset$ + - exclude: + uidRegex: .* +"""; + + await File.WriteAllTextAsync(_docFxFilterFilePath!, docFxFilterConfigContent, TestContext.Current.CancellationToken); + + var serviceCollection = new ServiceCollection(); + + serviceCollection.AddLogging(builder => + { + builder.SetMinimumLevel(LogLevel.Trace); + builder.AddFakeLogging(); + builder.Services.AddSingleton(new XUnitLoggerProvider(output, appendScope: false)); + }); + + await using ServiceProvider serviceProvider = serviceCollection.BuildServiceProvider(); + + FakeLogCollector fakeLogCollector = serviceProvider.GetFakeLogCollector(); + + ILoggerFactory loggerFactory = serviceProvider.GetRequiredService(); + + ILogger logger = loggerFactory.CreateLogger(); + + InvocationConfiguration invocationConfiguration = new() + { + Error = new CustomStringWriter(logger, LogLevel.Error), + Output = new CustomStringWriter(logger, LogLevel.Information), + }; + + string[] testedVersions = ["1.14.0", "1.14.2"]; + + BuildCommand buildCommand = new(loggerFactory.CreateLogger()); + + string xrefDirectoryName = "test"; + string xrefFileName = "test2.yml"; + + string[] buildArgs = [ + "--repositoryUrl", + "https://github.com/needle-mirror/com.unity.inputsystem.git", + "--repositoryPath", + _repositoryDirectoryPath!, + "--apiUrl", + "https://docs.unity3d.com/Packages/com.unity.inputsystem@{0}/api/", + "--docFxConfigurationFilePath", + _docFxFilePath!, + "--xrefMapsPath", + $"{_xrefDirectoryPath}/{xrefDirectoryName}/{{0}}/{xrefFileName}" + ]; + + buildArgs = [.. buildArgs, .. testedVersions.SelectMany(v => new string[] { "--repositoryTags", v })]; + + Assert.Equal( + 0, + await buildCommand + .Parse(buildArgs) + .InvokeAsync( + invocationConfiguration, + TestContext.Current.CancellationToken)); + + IReadOnlyList logRecords = fakeLogCollector.GetSnapshot(); + + Assert.Equal(2, logRecords.Count(l => l.Message.Contains("XRef map exported.", StringComparison.OrdinalIgnoreCase))); + + foreach (string testedVersion in testedVersions) + { + string xrefFilePath = $"{_xrefDirectoryPath}/{xrefDirectoryName}/{testedVersion}/{xrefFileName}"; + + Assert.True(File.Exists(xrefFilePath)); + + string xrefFileContent = await File.ReadAllTextAsync(xrefFilePath, TestContext.Current.CancellationToken); + + logger.LogInformation("{FilePath}:\n\n{FileContent}", xrefFilePath, xrefFileContent); + } + + TestCommand testCommand = new(loggerFactory.CreateLogger()); + + foreach (string testedVersion in testedVersions) + { + fakeLogCollector.Clear(); + + string xrefFilePath = $"{_xrefDirectoryPath}/{xrefDirectoryName}/{testedVersion}/{xrefFileName}"; + + XrefMap xrefMap = await XrefMap.Load(xrefFilePath, TestContext.Current.CancellationToken); + + Assert.Equal(3, xrefMap.References!.Length); + + Assert.Equal("UnityEngine.InputSystem", xrefMap.References[0].Uid); + Assert.Equal("UnityEngine.InputSystem.InputActionAsset", xrefMap.References[1].Uid); + Assert.Equal("UnityEngine.InputSystem.InputSystem", xrefMap.References[2].Uid); + + string[] testArgs = [ + "--xrefPath", + xrefFilePath + ]; + + Assert.Equal( + 0, + await testCommand + .Parse(testArgs) + .InvokeAsync( + invocationConfiguration, + TestContext.Current.CancellationToken)); + + Assert.Equal(0, fakeLogCollector.Count); + } + } + // https://stackoverflow.com/a/1702920 private static void DeleteDirectory(string targetDir) { @@ -186,6 +314,16 @@ private static void DeleteDirectory(string targetDir) } Directory.Delete(targetDir, false); + } + + public ValueTask InitializeAsync() + { + _repositoryDirectoryPath = Guid.NewGuid().ToString(); + _xrefDirectoryPath = Guid.NewGuid().ToString(); + _docFxFilePath = Guid.NewGuid().ToString() + "_config.json"; + _docFxFilterFilePath = Guid.NewGuid().ToString() + "_filter_config.yml"; + + return ValueTask.CompletedTask; } public async ValueTask DisposeAsync() @@ -197,14 +335,14 @@ public async ValueTask DisposeAsync() protected virtual async ValueTask DisposeAsyncCore() { - File.Delete(docFxFilePath); - File.Delete(docFxFilterFilePath); + File.Delete(_docFxFilePath!); + File.Delete(_docFxFilterFilePath!); const int maxRetries = 3; const int delay = 500; - await DeleteDirectoryWithRetries(repositoryDirectoryPath, maxRetries, delay); - await DeleteDirectoryWithRetries(xrefDirectoryPath, maxRetries, delay); + await DeleteDirectoryWithRetries(_repositoryDirectoryPath!, maxRetries, delay); + await DeleteDirectoryWithRetries(_xrefDirectoryPath!, maxRetries, delay); } private async Task DeleteDirectoryWithRetries(string directoryPath, int maxRetries, int delay) @@ -241,6 +379,6 @@ private async Task DeleteDirectoryWithRetries(string directoryPath, int maxRetri } } } - } + } } } diff --git a/UnityXrefMaps/Commands/BuildCommand.cs b/UnityXrefMaps/Commands/BuildCommand.cs index 77dff8b..0eea3fe 100644 --- a/UnityXrefMaps/Commands/BuildCommand.cs +++ b/UnityXrefMaps/Commands/BuildCommand.cs @@ -53,6 +53,10 @@ public BuildCommand(ILogger logger) $"the Unity package version (example: {string.Format(Constants.DefaultXrefMapsPath, "1.0.0")}) in the case of a package.", DefaultValueFactory = _ => Constants.DefaultXrefMapsPath }; + Option trimNamespacesOption = new("--trimNamespaces") + { + Description = "Namespaces for trimming." + }; Options.Add(repositoryUrlOption); Options.Add(repositoryBranchOption); @@ -61,6 +65,7 @@ public BuildCommand(ILogger logger) Options.Add(apiUrlOption); Options.Add(docFxConfigurationFilePathOption); Options.Add(xrefMapsPathOption); + Options.Add(trimNamespacesOption); SetAction(async (parseResult, cancellationToken) => { @@ -73,6 +78,7 @@ public BuildCommand(ILogger logger) string? apiUrl = parseResult.GetValue(apiUrlOption); string? docFxFilePath = parseResult.GetValue(docFxConfigurationFilePathOption); string? xrefMapsPath = parseResult.GetValue(xrefMapsPathOption); + string[]? trimNamespaces = parseResult.GetValue(trimNamespacesOption); using Stream docFxStream = File.OpenRead(docFxFilePath!); @@ -132,7 +138,7 @@ await Utils.RunCommand( XrefMap xrefMap = await XrefMap.Load(xrefMapPath, cancellationToken); - xrefMap.FixHrefs(apiUrl: string.Format(apiUrl!, shortVersion)); + xrefMap.FixHrefs(string.Format(apiUrl!, shortVersion), trimNamespaces!); await xrefMap.Save(xrefMapPath, cancellationToken); } diff --git a/UnityXrefMaps/Properties/launchSettings.json b/UnityXrefMaps/Properties/launchSettings.json index 43e9b4d..34c8bae 100644 --- a/UnityXrefMaps/Properties/launchSettings.json +++ b/UnityXrefMaps/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "DocFxForUnity": { "commandName": "Project", - "commandLineArgs": "--repositoryTags 6000.0.1f1" + "commandLineArgs": "--repositoryTags 6000.0.1f1 --trimNamespaces UnityEditor --trimNamespaces UnityEngine" } }, "$schema": "http://json.schemastore.org/launchsettings.json" diff --git a/UnityXrefMaps/XrefMap.cs b/UnityXrefMaps/XrefMap.cs index 1be0b71..a253920 100644 --- a/UnityXrefMaps/XrefMap.cs +++ b/UnityXrefMaps/XrefMap.cs @@ -40,7 +40,7 @@ public static async Task Load(string filePath, CancellationToken cancel /// Fix the of of this . /// /// The URL of the online API documentation of Unity. - public void FixHrefs(string apiUrl) + public void FixHrefs(string apiUrl, IEnumerable hrefNamespacesToTrim) { var fixedReferences = new List(); @@ -51,7 +51,7 @@ public void FixHrefs(string apiUrl) continue; } - reference.FixHref(apiUrl); + reference.FixHref(apiUrl, hrefNamespacesToTrim); fixedReferences.Add(reference); } diff --git a/UnityXrefMaps/XrefMapReference.cs b/UnityXrefMaps/XrefMapReference.cs index a4cc3f5..330f12d 100644 --- a/UnityXrefMaps/XrefMapReference.cs +++ b/UnityXrefMaps/XrefMapReference.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Text.RegularExpressions; using YamlDotNet.Serialization; @@ -8,11 +9,6 @@ namespace UnityXrefMaps; /// public sealed partial class XrefMapReference { - /// - /// The online API documentation of Unity doesn't show some namespaces. - /// - private static readonly string[] s_hrefNamespacesToTrim = ["UnityEditor", "UnityEngine"]; - [YamlMember(Alias = "uid")] public string? Uid { get; set; } @@ -53,7 +49,7 @@ public sealed partial class XrefMapReference /// Sets to link to the online API documentation of Unity. /// /// The URL of the online API documentation of Unity. - public void FixHref(string apiUrl) + public void FixHref(string apiUrl, IEnumerable hrefNamespacesToTrim) { // Namespaces point to documentation index if (CommentId!.StartsWith("N:")) @@ -64,8 +60,7 @@ public void FixHref(string apiUrl) { Href = Uid; - // Trim UnityEngine and UnityEditor namespaces from href - foreach (string hrefNamespaceToTrim in s_hrefNamespacesToTrim) + foreach (string hrefNamespaceToTrim in hrefNamespacesToTrim) { Href = Href!.Replace(hrefNamespaceToTrim + ".", string.Empty); } diff --git a/UnityXrefMaps/docfx.json b/UnityXrefMaps/docfx.json index 824a1e5..eefbc5e 100644 --- a/UnityXrefMaps/docfx.json +++ b/UnityXrefMaps/docfx.json @@ -8,7 +8,8 @@ ], "filter": "filterConfig.yml", "dest": "UnityCsReference/_api", - "disableGitFeatures": true + "disableGitFeatures": true, + "allowCompilationErrors": true } ], "build": { From df7aa64216d24754ee3d468bb3b7930478a4d43d Mon Sep 17 00:00:00 2001 From: bdovaz Date: Thu, 25 Sep 2025 22:46:47 +0200 Subject: [PATCH 07/29] wip --- UnityXrefMaps.Tests/CommandTests.cs | 66 ++++++++-------- .../UnityXrefMaps.Tests.csproj | 78 +++++++++---------- UnityXrefMaps/UnityXrefMaps.csproj | 14 ++-- 3 files changed, 79 insertions(+), 79 deletions(-) diff --git a/UnityXrefMaps.Tests/CommandTests.cs b/UnityXrefMaps.Tests/CommandTests.cs index cb91fcd..2101d20 100644 --- a/UnityXrefMaps.Tests/CommandTests.cs +++ b/UnityXrefMaps.Tests/CommandTests.cs @@ -141,14 +141,14 @@ await buildCommand { fakeLogCollector.Clear(); - string xrefFilePath = $"{_xrefDirectoryPath}/{xrefDirectoryName}/{testedVersion}/{xrefFileName}"; - - XrefMap xrefMap = await XrefMap.Load(xrefFilePath, TestContext.Current.CancellationToken); - - Assert.Equal(3, xrefMap.References!.Length); - - Assert.Equal("UnityEngine", xrefMap.References[0].Uid); - Assert.Equal("UnityEngine.Vector2", xrefMap.References[1].Uid); + string xrefFilePath = $"{_xrefDirectoryPath}/{xrefDirectoryName}/{testedVersion}/{xrefFileName}"; + + XrefMap xrefMap = await XrefMap.Load(xrefFilePath, TestContext.Current.CancellationToken); + + Assert.Equal(3, xrefMap.References!.Length); + + Assert.Equal("UnityEngine", xrefMap.References[0].Uid); + Assert.Equal("UnityEngine.Vector2", xrefMap.References[1].Uid); Assert.Equal("UnityEngine.Vector3", xrefMap.References[2].Uid); string[] testArgs = [ @@ -171,8 +171,8 @@ await testCommand [Fact] public async Task UnityPackage_BuildTest_Success() { - string docFxFileContent = await File.ReadAllTextAsync("docfx.json", TestContext.Current.CancellationToken); - + string docFxFileContent = await File.ReadAllTextAsync("docfx.json", TestContext.Current.CancellationToken); + docFxFileContent = docFxFileContent.Replace("UnityCsReference/Projects/CSharp/*.csproj", "UnityCsReference/InputSystem/**/*.cs"); docFxFileContent = docFxFileContent.Replace("UnityCsReference/", _repositoryDirectoryPath + '/'); docFxFileContent = docFxFileContent.Replace("filterConfig.yml", _docFxFilterFilePath); @@ -182,12 +182,12 @@ public async Task UnityPackage_BuildTest_Success() string docFxFilterConfigContent = """ ### YamlMime:ManagedReference --- -apiRules: - - include: - uidRegex: ^UnityEngine\.InputSystem\.InputSystem$ - - include: - uidRegex: ^UnityEngine\.InputSystem\.InputActionAsset$ - - exclude: +apiRules: + - include: + uidRegex: ^UnityEngine\.InputSystem\.InputSystem$ + - include: + uidRegex: ^UnityEngine\.InputSystem\.InputActionAsset$ + - exclude: uidRegex: .* """; @@ -267,15 +267,15 @@ await buildCommand { fakeLogCollector.Clear(); - string xrefFilePath = $"{_xrefDirectoryPath}/{xrefDirectoryName}/{testedVersion}/{xrefFileName}"; - - XrefMap xrefMap = await XrefMap.Load(xrefFilePath, TestContext.Current.CancellationToken); - - Assert.Equal(3, xrefMap.References!.Length); - - Assert.Equal("UnityEngine.InputSystem", xrefMap.References[0].Uid); + string xrefFilePath = $"{_xrefDirectoryPath}/{xrefDirectoryName}/{testedVersion}/{xrefFileName}"; + + XrefMap xrefMap = await XrefMap.Load(xrefFilePath, TestContext.Current.CancellationToken); + + Assert.Equal(3, xrefMap.References!.Length); + + Assert.Equal("UnityEngine.InputSystem", xrefMap.References[0].Uid); Assert.Equal("UnityEngine.InputSystem.InputActionAsset", xrefMap.References[1].Uid); - Assert.Equal("UnityEngine.InputSystem.InputSystem", xrefMap.References[2].Uid); + Assert.Equal("UnityEngine.InputSystem.InputSystem", xrefMap.References[2].Uid); string[] testArgs = [ "--xrefPath", @@ -314,16 +314,16 @@ private static void DeleteDirectory(string targetDir) } Directory.Delete(targetDir, false); - } - - public ValueTask InitializeAsync() - { - _repositoryDirectoryPath = Guid.NewGuid().ToString(); - _xrefDirectoryPath = Guid.NewGuid().ToString(); - _docFxFilePath = Guid.NewGuid().ToString() + "_config.json"; + } + + public ValueTask InitializeAsync() + { + _repositoryDirectoryPath = Guid.NewGuid().ToString(); + _xrefDirectoryPath = Guid.NewGuid().ToString(); + _docFxFilePath = Guid.NewGuid().ToString() + "_config.json"; _docFxFilterFilePath = Guid.NewGuid().ToString() + "_filter_config.yml"; - return ValueTask.CompletedTask; + return ValueTask.CompletedTask; } public async ValueTask DisposeAsync() @@ -379,6 +379,6 @@ private async Task DeleteDirectoryWithRetries(string directoryPath, int maxRetri } } } - } + } } } diff --git a/UnityXrefMaps.Tests/UnityXrefMaps.Tests.csproj b/UnityXrefMaps.Tests/UnityXrefMaps.Tests.csproj index 0367ae8..fc52459 100644 --- a/UnityXrefMaps.Tests/UnityXrefMaps.Tests.csproj +++ b/UnityXrefMaps.Tests/UnityXrefMaps.Tests.csproj @@ -1,39 +1,39 @@ - - - - Exe - net9.0 - enable - false - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - PreserveNewest - - - - - - - - - - - - + + + + Exe + net9.0 + enable + false + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + PreserveNewest + + + + + + + + + + + + diff --git a/UnityXrefMaps/UnityXrefMaps.csproj b/UnityXrefMaps/UnityXrefMaps.csproj index 66a129f..d116153 100644 --- a/UnityXrefMaps/UnityXrefMaps.csproj +++ b/UnityXrefMaps/UnityXrefMaps.csproj @@ -24,13 +24,13 @@ - - - PreserveNewest - - - PreserveNewest - + + + PreserveNewest + + + PreserveNewest + From d66ec32010942852c880006a327594d652ea6c8b Mon Sep 17 00:00:00 2001 From: bdovaz Date: Thu, 25 Sep 2025 23:23:42 +0200 Subject: [PATCH 08/29] wip --- .github/workflows/tool.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/tool.yml b/.github/workflows/tool.yml index bb18d7a..8b50fd5 100644 --- a/.github/workflows/tool.yml +++ b/.github/workflows/tool.yml @@ -13,6 +13,9 @@ jobs: build: runs-on: ubuntu-latest + permissions: + id-token: write + steps: - name: Checkout uses: actions/checkout@v5 From a639d7ec97e40dd402c351b6a574c09c586c9610 Mon Sep 17 00:00:00 2001 From: bdovaz Date: Fri, 26 Sep 2025 10:14:27 +0200 Subject: [PATCH 09/29] wip --- UnityXrefMaps/Commands/BuildCommand.cs | 15 ++++++++++++++- UnityXrefMaps/Properties/launchSettings.json | 2 +- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/UnityXrefMaps/Commands/BuildCommand.cs b/UnityXrefMaps/Commands/BuildCommand.cs index 0eea3fe..60ea671 100644 --- a/UnityXrefMaps/Commands/BuildCommand.cs +++ b/UnityXrefMaps/Commands/BuildCommand.cs @@ -46,6 +46,10 @@ public BuildCommand(ILogger logger) Description = "The path to the DocFX configuration file.", DefaultValueFactory = _ => Constants.DefaultDocFxConfigurationFilePath }; + Option docFxAdditionalArgumentsOption = new("--docFxAdditionalArguments") + { + Description = "Additional arguments for DocFX." + }; Option xrefMapsPathOption = new("--xrefMapsPath") { Description = $"The path where the final {Constants.DefaultXrefMapFileName} files will be generated. " + @@ -64,6 +68,7 @@ public BuildCommand(ILogger logger) Options.Add(repositoryTagsOption); Options.Add(apiUrlOption); Options.Add(docFxConfigurationFilePathOption); + Options.Add(docFxAdditionalArgumentsOption); Options.Add(xrefMapsPathOption); Options.Add(trimNamespacesOption); @@ -77,6 +82,7 @@ public BuildCommand(ILogger logger) string[]? repositoryTags = parseResult.GetValue(repositoryTagsOption); string? apiUrl = parseResult.GetValue(apiUrlOption); string? docFxFilePath = parseResult.GetValue(docFxConfigurationFilePathOption); + string? docFxAdditionalArguments = parseResult.GetValue(docFxAdditionalArgumentsOption); string? xrefMapsPath = parseResult.GetValue(xrefMapsPathOption); string[]? trimNamespaces = parseResult.GetValue(trimNamespacesOption); @@ -91,6 +97,13 @@ public BuildCommand(ILogger logger) using Repository repository = Git.GetSyncRepository(repositoryUrl!, repositoryPath!, repositoryBranch!, logger); + string docFxArguments = docFxFilePath!; + + if (!string.IsNullOrEmpty(docFxAdditionalArguments)) + { + docFxArguments += ' ' + docFxAdditionalArguments; + } + foreach (string repositoryTag in repositoryTags!) { Match versionMatch = VersionRegex().Match(repositoryTag); @@ -106,7 +119,7 @@ public BuildCommand(ILogger logger) logger.LogInformation("Running DocFX on '{RepositoryTag}'", repositoryTag); await Utils.RunCommand( - "dotnet", $"docfx {docFxFilePath}", + "docfx", docFxArguments, value => { if (!string.IsNullOrEmpty(value)) diff --git a/UnityXrefMaps/Properties/launchSettings.json b/UnityXrefMaps/Properties/launchSettings.json index 34c8bae..a7cb8ea 100644 --- a/UnityXrefMaps/Properties/launchSettings.json +++ b/UnityXrefMaps/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "DocFxForUnity": { "commandName": "Project", - "commandLineArgs": "--repositoryTags 6000.0.1f1 --trimNamespaces UnityEditor --trimNamespaces UnityEngine" + "commandLineArgs": "--repositoryTags 6000.0.1f1 --docFxAdditionalArguments \"--verbose\" --trimNamespaces UnityEditor --trimNamespaces UnityEngine" } }, "$schema": "http://json.schemastore.org/launchsettings.json" From acb9962bff8b5a3b643defd544bf495b14720d8d Mon Sep 17 00:00:00 2001 From: bdovaz Date: Fri, 26 Sep 2025 10:28:06 +0200 Subject: [PATCH 10/29] wip --- UnityXrefMaps/Commands/BuildCommand.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/UnityXrefMaps/Commands/BuildCommand.cs b/UnityXrefMaps/Commands/BuildCommand.cs index 60ea671..3270039 100644 --- a/UnityXrefMaps/Commands/BuildCommand.cs +++ b/UnityXrefMaps/Commands/BuildCommand.cs @@ -90,7 +90,9 @@ public BuildCommand(ILogger logger) DocFxConfiguration? docFxConfiguration = await JsonSerializer.DeserializeAsync(docFxStream, cancellationToken: cancellationToken); - string generatedDocsPath = docFxConfiguration!.Build!.Destination!; + string docFxFileDirectoryPath = Path.GetDirectoryName(docFxFilePath)!; + + string generatedDocsPath = Path.Combine(docFxFileDirectoryPath, docFxConfiguration!.Build!.Destination!); string generatedXrefMapPath = Path.Combine(generatedDocsPath, Constants.DefaultXrefMapFileName!); logger.LogInformation("Sync the Unity repository in '{RepositoryPath}'", Path.GetFullPath(repositoryPath!)); From 7f0ed794a9c8a2d4b0bc3a42d90ad87a106185c6 Mon Sep 17 00:00:00 2001 From: bdovaz Date: Fri, 26 Sep 2025 15:18:27 +0200 Subject: [PATCH 11/29] wip --- UnityXrefMaps/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UnityXrefMaps/Program.cs b/UnityXrefMaps/Program.cs index d1d809a..eddd818 100644 --- a/UnityXrefMaps/Program.cs +++ b/UnityXrefMaps/Program.cs @@ -10,4 +10,4 @@ RootCommand rootCommand = new BuildCommand(factory.CreateLogger()); rootCommand.Subcommands.Add(new TestCommand(factory.CreateLogger())); -await rootCommand.Parse(args).InvokeAsync(); +return await rootCommand.Parse(args).InvokeAsync(); From 9e5d56e97d48c9668273ddabb9554f11b621c764 Mon Sep 17 00:00:00 2001 From: bdovaz Date: Sat, 27 Sep 2025 16:55:25 +0200 Subject: [PATCH 12/29] fixes --- .config/dotnet-tools.json | 12 ------ .github/workflows/ci.yml | 2 +- .github/workflows/unity-xref-maps.yml | 2 +- README.md | 62 ++++++++++++++++++++++++--- 4 files changed, 58 insertions(+), 20 deletions(-) delete mode 100644 .config/dotnet-tools.json diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json deleted file mode 100644 index be28e25..0000000 --- a/.config/dotnet-tools.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "version": 1, - "isRoot": true, - "tools": { - "docfx": { - "version": "2.78.3", - "commands": [ - "docfx" - ] - } - } -} \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ab74a5b..71b8393 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: uses: actions/checkout@v5 - name: Install DocFX - run: dotnet tool restore + run: dotnet tool install --global docfx - name: Build & test (Release) run: dotnet test -c Release diff --git a/.github/workflows/unity-xref-maps.yml b/.github/workflows/unity-xref-maps.yml index 6f416f1..fc3c62e 100644 --- a/.github/workflows/unity-xref-maps.yml +++ b/.github/workflows/unity-xref-maps.yml @@ -18,7 +18,7 @@ jobs: uses: actions/checkout@v5 - name: Install DocFX - run: dotnet tool restore + run: dotnet tool install --global docfx - name: Restore run: dotnet restore diff --git a/README.md b/README.md index 13c105c..18ad0af 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Unity API references for DocFX +# Unity API references for DocFX [![Build Status](https://img.shields.io/github/actions/workflow/status/NormandErwan/UnityXrefMaps/ci.yml?branch=main)](https://github.com/NormandErwan/UnityXrefMaps/actions/workflows/ci.yml) [![NuGet Package](https://img.shields.io/nuget/v/UnityXrefMaps)](https://www.nuget.org/packages/UnityXrefMaps) > Automatically add clickable links to the Unity API on a DocFX documentation @@ -6,7 +6,7 @@ Generates references of the Unity API to use with DocFX (the [cross reference maps](https://dotnet.github.io/docfx/docs/links-and-cross-references.html#cross-reference-to-net-basic-class-library)). DocFX will set clickable all the references of the Unity API on your documentation. -## Usage +## Basic usage 1. Make sure you have setup a DocFX documentation. You can follow the [DocFxForUnity](https://github.com/NormandErwan/DocFxForUnity) instructions otherwise. @@ -35,7 +35,7 @@ DocFX will set clickable all the references of the Unity API on your documentati where `` is a Unity version in the form of `YYYY.x` (*e.g.* 2018.4, 2019.3, 2020.1). - - If you prefer relying in a offline file: + - If you prefer relying in a offline file: ```diff "build": { @@ -49,17 +49,67 @@ DocFX will set clickable all the references of the Unity API on your documentati 3. Generate your documentation! +## Advanced usage + +Install tool with: `dotnet tool install --global UnityXrefMaps`. + +### Generate documentation for a specific version of the Unity editor + +You need to provide at least: + +- A `docfx.json` file. You can base it on the [docfx.json in the repository](UnityXrefMaps/docfx.json). +- The version of the Unity editor. +- Trim `UnityEngine` and `UnityEditor` because Unity omits these namespaces in its documentation links. + +Example: + +```bash +unityxrefmaps \ + --repositoryPath xref/Unity/Repository \ + --repositoryTags 6000.0.1f1 \ + --trimNamespaces UnityEditor \ + --trimNamespaces UnityEngine +``` + +### Generate documentation for a specific version of a Unity package + +You need to provide at least: + +- A `docfx.json` file. You can base it on the [docfx.json in the repository](UnityXrefMaps/docfx.json). +- The package version. +- The API URL, which will be different for each package. + +Example: + +```bash +unityxrefmaps \ + --repositoryUrl "https://github.com/needle-mirror/com.unity.inputsystem.git" \ + --repositoryTags '1.0.0' \ + --apiUrl "https://docs.unity3d.com/Packages/com.unity.inputsystem@{0}/api/" +``` + +### Testing that links are generated correctly + +Providing an `xrefmap.yml` file will check each `href` element to see if it is a valid URL. + +Example: + +```bash +unityXrefMaps test \ + --xrefPath xrefmap.yml +``` + ## Contribute - To run this program: 1. Install Visual Studio 2022. 2. Install [.NET 9.0](https://dotnet.microsoft.com/download/dotnet) SDK. - 3. Clone this repository on your computer. - 4. Open a terminal on the cloned repository and run: + 3. Install [DocFX](https://www.nuget.org/packages/docfx). + 4. Clone this repository on your computer. + 5. Open a terminal on the cloned repository and run: ```sh - dotnet tool restore dotnet run ``` From b89a9d5093622a525754f7c16ce78af131c4321b Mon Sep 17 00:00:00 2001 From: Erwan Normand Date: Wed, 26 Nov 2025 18:42:55 +0100 Subject: [PATCH 13/29] Fix run command in README --- README.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 18ad0af..360e5ae 100644 --- a/README.md +++ b/README.md @@ -103,14 +103,13 @@ unityXrefMaps test \ - To run this program: - 1. Install Visual Studio 2022. - 2. Install [.NET 9.0](https://dotnet.microsoft.com/download/dotnet) SDK. - 3. Install [DocFX](https://www.nuget.org/packages/docfx). - 4. Clone this repository on your computer. - 5. Open a terminal on the cloned repository and run: + 1. Install [.NET 9.0](https://dotnet.microsoft.com/download/dotnet) SDK. + 2. Install [DocFX](https://www.nuget.org/packages/docfx). + 3. Clone this repository on your computer. + 4. Open a terminal on the cloned repository and run: ```sh - dotnet run + dotnet run --project UnityXrefMaps/UnityXrefMaps.csproj ``` - For any question or comment, please [open a new issue](https://github.com/NormandErwan/UnityXrefMaps/issues/new). From 88c6bf217ff752398c6c4f9aae37d6184f5492b9 Mon Sep 17 00:00:00 2001 From: Erwan Normand Date: Wed, 26 Nov 2025 19:04:58 +0100 Subject: [PATCH 14/29] Fix run command in README --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 360e5ae..4345fd6 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,8 @@ unityXrefMaps test \ 4. Open a terminal on the cloned repository and run: ```sh - dotnet run --project UnityXrefMaps/UnityXrefMaps.csproj + cd UnityXrefMaps + dotnet run ``` - For any question or comment, please [open a new issue](https://github.com/NormandErwan/UnityXrefMaps/issues/new). From da91f811b259fb0a082398476b7c3e306736a090 Mon Sep 17 00:00:00 2001 From: bdovaz Date: Fri, 28 Nov 2025 15:01:16 +0100 Subject: [PATCH 15/29] Update to .NET 10 --- UnityXrefMaps.Tests/CommandTests.cs | 49 +++++++++------ .../UnityXrefMaps.Tests.csproj | 12 ++-- UnityXrefMaps/Commands/BuildCommand.cs | 62 +++++++++++++------ UnityXrefMaps/Commands/TestCommand.cs | 5 +- UnityXrefMaps/UnityXrefMaps.csproj | 6 +- 5 files changed, 84 insertions(+), 50 deletions(-) diff --git a/UnityXrefMaps.Tests/CommandTests.cs b/UnityXrefMaps.Tests/CommandTests.cs index 2101d20..173b77a 100644 --- a/UnityXrefMaps.Tests/CommandTests.cs +++ b/UnityXrefMaps.Tests/CommandTests.cs @@ -21,28 +21,31 @@ public class CommandTests : IAsyncLifetime, IAsyncDisposable private class CustomStringWriter : StringWriter { - private readonly ILogger logger; - private readonly LogLevel logLevel; + private readonly ILogger _logger; + private readonly LogLevel _logLevel; public CustomStringWriter(ILogger logger, LogLevel logLevel) { - this.logger = logger; - this.logLevel = logLevel; + _logger = logger; + _logLevel = logLevel; } public override void Write(string? value) { base.Write(value); - logger.Log(logLevel, "{Message}", value); + if (_logger.IsEnabled(_logLevel)) + { + _logger.Log(_logLevel, "{Message}", value); + } } } - private readonly ITestOutputHelper output; + private readonly ITestOutputHelper _output; public CommandTests(ITestOutputHelper output) { - this.output = output; + _output = output; } [Fact] @@ -75,7 +78,7 @@ public async Task UnityEditor_BuildTest_Success() { builder.SetMinimumLevel(LogLevel.Trace); builder.AddFakeLogging(); - builder.Services.AddSingleton(new XUnitLoggerProvider(output, appendScope: false)); + builder.Services.AddSingleton(new XUnitLoggerProvider(_output, appendScope: false)); }); await using ServiceProvider serviceProvider = serviceCollection.BuildServiceProvider(); @@ -130,9 +133,12 @@ await buildCommand Assert.True(File.Exists(xrefFilePath)); - string xrefFileContent = await File.ReadAllTextAsync(xrefFilePath, TestContext.Current.CancellationToken); - - logger.LogInformation("{FilePath}:\n\n{FileContent}", xrefFilePath, xrefFileContent); + string xrefFileContent = await File.ReadAllTextAsync(xrefFilePath, TestContext.Current.CancellationToken); + + if (logger.IsEnabled(LogLevel.Information)) + { + logger.LogInformation("{FilePath}:\n\n{FileContent}", xrefFilePath, xrefFileContent); + } } TestCommand testCommand = new(loggerFactory.CreateLogger()); @@ -199,7 +205,7 @@ public async Task UnityPackage_BuildTest_Success() { builder.SetMinimumLevel(LogLevel.Trace); builder.AddFakeLogging(); - builder.Services.AddSingleton(new XUnitLoggerProvider(output, appendScope: false)); + builder.Services.AddSingleton(new XUnitLoggerProvider(_output, appendScope: false)); }); await using ServiceProvider serviceProvider = serviceCollection.BuildServiceProvider(); @@ -256,9 +262,12 @@ await buildCommand Assert.True(File.Exists(xrefFilePath)); - string xrefFileContent = await File.ReadAllTextAsync(xrefFilePath, TestContext.Current.CancellationToken); - - logger.LogInformation("{FilePath}:\n\n{FileContent}", xrefFilePath, xrefFileContent); + string xrefFileContent = await File.ReadAllTextAsync(xrefFilePath, TestContext.Current.CancellationToken); + + if (logger.IsEnabled(LogLevel.Information)) + { + logger.LogInformation("{FilePath}:\n\n{FileContent}", xrefFilePath, xrefFileContent); + } } TestCommand testCommand = new(loggerFactory.CreateLogger()); @@ -353,29 +362,29 @@ private async Task DeleteDirectoryWithRetries(string directoryPath, int maxRetri { try { - output.WriteLine($"Trying to delete directory: {directoryPath}"); + _output.WriteLine($"Trying to delete directory: {directoryPath}"); DeleteDirectory(directoryPath); - output.WriteLine($"Directory deleted: {directoryPath}"); + _output.WriteLine($"Directory deleted: {directoryPath}"); break; } catch (Exception e) { - output.WriteLine($"Error deleting directory: {directoryPath}. Error: {e}"); + _output.WriteLine($"Error deleting directory: {directoryPath}. Error: {e}"); retries++; if (retries < maxRetries) { - output.WriteLine($"Retrying in {delay}ms..."); + _output.WriteLine($"Retrying in {delay}ms..."); await Task.Delay(delay); } else { - output.WriteLine("Retry limit reached."); + _output.WriteLine("Retry limit reached."); } } } diff --git a/UnityXrefMaps.Tests/UnityXrefMaps.Tests.csproj b/UnityXrefMaps.Tests/UnityXrefMaps.Tests.csproj index fc52459..60c7bf2 100644 --- a/UnityXrefMaps.Tests/UnityXrefMaps.Tests.csproj +++ b/UnityXrefMaps.Tests/UnityXrefMaps.Tests.csproj @@ -2,7 +2,7 @@ Exe - net9.0 + net10.0 enable false @@ -12,11 +12,11 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/UnityXrefMaps/Commands/BuildCommand.cs b/UnityXrefMaps/Commands/BuildCommand.cs index 3270039..05a606e 100644 --- a/UnityXrefMaps/Commands/BuildCommand.cs +++ b/UnityXrefMaps/Commands/BuildCommand.cs @@ -5,6 +5,7 @@ using LibGit2Sharp; using Microsoft.Extensions.Logging; using UnityXrefMaps.DocFX; +using LogLevel = Microsoft.Extensions.Logging.LogLevel; namespace UnityXrefMaps.Commands; @@ -93,62 +94,83 @@ public BuildCommand(ILogger logger) string docFxFileDirectoryPath = Path.GetDirectoryName(docFxFilePath)!; string generatedDocsPath = Path.Combine(docFxFileDirectoryPath, docFxConfiguration!.Build!.Destination!); - string generatedXrefMapPath = Path.Combine(generatedDocsPath, Constants.DefaultXrefMapFileName!); - - logger.LogInformation("Sync the Unity repository in '{RepositoryPath}'", Path.GetFullPath(repositoryPath!)); + string generatedXrefMapPath = Path.Combine(generatedDocsPath, Constants.DefaultXrefMapFileName!); + + if (logger.IsEnabled(LogLevel.Information)) + { + logger.LogInformation("Sync the Unity repository in '{RepositoryPath}'", Path.GetFullPath(repositoryPath!)); + } using Repository repository = Git.GetSyncRepository(repositoryUrl!, repositoryPath!, repositoryBranch!, logger); string docFxArguments = docFxFilePath!; - if (!string.IsNullOrEmpty(docFxAdditionalArguments)) + if (!string.IsNullOrEmpty(docFxAdditionalArguments)) { - docFxArguments += ' ' + docFxAdditionalArguments; + docFxArguments += ' ' + docFxAdditionalArguments; } foreach (string repositoryTag in repositoryTags!) { Match versionMatch = VersionRegex().Match(repositoryTag); - string shortVersion = $"{versionMatch.Groups["majorVersion"].Value}.{versionMatch.Groups["minorVersion"].Value}"; - - logger.LogInformation("Generating Unity '{ShortVersion}' xref map", shortVersion); + string shortVersion = $"{versionMatch.Groups["majorVersion"].Value}.{versionMatch.Groups["minorVersion"].Value}"; + + if (logger.IsEnabled(LogLevel.Information)) + { + logger.LogInformation("Generating Unity '{ShortVersion}' xref map", shortVersion); + } repository.HardReset(repositoryTag, logger); - string xrefMapPath = string.Format(xrefMapsPath!, repositoryTag); // .//xrefmap.yml - - logger.LogInformation("Running DocFX on '{RepositoryTag}'", repositoryTag); + string xrefMapPath = string.Format(xrefMapsPath!, repositoryTag); // .//xrefmap.yml + + if (logger.IsEnabled(LogLevel.Information)) + { + logger.LogInformation("Running DocFX on '{RepositoryTag}'", repositoryTag); + } await Utils.RunCommand( "docfx", docFxArguments, value => { if (!string.IsNullOrEmpty(value)) - { - logger.LogInformation("{Message}", value); + { + if (logger.IsEnabled(LogLevel.Information)) + { + logger.LogInformation("{Message}", value); + } } }, value => { if (!string.IsNullOrEmpty(value)) - { - logger.LogError("{Message}", value); + { + if (logger.IsEnabled(LogLevel.Error)) + { + logger.LogError("{Message}", value); + } } }, cancellationToken); if (!File.Exists(generatedXrefMapPath)) { - result = false; - - logger.LogError("Error: '{XrefMapFilePath}' for Unity '{RepositoryTag}' not generated", generatedXrefMapPath, repositoryTag); + result = false; + + if (logger.IsEnabled(LogLevel.Error)) + { + logger.LogError("Error: '{XrefMapFilePath}' for Unity '{RepositoryTag}' not generated", generatedXrefMapPath, repositoryTag); + } continue; + } + + if (logger.IsEnabled(LogLevel.Information)) + { + logger.LogInformation("Fixing hrefs in '{XrefMapFilePath}'", Path.GetFullPath(xrefMapPath)); } - logger.LogInformation("Fixing hrefs in '{XrefMapFilePath}'", Path.GetFullPath(xrefMapPath)); - await Utils.CopyFile(generatedXrefMapPath, xrefMapPath, cancellationToken); XrefMap xrefMap = await XrefMap.Load(xrefMapPath, cancellationToken); diff --git a/UnityXrefMaps/Commands/TestCommand.cs b/UnityXrefMaps/Commands/TestCommand.cs index 0ffe91d..284b282 100644 --- a/UnityXrefMaps/Commands/TestCommand.cs +++ b/UnityXrefMaps/Commands/TestCommand.cs @@ -29,7 +29,10 @@ public TestCommand(ILogger logger) : base("test", $"Check that the { result = false; - logger.LogWarning("Invalid URL {Href} for {Uid} uid", reference.Href, reference.Uid); + if (logger.IsEnabled(LogLevel.Warning)) + { + logger.LogWarning("Invalid URL {Href} for {Uid} uid", reference.Href, reference.Uid); + } } } diff --git a/UnityXrefMaps/UnityXrefMaps.csproj b/UnityXrefMaps/UnityXrefMaps.csproj index d116153..f360c95 100644 --- a/UnityXrefMaps/UnityXrefMaps.csproj +++ b/UnityXrefMaps/UnityXrefMaps.csproj @@ -2,7 +2,7 @@ Exe - net9.0 + net10.0 enable true unityXrefMaps @@ -14,8 +14,8 @@ - - + + From 7e17a1a40c077bafc15fcb18f7b9b7b5f4130077 Mon Sep 17 00:00:00 2001 From: bdovaz Date: Fri, 28 Nov 2025 15:32:44 +0100 Subject: [PATCH 16/29] wip --- .editorconfig | 402 +++++++++--------- .github/workflows/ci.yml | 44 +- UnityXrefMaps.Tests/CommandTests.cs | 28 +- .../UnityXrefMaps.Tests.csproj | 2 +- UnityXrefMaps.sln | 62 +-- UnityXrefMaps/AssemblyInfo.cs | 2 +- UnityXrefMaps/Commands/BuildCommand.cs | 68 +-- UnityXrefMaps/Commands/TestCommand.cs | 2 +- UnityXrefMaps/Constants.cs | 2 +- UnityXrefMaps/DocFX/DocFxConfiguration.cs | 2 +- .../DocFX/DocFxConfigurationBuild.cs | 2 +- UnityXrefMaps/Program.cs | 2 +- UnityXrefMaps/UnityXrefMaps.csproj | 2 +- 13 files changed, 310 insertions(+), 310 deletions(-) diff --git a/.editorconfig b/.editorconfig index 16e1a86..67fc5fe 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,201 +1,201 @@ -# editorconfig.org - -# top-most EditorConfig file -root = true - -# Default settings: -# A newline ending every file -# Use 4 spaces as indentation -[*] -insert_final_newline = true -indent_style = space -indent_size = 4 -trim_trailing_whitespace = true - -# Specify UTF-8 without byte-order mark -[*.{csproj,locproj,nativeproj,proj,resx,slnx,vbproj}] -charset = utf-8 - -# Generated code -[*{_AssemblyInfo.cs,.notsupported.cs,AsmOffsets.cs}] -generated_code = true - -# C# files -[*.cs] -# New line preferences -csharp_new_line_before_open_brace = all -csharp_new_line_before_else = true -csharp_new_line_before_catch = true -csharp_new_line_before_finally = true -csharp_new_line_before_members_in_object_initializers = true -csharp_new_line_before_members_in_anonymous_types = true -csharp_new_line_between_query_expression_clauses = true - -# Indentation preferences -csharp_indent_block_contents = true -csharp_indent_braces = false -csharp_indent_case_contents = true -csharp_indent_case_contents_when_block = false -csharp_indent_switch_labels = true -csharp_indent_labels = one_less_than_current - -# Modifier preferences -csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async:suggestion - -# avoid this. unless absolutely necessary -dotnet_style_qualification_for_field = false:suggestion -dotnet_style_qualification_for_property = false:suggestion -dotnet_style_qualification_for_method = false:suggestion -dotnet_style_qualification_for_event = false:suggestion - -# Types: use keywords instead of BCL types, and permit var only when the type is clear -csharp_style_var_for_built_in_types = false:suggestion -csharp_style_var_when_type_is_apparent = false:none -csharp_style_var_elsewhere = false:suggestion -dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion -dotnet_style_predefined_type_for_member_access = true:suggestion - -# name all constant fields using PascalCase -dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion -dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields -dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style -dotnet_naming_symbols.constant_fields.applicable_kinds = field -dotnet_naming_symbols.constant_fields.required_modifiers = const -dotnet_naming_style.pascal_case_style.capitalization = pascal_case - -# static fields should have s_ prefix -dotnet_naming_rule.static_fields_should_have_prefix.severity = suggestion -dotnet_naming_rule.static_fields_should_have_prefix.symbols = static_fields -dotnet_naming_rule.static_fields_should_have_prefix.style = static_prefix_style -dotnet_naming_symbols.static_fields.applicable_kinds = field -dotnet_naming_symbols.static_fields.required_modifiers = static -dotnet_naming_symbols.static_fields.applicable_accessibilities = private, internal, private_protected -dotnet_naming_style.static_prefix_style.required_prefix = s_ -dotnet_naming_style.static_prefix_style.capitalization = camel_case - -# internal and private fields should be _camelCase -dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion -dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields -dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style -dotnet_naming_symbols.private_internal_fields.applicable_kinds = field -dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal -dotnet_naming_style.camel_case_underscore_style.required_prefix = _ -dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case - -# Code style defaults -csharp_using_directive_placement = outside_namespace:suggestion -dotnet_sort_system_directives_first = true -csharp_prefer_braces = true:silent -csharp_preserve_single_line_blocks = true:none -csharp_preserve_single_line_statements = false:none -csharp_prefer_static_local_function = true:suggestion -csharp_prefer_simple_using_statement = false:none -csharp_style_prefer_switch_expression = true:suggestion -dotnet_style_readonly_field = true:suggestion - -# Expression-level preferences -dotnet_style_object_initializer = true:suggestion -dotnet_style_collection_initializer = true:suggestion -dotnet_style_prefer_collection_expression = when_types_exactly_match -dotnet_style_explicit_tuple_names = true:suggestion -dotnet_style_coalesce_expression = true:suggestion -dotnet_style_null_propagation = true -dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion -dotnet_style_prefer_inferred_tuple_names = true:suggestion -dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion -dotnet_style_prefer_auto_properties = true:suggestion -dotnet_style_prefer_conditional_expression_over_assignment = true:silent -dotnet_style_prefer_conditional_expression_over_return = true:silent -csharp_prefer_simple_default_expression = true:suggestion - -# Expression-bodied members -csharp_style_expression_bodied_methods = true:silent -csharp_style_expression_bodied_constructors = true:silent -csharp_style_expression_bodied_operators = true:silent -csharp_style_expression_bodied_properties = true:silent -csharp_style_expression_bodied_indexers = true:silent -csharp_style_expression_bodied_accessors = true:silent -csharp_style_expression_bodied_lambdas = true:silent -csharp_style_expression_bodied_local_functions = true:silent - -# Pattern matching -csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion -csharp_style_pattern_matching_over_as_with_null_check = true:suggestion -csharp_style_inlined_variable_declaration = true:suggestion - -# Null checking preferences -csharp_style_throw_expression = true:suggestion -csharp_style_conditional_delegate_call = true:suggestion - -# Other features -csharp_style_prefer_index_operator = false:none -csharp_style_prefer_range_operator = false:none -csharp_style_pattern_local_over_anonymous_function = false:none - -# Space preferences -csharp_space_after_cast = false -csharp_space_after_colon_in_inheritance_clause = true -csharp_space_after_comma = true -csharp_space_after_dot = false -csharp_space_after_keywords_in_control_flow_statements = true -csharp_space_after_semicolon_in_for_statement = true -csharp_space_around_binary_operators = before_and_after -csharp_space_around_declaration_statements = do_not_ignore -csharp_space_before_colon_in_inheritance_clause = true -csharp_space_before_comma = false -csharp_space_before_dot = false -csharp_space_before_open_square_brackets = false -csharp_space_before_semicolon_in_for_statement = false -csharp_space_between_empty_square_brackets = false -csharp_space_between_method_call_empty_parameter_list_parentheses = false -csharp_space_between_method_call_name_and_opening_parenthesis = false -csharp_space_between_method_call_parameter_list_parentheses = false -csharp_space_between_method_declaration_empty_parameter_list_parentheses = false -csharp_space_between_method_declaration_name_and_open_parenthesis = false -csharp_space_between_method_declaration_parameter_list_parentheses = false -csharp_space_between_parentheses = false -csharp_space_between_square_brackets = false - -# License header -file_header_template = - -[src/libraries/System.Net.Http/src/System/Net/Http/{SocketsHttpHandler/Http3RequestStream.cs,BrowserHttpHandler/BrowserHttpHandler.cs}] -# disable CA2025, the analyzer throws a NullReferenceException when processing this file: https://github.com/dotnet/roslyn-analyzers/issues/7652 -dotnet_diagnostic.CA2025.severity = none - -# C++ Files -[*.{cpp,h,in}] -curly_bracket_next_line = true -indent_brace_style = Allman - -# Xml project files -[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}] -indent_size = 2 - -# Xml build files -[*.builds] -indent_size = 2 - -# Xml files -[*.{resx,ruleset,slnx,stylecop,xml}] -indent_size = 2 - -# Xml resource files -[*.resx] -# match Visual Studio behavior -insert_final_newline = false -trim_trailing_whitespace = false - -# Xml config files -[*.{props,targets,config,nuspec}] -indent_size = 2 - -# Data serialization -[*.{json,yaml,yml}] -indent_size = 2 - -# Shell scripts -[*.sh] -end_of_line = lf -[*.{cmd,bat}] -end_of_line = crlf +# editorconfig.org + +# top-most EditorConfig file +root = true + +# Default settings: +# A newline ending every file +# Use 4 spaces as indentation +[*] +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +# Specify UTF-8 without byte-order mark +[*.{csproj,locproj,nativeproj,proj,resx,slnx,vbproj}] +charset = utf-8 + +# Generated code +[*{_AssemblyInfo.cs,.notsupported.cs,AsmOffsets.cs}] +generated_code = true + +# C# files +[*.cs] +# New line preferences +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = false +csharp_indent_switch_labels = true +csharp_indent_labels = one_less_than_current + +# Modifier preferences +csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async:suggestion + +# avoid this. unless absolutely necessary +dotnet_style_qualification_for_field = false:suggestion +dotnet_style_qualification_for_property = false:suggestion +dotnet_style_qualification_for_method = false:suggestion +dotnet_style_qualification_for_event = false:suggestion + +# Types: use keywords instead of BCL types, and permit var only when the type is clear +csharp_style_var_for_built_in_types = false:suggestion +csharp_style_var_when_type_is_apparent = false:none +csharp_style_var_elsewhere = false:suggestion +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion + +# name all constant fields using PascalCase +dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style +dotnet_naming_symbols.constant_fields.applicable_kinds = field +dotnet_naming_symbols.constant_fields.required_modifiers = const +dotnet_naming_style.pascal_case_style.capitalization = pascal_case + +# static fields should have s_ prefix +dotnet_naming_rule.static_fields_should_have_prefix.severity = suggestion +dotnet_naming_rule.static_fields_should_have_prefix.symbols = static_fields +dotnet_naming_rule.static_fields_should_have_prefix.style = static_prefix_style +dotnet_naming_symbols.static_fields.applicable_kinds = field +dotnet_naming_symbols.static_fields.required_modifiers = static +dotnet_naming_symbols.static_fields.applicable_accessibilities = private, internal, private_protected +dotnet_naming_style.static_prefix_style.required_prefix = s_ +dotnet_naming_style.static_prefix_style.capitalization = camel_case + +# internal and private fields should be _camelCase +dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion +dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields +dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style +dotnet_naming_symbols.private_internal_fields.applicable_kinds = field +dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal +dotnet_naming_style.camel_case_underscore_style.required_prefix = _ +dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case + +# Code style defaults +csharp_using_directive_placement = outside_namespace:suggestion +dotnet_sort_system_directives_first = true +csharp_prefer_braces = true:silent +csharp_preserve_single_line_blocks = true:none +csharp_preserve_single_line_statements = false:none +csharp_prefer_static_local_function = true:suggestion +csharp_prefer_simple_using_statement = false:none +csharp_style_prefer_switch_expression = true:suggestion +dotnet_style_readonly_field = true:suggestion + +# Expression-level preferences +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_collection_expression = when_types_exactly_match +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_auto_properties = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +csharp_prefer_simple_default_expression = true:suggestion + +# Expression-bodied members +csharp_style_expression_bodied_methods = true:silent +csharp_style_expression_bodied_constructors = true:silent +csharp_style_expression_bodied_operators = true:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = true:silent + +# Pattern matching +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion + +# Null checking preferences +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion + +# Other features +csharp_style_prefer_index_operator = false:none +csharp_style_prefer_range_operator = false:none +csharp_style_pattern_local_over_anonymous_function = false:none + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = do_not_ignore +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# License header +file_header_template = + +[src/libraries/System.Net.Http/src/System/Net/Http/{SocketsHttpHandler/Http3RequestStream.cs,BrowserHttpHandler/BrowserHttpHandler.cs}] +# disable CA2025, the analyzer throws a NullReferenceException when processing this file: https://github.com/dotnet/roslyn-analyzers/issues/7652 +dotnet_diagnostic.CA2025.severity = none + +# C++ Files +[*.{cpp,h,in}] +curly_bracket_next_line = true +indent_brace_style = Allman + +# Xml project files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}] +indent_size = 2 + +# Xml build files +[*.builds] +indent_size = 2 + +# Xml files +[*.{resx,ruleset,slnx,stylecop,xml}] +indent_size = 2 + +# Xml resource files +[*.resx] +# match Visual Studio behavior +insert_final_newline = false +trim_trailing_whitespace = false + +# Xml config files +[*.{props,targets,config,nuspec}] +indent_size = 2 + +# Data serialization +[*.{json,yaml,yml}] +indent_size = 2 + +# Shell scripts +[*.sh] +end_of_line = lf +[*.{cmd,bat}] +end_of_line = crlf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 71b8393..34806eb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,22 +1,22 @@ ---- -name: ci - -on: - push: - branches: - - main - pull_request: - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v5 - - - name: Install DocFX - run: dotnet tool install --global docfx - - - name: Build & test (Release) - run: dotnet test -c Release +--- +name: ci + +on: + push: + branches: + - main + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Install DocFX + run: dotnet tool install --global docfx + + - name: Build & test (Release) + run: dotnet test -c Release diff --git a/UnityXrefMaps.Tests/CommandTests.cs b/UnityXrefMaps.Tests/CommandTests.cs index 173b77a..a99e6be 100644 --- a/UnityXrefMaps.Tests/CommandTests.cs +++ b/UnityXrefMaps.Tests/CommandTests.cs @@ -1,4 +1,4 @@ -using Meziantou.Extensions.Logging.Xunit.v3; +using Meziantou.Extensions.Logging.Xunit.v3; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; @@ -34,9 +34,9 @@ public override void Write(string? value) { base.Write(value); - if (_logger.IsEnabled(_logLevel)) - { - _logger.Log(_logLevel, "{Message}", value); + if (_logger.IsEnabled(_logLevel)) + { + _logger.Log(_logLevel, "{Message}", value); } } } @@ -133,11 +133,11 @@ await buildCommand Assert.True(File.Exists(xrefFilePath)); - string xrefFileContent = await File.ReadAllTextAsync(xrefFilePath, TestContext.Current.CancellationToken); - - if (logger.IsEnabled(LogLevel.Information)) - { - logger.LogInformation("{FilePath}:\n\n{FileContent}", xrefFilePath, xrefFileContent); + string xrefFileContent = await File.ReadAllTextAsync(xrefFilePath, TestContext.Current.CancellationToken); + + if (logger.IsEnabled(LogLevel.Information)) + { + logger.LogInformation("{FilePath}:\n\n{FileContent}", xrefFilePath, xrefFileContent); } } @@ -262,11 +262,11 @@ await buildCommand Assert.True(File.Exists(xrefFilePath)); - string xrefFileContent = await File.ReadAllTextAsync(xrefFilePath, TestContext.Current.CancellationToken); - - if (logger.IsEnabled(LogLevel.Information)) - { - logger.LogInformation("{FilePath}:\n\n{FileContent}", xrefFilePath, xrefFileContent); + string xrefFileContent = await File.ReadAllTextAsync(xrefFilePath, TestContext.Current.CancellationToken); + + if (logger.IsEnabled(LogLevel.Information)) + { + logger.LogInformation("{FilePath}:\n\n{FileContent}", xrefFilePath, xrefFileContent); } } diff --git a/UnityXrefMaps.Tests/UnityXrefMaps.Tests.csproj b/UnityXrefMaps.Tests/UnityXrefMaps.Tests.csproj index 60c7bf2..60a43ab 100644 --- a/UnityXrefMaps.Tests/UnityXrefMaps.Tests.csproj +++ b/UnityXrefMaps.Tests/UnityXrefMaps.Tests.csproj @@ -1,4 +1,4 @@ - + Exe diff --git a/UnityXrefMaps.sln b/UnityXrefMaps.sln index ae261f7..01599da 100644 --- a/UnityXrefMaps.sln +++ b/UnityXrefMaps.sln @@ -1,31 +1,31 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.14.36511.14 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnityXrefMaps", "UnityXrefMaps\UnityXrefMaps.csproj", "{97BE34CD-6988-1F05-D1B2-EBF906DAA098}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnityXrefMaps.Tests", "UnityXrefMaps.Tests\UnityXrefMaps.Tests.csproj", "{2A7A5BA2-A0E4-D775-C22B-0C5A6B68260B}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {97BE34CD-6988-1F05-D1B2-EBF906DAA098}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {97BE34CD-6988-1F05-D1B2-EBF906DAA098}.Debug|Any CPU.Build.0 = Debug|Any CPU - {97BE34CD-6988-1F05-D1B2-EBF906DAA098}.Release|Any CPU.ActiveCfg = Release|Any CPU - {97BE34CD-6988-1F05-D1B2-EBF906DAA098}.Release|Any CPU.Build.0 = Release|Any CPU - {2A7A5BA2-A0E4-D775-C22B-0C5A6B68260B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2A7A5BA2-A0E4-D775-C22B-0C5A6B68260B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2A7A5BA2-A0E4-D775-C22B-0C5A6B68260B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2A7A5BA2-A0E4-D775-C22B-0C5A6B68260B}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {FCDA8159-7FB5-4653-8EBE-A68917137BBC} - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36511.14 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnityXrefMaps", "UnityXrefMaps\UnityXrefMaps.csproj", "{97BE34CD-6988-1F05-D1B2-EBF906DAA098}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnityXrefMaps.Tests", "UnityXrefMaps.Tests\UnityXrefMaps.Tests.csproj", "{2A7A5BA2-A0E4-D775-C22B-0C5A6B68260B}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {97BE34CD-6988-1F05-D1B2-EBF906DAA098}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {97BE34CD-6988-1F05-D1B2-EBF906DAA098}.Debug|Any CPU.Build.0 = Debug|Any CPU + {97BE34CD-6988-1F05-D1B2-EBF906DAA098}.Release|Any CPU.ActiveCfg = Release|Any CPU + {97BE34CD-6988-1F05-D1B2-EBF906DAA098}.Release|Any CPU.Build.0 = Release|Any CPU + {2A7A5BA2-A0E4-D775-C22B-0C5A6B68260B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2A7A5BA2-A0E4-D775-C22B-0C5A6B68260B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2A7A5BA2-A0E4-D775-C22B-0C5A6B68260B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2A7A5BA2-A0E4-D775-C22B-0C5A6B68260B}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {FCDA8159-7FB5-4653-8EBE-A68917137BBC} + EndGlobalSection +EndGlobal diff --git a/UnityXrefMaps/AssemblyInfo.cs b/UnityXrefMaps/AssemblyInfo.cs index 08d26de..245b6bf 100644 --- a/UnityXrefMaps/AssemblyInfo.cs +++ b/UnityXrefMaps/AssemblyInfo.cs @@ -1,3 +1,3 @@ -using System.Runtime.CompilerServices; +using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("UnityXrefMaps.Tests")] diff --git a/UnityXrefMaps/Commands/BuildCommand.cs b/UnityXrefMaps/Commands/BuildCommand.cs index 05a606e..603aecf 100644 --- a/UnityXrefMaps/Commands/BuildCommand.cs +++ b/UnityXrefMaps/Commands/BuildCommand.cs @@ -1,4 +1,4 @@ -using System.CommandLine; +using System.CommandLine; using System.IO; using System.Text.Json; using System.Text.RegularExpressions; @@ -94,11 +94,11 @@ public BuildCommand(ILogger logger) string docFxFileDirectoryPath = Path.GetDirectoryName(docFxFilePath)!; string generatedDocsPath = Path.Combine(docFxFileDirectoryPath, docFxConfiguration!.Build!.Destination!); - string generatedXrefMapPath = Path.Combine(generatedDocsPath, Constants.DefaultXrefMapFileName!); - - if (logger.IsEnabled(LogLevel.Information)) - { - logger.LogInformation("Sync the Unity repository in '{RepositoryPath}'", Path.GetFullPath(repositoryPath!)); + string generatedXrefMapPath = Path.Combine(generatedDocsPath, Constants.DefaultXrefMapFileName!); + + if (logger.IsEnabled(LogLevel.Information)) + { + logger.LogInformation("Sync the Unity repository in '{RepositoryPath}'", Path.GetFullPath(repositoryPath!)); } using Repository repository = Git.GetSyncRepository(repositoryUrl!, repositoryPath!, repositoryBranch!, logger); @@ -114,20 +114,20 @@ public BuildCommand(ILogger logger) { Match versionMatch = VersionRegex().Match(repositoryTag); - string shortVersion = $"{versionMatch.Groups["majorVersion"].Value}.{versionMatch.Groups["minorVersion"].Value}"; - - if (logger.IsEnabled(LogLevel.Information)) - { - logger.LogInformation("Generating Unity '{ShortVersion}' xref map", shortVersion); + string shortVersion = $"{versionMatch.Groups["majorVersion"].Value}.{versionMatch.Groups["minorVersion"].Value}"; + + if (logger.IsEnabled(LogLevel.Information)) + { + logger.LogInformation("Generating Unity '{ShortVersion}' xref map", shortVersion); } repository.HardReset(repositoryTag, logger); - string xrefMapPath = string.Format(xrefMapsPath!, repositoryTag); // .//xrefmap.yml - - if (logger.IsEnabled(LogLevel.Information)) - { - logger.LogInformation("Running DocFX on '{RepositoryTag}'", repositoryTag); + string xrefMapPath = string.Format(xrefMapsPath!, repositoryTag); // .//xrefmap.yml + + if (logger.IsEnabled(LogLevel.Information)) + { + logger.LogInformation("Running DocFX on '{RepositoryTag}'", repositoryTag); } await Utils.RunCommand( @@ -135,20 +135,20 @@ await Utils.RunCommand( value => { if (!string.IsNullOrEmpty(value)) - { - if (logger.IsEnabled(LogLevel.Information)) - { - logger.LogInformation("{Message}", value); + { + if (logger.IsEnabled(LogLevel.Information)) + { + logger.LogInformation("{Message}", value); } } }, value => { if (!string.IsNullOrEmpty(value)) - { - if (logger.IsEnabled(LogLevel.Error)) - { - logger.LogError("{Message}", value); + { + if (logger.IsEnabled(LogLevel.Error)) + { + logger.LogError("{Message}", value); } } }, @@ -156,19 +156,19 @@ await Utils.RunCommand( if (!File.Exists(generatedXrefMapPath)) { - result = false; - - if (logger.IsEnabled(LogLevel.Error)) - { - logger.LogError("Error: '{XrefMapFilePath}' for Unity '{RepositoryTag}' not generated", generatedXrefMapPath, repositoryTag); + result = false; + + if (logger.IsEnabled(LogLevel.Error)) + { + logger.LogError("Error: '{XrefMapFilePath}' for Unity '{RepositoryTag}' not generated", generatedXrefMapPath, repositoryTag); } continue; - } - - if (logger.IsEnabled(LogLevel.Information)) - { - logger.LogInformation("Fixing hrefs in '{XrefMapFilePath}'", Path.GetFullPath(xrefMapPath)); + } + + if (logger.IsEnabled(LogLevel.Information)) + { + logger.LogInformation("Fixing hrefs in '{XrefMapFilePath}'", Path.GetFullPath(xrefMapPath)); } await Utils.CopyFile(generatedXrefMapPath, xrefMapPath, cancellationToken); diff --git a/UnityXrefMaps/Commands/TestCommand.cs b/UnityXrefMaps/Commands/TestCommand.cs index 284b282..a84843e 100644 --- a/UnityXrefMaps/Commands/TestCommand.cs +++ b/UnityXrefMaps/Commands/TestCommand.cs @@ -1,4 +1,4 @@ -using System.CommandLine; +using System.CommandLine; using Microsoft.Extensions.Logging; namespace UnityXrefMaps.Commands; diff --git a/UnityXrefMaps/Constants.cs b/UnityXrefMaps/Constants.cs index d6af748..d49f7b0 100644 --- a/UnityXrefMaps/Constants.cs +++ b/UnityXrefMaps/Constants.cs @@ -1,4 +1,4 @@ -namespace UnityXrefMaps; +namespace UnityXrefMaps; internal static class Constants { diff --git a/UnityXrefMaps/DocFX/DocFxConfiguration.cs b/UnityXrefMaps/DocFX/DocFxConfiguration.cs index 8ce9db5..20da869 100644 --- a/UnityXrefMaps/DocFX/DocFxConfiguration.cs +++ b/UnityXrefMaps/DocFX/DocFxConfiguration.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; namespace UnityXrefMaps.DocFX; diff --git a/UnityXrefMaps/DocFX/DocFxConfigurationBuild.cs b/UnityXrefMaps/DocFX/DocFxConfigurationBuild.cs index 455e495..20bbc90 100644 --- a/UnityXrefMaps/DocFX/DocFxConfigurationBuild.cs +++ b/UnityXrefMaps/DocFX/DocFxConfigurationBuild.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; namespace UnityXrefMaps.DocFX; diff --git a/UnityXrefMaps/Program.cs b/UnityXrefMaps/Program.cs index eddd818..e413f40 100644 --- a/UnityXrefMaps/Program.cs +++ b/UnityXrefMaps/Program.cs @@ -1,4 +1,4 @@ -using System.CommandLine; +using System.CommandLine; using Microsoft.Extensions.Logging; using UnityXrefMaps.Commands; diff --git a/UnityXrefMaps/UnityXrefMaps.csproj b/UnityXrefMaps/UnityXrefMaps.csproj index f360c95..16152ee 100644 --- a/UnityXrefMaps/UnityXrefMaps.csproj +++ b/UnityXrefMaps/UnityXrefMaps.csproj @@ -1,4 +1,4 @@ - + Exe From 38757a65b5e2ccf88fa846ebf788da3abba2f186 Mon Sep 17 00:00:00 2001 From: bdovaz Date: Sat, 29 Nov 2025 00:34:39 +0100 Subject: [PATCH 17/29] wip --- .github/workflows/resources/docfx.json | 31 +++ .github/workflows/resources/filterConfig.yml | 24 +++ .github/workflows/unity-xref-maps.yml | 189 ++++++++++++++++++- 3 files changed, 239 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/resources/docfx.json create mode 100644 .github/workflows/resources/filterConfig.yml diff --git a/.github/workflows/resources/docfx.json b/.github/workflows/resources/docfx.json new file mode 100644 index 0000000..51b3920 --- /dev/null +++ b/.github/workflows/resources/docfx.json @@ -0,0 +1,31 @@ +{ + "metadata": [ + { + "src": [ + { + "files": [ "UnityCsReference/Projects/CSharp/*.csproj" ] + } + ], + "filter": "filterConfig.yml", + "dest": "UnityCsReference/_api", + "disableGitFeatures": true, + "allowCompilationErrors": true, + "properties": { + "AllowUnsafeBlocks": "true" + } + } + ], + "build": { + "xref": [ + "https://learn.microsoft.com/en-us/dotnet/.xrefmap.json" + ], + "content": [ + { + "src": "UnityCsReference/_api", + "files": [ "*.yml" ], + "dest": "UnityCsReference/_api" + } + ], + "dest": "UnityCsReference/_site" + } +} diff --git a/.github/workflows/resources/filterConfig.yml b/.github/workflows/resources/filterConfig.yml new file mode 100644 index 0000000..b70045a --- /dev/null +++ b/.github/workflows/resources/filterConfig.yml @@ -0,0 +1,24 @@ +### YamlMime:ManagedReference +--- +apiRules: + - exclude: + uidRegex: ^AOT + - exclude: + uidRegex: ^JetBrains + - exclude: + uidRegex: ^TreeEditor + - exclude: + uidRegex: ^Unity\.CodeEditor + - exclude: + uidRegex: ^UnityEditorInternal + - exclude: + uidRegex: ^UnityEditor\.InspectorMode + - exclude: + uidRegex: ^UnityEngineInternal + - exclude: + uidRegex: ^UnityEngine.Internal + - exclude: + uidRegex: Finalize$ + - exclude: + hasAttribute: + uid: UnityEngine.Internal.ExcludeFromDocsAttribute diff --git a/.github/workflows/unity-xref-maps.yml b/.github/workflows/unity-xref-maps.yml index fc3c62e..c949f7d 100644 --- a/.github/workflows/unity-xref-maps.yml +++ b/.github/workflows/unity-xref-maps.yml @@ -10,9 +10,169 @@ on: - cron: '0 5 * * 0' # At 05:00 on Sunday jobs: - build: + get-unity-versions: + name: Get Unity Versions + + runs-on: ubuntu-latest + + outputs: + unityReleasesShortVersions: ${{ steps.set-unity-releases-versions.outputs.shortVersions }} + unityResolvedVersions: ${{ steps.resolve-versions.outputs.versions }} + + steps: + - name: Clone UnityCsReference repository + shell: pwsh + id: clone-unity-cs-reference-repository + run: | + $repositoryPath = '${{ runner.temp }}/UnityCsReference' + + git clone https://github.com/Unity-Technologies/UnityCsReference.git $repositoryPath + + Write-Output "repositoryPath=$repositoryPath" >> $env:GITHUB_OUTPUT + + - name: Get UnityCsReference repository tags + shell: pwsh + id: get-unity-repository-tags + run: | + $repositoryPath = '${{ steps.clone-unity-cs-reference-repository.outputs.repositoryPath }}' + + Push-Location $repositoryPath + + $sortedRepositoryTagObjects = git tag + | ForEach-Object { + $match = [regex]::Match($_, '(?(?(\d+)\.(\d+))\.(\d+))') + + @{ + Raw = $_ + Version = New-Object System.Version ($match.Groups['Version'].Value) + } + } + | Sort-Object -Property Version -Descending + + Write-Output "tags=$($sortedRepositoryTagObjects | Select-Object -ExpandProperty Raw | ConvertTo-Json -Compress)" >> $env:GITHUB_OUTPUT + + - name: Get Unity repository versions and filter + shell: pwsh + id: set-unity-repository-versions + run: | + $tagsString = '${{ steps.get-unity-repository-tags.outputs.tags }}' | ConvertFrom-Json + + $repositoryVersions = $tagsString + | ForEach-Object { + $match = [regex]::Match($_, '(?(?(\d+)\.(\d+))\.(\d+))') + + @{ + RawString = $_ + VersionString = $match.Groups['Version'].Value + ShortVersionString = $match.Groups['ShortVersion'].Value + Version = New-Object System.Version ($match.Groups['Version'].Value) + } + } + | Group-Object -Property ShortVersionString + | ForEach-Object { + [array] $sortedVersions = $_.Group | Sort-Object -Property Version -Descending + + @{ + ShortVersion = New-Object System.Version ($_.Name) + RawString = $sortedVersions[0].RawString + VersionString = $sortedVersions[0].VersionString + ShortVersionString = $sortedVersions[0].ShortVersionString + } + } + | Sort-Object -Property ShortVersion -Descending + | ForEach-Object { + @{ + Raw = $_.RawString + ShortVersion = $_.ShortVersionString + } + } + + Write-Output "shortVersions=$($repositoryVersions | Select-Object -ExpandProperty ShortVersion | ConvertTo-Json -Compress)" >> $env:GITHUB_OUTPUT + Write-Output "versions=$($repositoryVersions | ConvertTo-Json -Compress)" >> $env:GITHUB_OUTPUT + + - name: Get Unity releases versions and filter + shell: pwsh + id: set-unity-releases-versions + run: | + $url = 'https://services.api.unity.com/unity/editor/release/v1/releases?' + ` + 'architecture=X86_64&' + ` + 'platform=WINDOWS&' + ` + 'limit=25&' + ` + 'stream=LTS&' + ` + 'stream=SUPPORTED&' + ` + 'stream=TECH' + + $response = Invoke-RestMethod $url + + $versions = $response.results + | Select-Object -ExpandProperty version + | ForEach-Object { + $match = [regex]::Match($_, '(?(?(\d+)\.(\d+))\.(\d+))') + + @{ + RawString = $_ + VersionString = $match.Groups['Version'].Value + ShortVersionString = $match.Groups['ShortVersion'].Value + Version = New-Object System.Version ($match.Groups['Version'].Value) + } + } + | Group-Object -Property ShortVersionString + | ForEach-Object { + [array] $sortedVersions = $_.Group | Sort-Object -Property Version -Descending + + @{ + ShortVersion = New-Object System.Version ($_.Name) + RawString = $sortedVersions[0].RawString + VersionString = $sortedVersions[0].VersionString + ShortVersionString = $sortedVersions[0].ShortVersionString + } + } + | Sort-Object -Property ShortVersion -Descending + | ForEach-Object { + @{ + Raw = $_.RawString + ShortVersion = $_.ShortVersionString + } + } + + Write-Output "shortVersions=$($versions | Select-Object -ExpandProperty ShortVersion | ConvertTo-Json -Compress)" >> $env:GITHUB_OUTPUT + Write-Output "versions=$($versions | ConvertTo-Json -Compress)" >> $env:GITHUB_OUTPUT + + - name: Resolve versions + shell: pwsh + id: resolve-versions + run: | + $repositoryVersions = '${{ steps.set-unity-repository-versions.outputs.versions }}' | ConvertFrom-Json + $versions = '${{ steps.set-unity-releases-versions.outputs.versions }}' | ConvertFrom-Json + + $resolvedVersions = @() + + foreach ($version in $versions) + { + $repositoryVersion = $repositoryVersions | Where-Object { $_.ShortVersion -eq $version.ShortVersion } | Select-Object -First 1 + + Write-Host "Version $($version.Raw) (Unity Releases API) has been resolved with $($repositoryVersion.Raw) (UnityCsReference repository)." + + $resolvedVersions += $repositoryVersion + } + + Write-Host "The git tags in the UnityCsReference repository are not synchronized and always lag behind the releases." + + Write-Output "versions=$($resolvedVersions | ConvertTo-Json -Compress)" >> $env:GITHUB_OUTPUT + + generate-xref-maps: + name: Generate Unity Xref Maps + runs-on: ubuntu-latest + needs: get-unity-versions + + strategy: + fail-fast: false + + matrix: + shortVersion: ${{ fromJson(needs.get-unity-versions.outputs.unityReleasesShortVersions) }} + steps: - name: Checkout uses: actions/checkout@v5 @@ -28,13 +188,32 @@ jobs: run: dotnet build --no-restore --configuration Release --output ${{ runner.temp }}/UnityXrefMaps working-directory: UnityXrefMaps - - name: Run - run: ${{ runner.temp }}/UnityXrefMaps/UnityXrefMaps.dll - working-directory: UnityXrefMaps + - name: Generate Xref map + id: generate-xref-map + run: | + $versions = '${{ needs.get-unity-versions.outputs.unityResolvedVersions }}' | ConvertFrom-Json + + $version = $versions | Where-Object { $_.ShortVersion -eq '${{ matrix.shortVersion }}' } | Select-Object -First 1 + + if ($version -eq $null) + { + exit 1 + } + + dotnet UnityXrefMaps.dll ` + --repositoryPath '${{ runner.temp }}/UnityCsReference' ` + --repositoryTags "$($version.Raw)" ` + --docFxConfigurationFilePath "$($env:GITHUB_WORKSPACE)/.github/resources/docfx.json" ` + --trimNamespaces UnityEditor ` + --trimNamespaces UnityEngine ` + --xrefMapsPath "${{ runner.temp }}/_site/$($version.ShortVersion)/xrefmap.yml" + + Write-Output "shortVersion=$($version.ShortVersion)" >> $env:GITHUB_OUTPUT + working-directory: ${{ runner.temp }}/UnityXrefMaps - name: Deploy uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: _site + publish_dir: ${{ runner.temp }}/_site keep_files: true From f371c6f727018efe5c4343bf6016aa958c74bf6a Mon Sep 17 00:00:00 2001 From: bdovaz Date: Sat, 29 Nov 2025 00:36:25 +0100 Subject: [PATCH 18/29] wip --- .github/workflows/unity-xref-maps.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unity-xref-maps.yml b/.github/workflows/unity-xref-maps.yml index c949f7d..510d618 100644 --- a/.github/workflows/unity-xref-maps.yml +++ b/.github/workflows/unity-xref-maps.yml @@ -189,7 +189,7 @@ jobs: working-directory: UnityXrefMaps - name: Generate Xref map - id: generate-xref-map + shell: pwsh run: | $versions = '${{ needs.get-unity-versions.outputs.unityResolvedVersions }}' | ConvertFrom-Json From a61981c922c917e82cee3a40026be1f6b62f78f2 Mon Sep 17 00:00:00 2001 From: bdovaz Date: Sat, 29 Nov 2025 00:42:54 +0100 Subject: [PATCH 19/29] wip --- .github/workflows/unity-xref-maps.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unity-xref-maps.yml b/.github/workflows/unity-xref-maps.yml index 510d618..4a82a94 100644 --- a/.github/workflows/unity-xref-maps.yml +++ b/.github/workflows/unity-xref-maps.yml @@ -203,7 +203,7 @@ jobs: dotnet UnityXrefMaps.dll ` --repositoryPath '${{ runner.temp }}/UnityCsReference' ` --repositoryTags "$($version.Raw)" ` - --docFxConfigurationFilePath "$($env:GITHUB_WORKSPACE)/.github/resources/docfx.json" ` + --docFxConfigurationFilePath '${{ github.workspace }}/.github/workflows/resources/docfx.json' ` --trimNamespaces UnityEditor ` --trimNamespaces UnityEngine ` --xrefMapsPath "${{ runner.temp }}/_site/$($version.ShortVersion)/xrefmap.yml" From dff48a9f5c225f7647988213f59a36a91b13e822 Mon Sep 17 00:00:00 2001 From: bdovaz Date: Sat, 29 Nov 2025 00:48:43 +0100 Subject: [PATCH 20/29] wip --- .github/workflows/unity-xref-maps.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/unity-xref-maps.yml b/.github/workflows/unity-xref-maps.yml index 4a82a94..8c63d34 100644 --- a/.github/workflows/unity-xref-maps.yml +++ b/.github/workflows/unity-xref-maps.yml @@ -200,16 +200,15 @@ jobs: exit 1 } - dotnet UnityXrefMaps.dll ` + dotnet ${{ runner.temp }}/UnityXrefMaps/UnityXrefMaps.dll ` --repositoryPath '${{ runner.temp }}/UnityCsReference' ` --repositoryTags "$($version.Raw)" ` - --docFxConfigurationFilePath '${{ github.workspace }}/.github/workflows/resources/docfx.json' ` --trimNamespaces UnityEditor ` --trimNamespaces UnityEngine ` --xrefMapsPath "${{ runner.temp }}/_site/$($version.ShortVersion)/xrefmap.yml" Write-Output "shortVersion=$($version.ShortVersion)" >> $env:GITHUB_OUTPUT - working-directory: ${{ runner.temp }}/UnityXrefMaps + working-directory: ${{ github.workspace }}/.github/workflows/resources - name: Deploy uses: peaceiris/actions-gh-pages@v4 From f64e881d250b362269bdfad3ccacf67d22c967bf Mon Sep 17 00:00:00 2001 From: bdovaz Date: Sat, 29 Nov 2025 00:52:32 +0100 Subject: [PATCH 21/29] wip --- .github/workflows/unity-xref-maps.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/unity-xref-maps.yml b/.github/workflows/unity-xref-maps.yml index 8c63d34..f750205 100644 --- a/.github/workflows/unity-xref-maps.yml +++ b/.github/workflows/unity-xref-maps.yml @@ -200,15 +200,18 @@ jobs: exit 1 } + New-Item -Path ${{ runner.temp }}/Work -ItemType Directory + + Copy-Item ${{ github.workspace }}/.github/workflows/resources/* {{ runner.temp }}/Work + dotnet ${{ runner.temp }}/UnityXrefMaps/UnityXrefMaps.dll ` - --repositoryPath '${{ runner.temp }}/UnityCsReference' ` + --repositoryPath '${{ runner.temp }}/Work/UnityCsReference' ` --repositoryTags "$($version.Raw)" ` --trimNamespaces UnityEditor ` --trimNamespaces UnityEngine ` --xrefMapsPath "${{ runner.temp }}/_site/$($version.ShortVersion)/xrefmap.yml" Write-Output "shortVersion=$($version.ShortVersion)" >> $env:GITHUB_OUTPUT - working-directory: ${{ github.workspace }}/.github/workflows/resources - name: Deploy uses: peaceiris/actions-gh-pages@v4 From 64d3f07f1ce20bc91fb0b241d783fa41e583f1f9 Mon Sep 17 00:00:00 2001 From: bdovaz Date: Sat, 29 Nov 2025 00:53:55 +0100 Subject: [PATCH 22/29] wip --- .github/workflows/unity-xref-maps.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unity-xref-maps.yml b/.github/workflows/unity-xref-maps.yml index f750205..d978b28 100644 --- a/.github/workflows/unity-xref-maps.yml +++ b/.github/workflows/unity-xref-maps.yml @@ -202,7 +202,7 @@ jobs: New-Item -Path ${{ runner.temp }}/Work -ItemType Directory - Copy-Item ${{ github.workspace }}/.github/workflows/resources/* {{ runner.temp }}/Work + Copy-Item ${{ github.workspace }}/.github/workflows/resources/* ${{ runner.temp }}/Work dotnet ${{ runner.temp }}/UnityXrefMaps/UnityXrefMaps.dll ` --repositoryPath '${{ runner.temp }}/Work/UnityCsReference' ` From 4e0acb5e58a2f3f44c36bef5d7edd2ab5b665f8b Mon Sep 17 00:00:00 2001 From: bdovaz Date: Sat, 29 Nov 2025 00:55:42 +0100 Subject: [PATCH 23/29] wip --- .github/workflows/unity-xref-maps.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/unity-xref-maps.yml b/.github/workflows/unity-xref-maps.yml index d978b28..834d670 100644 --- a/.github/workflows/unity-xref-maps.yml +++ b/.github/workflows/unity-xref-maps.yml @@ -209,6 +209,7 @@ jobs: --repositoryTags "$($version.Raw)" ` --trimNamespaces UnityEditor ` --trimNamespaces UnityEngine ` + --docFxConfigurationFilePath '${{ runner.temp }}/Work/docfx.json' ` --xrefMapsPath "${{ runner.temp }}/_site/$($version.ShortVersion)/xrefmap.yml" Write-Output "shortVersion=$($version.ShortVersion)" >> $env:GITHUB_OUTPUT From 970226a4895c5aaf79ec020313a03a9feb2863a3 Mon Sep 17 00:00:00 2001 From: bdovaz Date: Sat, 29 Nov 2025 01:10:55 +0100 Subject: [PATCH 24/29] wip --- .github/workflows/unity-xref-maps.yml | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/.github/workflows/unity-xref-maps.yml b/.github/workflows/unity-xref-maps.yml index 834d670..f55b63a 100644 --- a/.github/workflows/unity-xref-maps.yml +++ b/.github/workflows/unity-xref-maps.yml @@ -212,11 +212,28 @@ jobs: --docFxConfigurationFilePath '${{ runner.temp }}/Work/docfx.json' ` --xrefMapsPath "${{ runner.temp }}/_site/$($version.ShortVersion)/xrefmap.yml" - Write-Output "shortVersion=$($version.ShortVersion)" >> $env:GITHUB_OUTPUT + - name: upload artifact + uses: actions/upload-artifact@v5 + with: + name: xref_${{ matrix.shortVersion }} + path: ${{ runner.temp }}/_site + + deploy: + name: Deploy + + runs-on: ubuntu-latest + + needs: generate-xref-maps + + steps: + - uses: actions/download-artifact@v6 + with: + pattern: xref_* + path: artifacts - name: Deploy - uses: peaceiris/actions-gh-pages@v4 + uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ${{ runner.temp }}/_site + publish_dir: ./artifacts keep_files: true From 17569561e2a9b319d7ded7468289391fd5cb2daf Mon Sep 17 00:00:00 2001 From: bdovaz Date: Sat, 29 Nov 2025 01:16:51 +0100 Subject: [PATCH 25/29] wip --- .github/workflows/unity-xref-maps.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/unity-xref-maps.yml b/.github/workflows/unity-xref-maps.yml index f55b63a..bf1eba5 100644 --- a/.github/workflows/unity-xref-maps.yml +++ b/.github/workflows/unity-xref-maps.yml @@ -167,6 +167,8 @@ jobs: needs: get-unity-versions + continue-on-error: true + strategy: fail-fast: false @@ -212,11 +214,12 @@ jobs: --docFxConfigurationFilePath '${{ runner.temp }}/Work/docfx.json' ` --xrefMapsPath "${{ runner.temp }}/_site/$($version.ShortVersion)/xrefmap.yml" - - name: upload artifact + - name: Upload artifact uses: actions/upload-artifact@v5 with: name: xref_${{ matrix.shortVersion }} path: ${{ runner.temp }}/_site + retention-days: 1 deploy: name: Deploy @@ -226,12 +229,13 @@ jobs: needs: generate-xref-maps steps: - - uses: actions/download-artifact@v6 + - name: Download artifacts + uses: actions/download-artifact@v6 with: pattern: xref_* path: artifacts - - name: Deploy + - name: Deploy pages uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} From 23a8ef3a2e870f87696b94cfb101f8197b829d43 Mon Sep 17 00:00:00 2001 From: bdovaz Date: Sat, 29 Nov 2025 01:25:12 +0100 Subject: [PATCH 26/29] wip --- .github/workflows/unity-xref-maps.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/unity-xref-maps.yml b/.github/workflows/unity-xref-maps.yml index bf1eba5..10797d3 100644 --- a/.github/workflows/unity-xref-maps.yml +++ b/.github/workflows/unity-xref-maps.yml @@ -234,6 +234,7 @@ jobs: with: pattern: xref_* path: artifacts + merge-multiple: true - name: Deploy pages uses: peaceiris/actions-gh-pages@v3 From e4c6a3e542c8fc2c93d5906dda97520d0fee69d9 Mon Sep 17 00:00:00 2001 From: bdovaz Date: Sat, 29 Nov 2025 01:38:04 +0100 Subject: [PATCH 27/29] wip --- .github/workflows/unity-xref-maps.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/unity-xref-maps.yml b/.github/workflows/unity-xref-maps.yml index 10797d3..0fd864f 100644 --- a/.github/workflows/unity-xref-maps.yml +++ b/.github/workflows/unity-xref-maps.yml @@ -233,12 +233,12 @@ jobs: uses: actions/download-artifact@v6 with: pattern: xref_* - path: artifacts + path: ${{ runner.temp }}/xref_artifacts merge-multiple: true - name: Deploy pages uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./artifacts + publish_dir: ${{ runner.temp }}/xref_artifacts keep_files: true From 4162c703ccb2c75827d0ea946ea79422853eb992 Mon Sep 17 00:00:00 2001 From: bdovaz Date: Thu, 4 Dec 2025 12:44:29 +0100 Subject: [PATCH 28/29] wip --- UnityXrefMaps.Tests/CommandTests.cs | 40 +++--- .../UnityXrefMaps.Tests.csproj | 4 +- UnityXrefMaps.Tests/XRefHrefFixerTests.cs | 76 +++++++++++ UnityXrefMaps.lutconfig | 6 + UnityXrefMaps/Commands/BuildCommand.cs | 26 +++- UnityXrefMaps/Commands/TestCommand.cs | 10 +- UnityXrefMaps/Constants.cs | 5 + UnityXrefMaps/Program.cs | 4 +- UnityXrefMaps/XRefHrefFixer.cs | 126 ++++++++++++++++++ UnityXrefMaps/XrefMap.cs | 61 +-------- UnityXrefMaps/XrefMapReference.cs | 70 +--------- UnityXrefMaps/XrefMapService.cs | 83 ++++++++++++ 12 files changed, 350 insertions(+), 161 deletions(-) create mode 100644 UnityXrefMaps.Tests/XRefHrefFixerTests.cs create mode 100644 UnityXrefMaps.lutconfig create mode 100644 UnityXrefMaps/XRefHrefFixer.cs create mode 100644 UnityXrefMaps/XrefMapService.cs diff --git a/UnityXrefMaps.Tests/CommandTests.cs b/UnityXrefMaps.Tests/CommandTests.cs index a99e6be..c80d59f 100644 --- a/UnityXrefMaps.Tests/CommandTests.cs +++ b/UnityXrefMaps.Tests/CommandTests.cs @@ -1,13 +1,13 @@ -using Meziantou.Extensions.Logging.Xunit.v3; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Testing; using System; using System.Collections.Generic; using System.CommandLine; using System.IO; using System.Linq; using System.Threading.Tasks; +using Meziantou.Extensions.Logging.Xunit.v3; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; using UnityXrefMaps.Commands; namespace UnityXrefMaps.Tests @@ -85,7 +85,9 @@ public async Task UnityEditor_BuildTest_Success() FakeLogCollector fakeLogCollector = serviceProvider.GetFakeLogCollector(); - ILoggerFactory loggerFactory = serviceProvider.GetRequiredService(); + ILoggerFactory loggerFactory = serviceProvider.GetRequiredService(); + + XrefMapService xrefMapService = new(loggerFactory.CreateLogger()); ILogger logger = loggerFactory.CreateLogger(); @@ -97,7 +99,7 @@ public async Task UnityEditor_BuildTest_Success() string[] testedVersions = ["6000.0.1f1", "6000.1.1f1"]; - BuildCommand buildCommand = new(loggerFactory.CreateLogger()); + BuildCommand buildCommand = new(loggerFactory); string xrefDirectoryName = "test"; string xrefFileName = "test2.yml"; @@ -141,7 +143,7 @@ await buildCommand } } - TestCommand testCommand = new(loggerFactory.CreateLogger()); + TestCommand testCommand = new(loggerFactory); foreach (string testedVersion in testedVersions) { @@ -149,7 +151,7 @@ await buildCommand string xrefFilePath = $"{_xrefDirectoryPath}/{xrefDirectoryName}/{testedVersion}/{xrefFileName}"; - XrefMap xrefMap = await XrefMap.Load(xrefFilePath, TestContext.Current.CancellationToken); + XrefMap xrefMap = await xrefMapService.Load(xrefFilePath, TestContext.Current.CancellationToken); Assert.Equal(3, xrefMap.References!.Length); @@ -190,9 +192,11 @@ public async Task UnityPackage_BuildTest_Success() --- apiRules: - include: - uidRegex: ^UnityEngine\.InputSystem\.InputSystem$ + uidRegex: ^UnityEngine\.InputSystem\.InputSystem + - include: + uidRegex: ^UnityEngine\.InputSystem\.InputAction - include: - uidRegex: ^UnityEngine\.InputSystem\.InputActionAsset$ + uidRegex: ^UnityEngine\.InputSystem\.InputActionAsset - exclude: uidRegex: .* """; @@ -212,7 +216,9 @@ public async Task UnityPackage_BuildTest_Success() FakeLogCollector fakeLogCollector = serviceProvider.GetFakeLogCollector(); - ILoggerFactory loggerFactory = serviceProvider.GetRequiredService(); + ILoggerFactory loggerFactory = serviceProvider.GetRequiredService(); + + XrefMapService xrefMapService = new(loggerFactory.CreateLogger()); ILogger logger = loggerFactory.CreateLogger(); @@ -224,7 +230,7 @@ public async Task UnityPackage_BuildTest_Success() string[] testedVersions = ["1.14.0", "1.14.2"]; - BuildCommand buildCommand = new(loggerFactory.CreateLogger()); + BuildCommand buildCommand = new(loggerFactory); string xrefDirectoryName = "test"; string xrefFileName = "test2.yml"; @@ -270,7 +276,7 @@ await buildCommand } } - TestCommand testCommand = new(loggerFactory.CreateLogger()); + TestCommand testCommand = new(loggerFactory); foreach (string testedVersion in testedVersions) { @@ -278,13 +284,9 @@ await buildCommand string xrefFilePath = $"{_xrefDirectoryPath}/{xrefDirectoryName}/{testedVersion}/{xrefFileName}"; - XrefMap xrefMap = await XrefMap.Load(xrefFilePath, TestContext.Current.CancellationToken); - - Assert.Equal(3, xrefMap.References!.Length); + XrefMap xrefMap = await xrefMapService.Load(xrefFilePath, TestContext.Current.CancellationToken); - Assert.Equal("UnityEngine.InputSystem", xrefMap.References[0].Uid); - Assert.Equal("UnityEngine.InputSystem.InputActionAsset", xrefMap.References[1].Uid); - Assert.Equal("UnityEngine.InputSystem.InputSystem", xrefMap.References[2].Uid); + Assert.NotEmpty(xrefMap.References!); string[] testArgs = [ "--xrefPath", diff --git a/UnityXrefMaps.Tests/UnityXrefMaps.Tests.csproj b/UnityXrefMaps.Tests/UnityXrefMaps.Tests.csproj index 60a43ab..62ef39c 100644 --- a/UnityXrefMaps.Tests/UnityXrefMaps.Tests.csproj +++ b/UnityXrefMaps.Tests/UnityXrefMaps.Tests.csproj @@ -12,10 +12,10 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/UnityXrefMaps.Tests/XRefHrefFixerTests.cs b/UnityXrefMaps.Tests/XRefHrefFixerTests.cs new file mode 100644 index 0000000..c6602a6 --- /dev/null +++ b/UnityXrefMaps.Tests/XRefHrefFixerTests.cs @@ -0,0 +1,76 @@ +using System; + +namespace UnityXrefMaps.Tests +{ + public class XRefHrefFixerTests + { + [Theory] + [InlineData( + "https://docs.unity3d.com/6000.0/Documentation/ScriptReference/", + "UnityEngine", + "N:UnityEngine", + "https://docs.unity3d.com/6000.0/Documentation/ScriptReference/index.html")] + [InlineData( + "https://docs.unity3d.com/6000.0/Documentation/ScriptReference/", + "GameObject", + "T:UnityEngine.GameObject", + "https://docs.unity3d.com/6000.0/Documentation/ScriptReference/GameObject.html")] + public void Unity_Fix(string apiUrl, string name, string commentId, string expected) + { + XrefMapReference xrefMapReference = new() + { + Name = name, + CommentId = commentId, + }; + + Assert.Equal(expected, XRefHrefFixer.Fix(apiUrl, xrefMapReference, ["UnityEngine"], false)); + } + + [Theory] + [InlineData( + "https://docs.unity3d.com/Packages/com.unity.inputsystem@1.17/api/", + "UnityEngine.InputSystem", + "N:UnityEngine.InputSystem", + "https://docs.unity3d.com/Packages/com.unity.inputsystem@1.17/api/UnityEngine.InputSystem.html")] + [InlineData( + "https://docs.unity3d.com/Packages/com.unity.inputsystem@1.17/api/", + "InputSystem", + "T:UnityEngine.InputSystem.InputSystem", + "https://docs.unity3d.com/Packages/com.unity.inputsystem@1.17/api/UnityEngine.InputSystem.InputSystem.html")] + [InlineData( + "https://docs.unity3d.com/Packages/com.unity.inputsystem@1.17/api/", + "Enable()", + "M:UnityEngine.InputSystem.InputActionAsset.Enable", + "https://docs.unity3d.com/Packages/com.unity.inputsystem@1.17/api/UnityEngine.InputSystem.InputActionAsset.html#UnityEngine_InputSystem_InputActionAsset_Enable")] + [InlineData( + "https://docs.unity3d.com/Packages/com.unity.inputsystem@1.17/api/", + "Contains(InputAction)", + "M:UnityEngine.InputSystem.InputActionAsset.Contains(UnityEngine.InputSystem.InputAction)", + "https://docs.unity3d.com/Packages/com.unity.inputsystem@1.17/api/UnityEngine.InputSystem.InputActionAsset.html#UnityEngine_InputSystem_InputActionAsset_Contains_UnityEngine_InputSystem_InputAction_")] + [InlineData( + "https://docs.unity3d.com/Packages/com.unity.inputsystem@1.17/api/", + "FindBinding(InputBinding, out InputAction)", + "M:UnityEngine.InputSystem.InputActionAsset.FindBinding(UnityEngine.InputSystem.InputBinding,UnityEngine.InputSystem.InputAction@)", + "https://docs.unity3d.com/Packages/com.unity.inputsystem@1.17/api/UnityEngine.InputSystem.InputActionAsset.html#UnityEngine_InputSystem_InputActionAsset_FindBinding_UnityEngine_InputSystem_InputBinding_UnityEngine_InputSystem_InputAction__")] + [InlineData( + "https://docs.unity3d.com/Packages/com.unity.inputsystem@1.17/api/", + "AddDevice(string)", + "M:UnityEngine.InputSystem.InputSystem.AddDevice``1(System.String)", + "https://docs.unity3d.com/Packages/com.unity.inputsystem@1.17/api/UnityEngine.InputSystem.InputSystem.html#UnityEngine_InputSystem_InputSystem_AddDevice__1_System_String_")] + [InlineData( + "https://docs.unity3d.com/Packages/com.unity.inputsystem@1.17/api/", + "remoting", + "P:UnityEngine.InputSystem.InputSystem.remoting", + "https://docs.unity3d.com/Packages/com.unity.inputsystem@1.17/api/UnityEngine.InputSystem.InputSystem.html#UnityEngine_InputSystem_InputSystem_remoting")] + public void Package_Fix(string apiUrl, string name, string commentId, string expected) + { + XrefMapReference xrefMapReference = new() + { + Name = name, + CommentId = commentId, + }; + + Assert.Equal(expected, XRefHrefFixer.Fix(apiUrl, xrefMapReference, Array.Empty(), true)); + } + } +} diff --git a/UnityXrefMaps.lutconfig b/UnityXrefMaps.lutconfig new file mode 100644 index 0000000..7d8c768 --- /dev/null +++ b/UnityXrefMaps.lutconfig @@ -0,0 +1,6 @@ + + + true + true + 180000 + \ No newline at end of file diff --git a/UnityXrefMaps/Commands/BuildCommand.cs b/UnityXrefMaps/Commands/BuildCommand.cs index 603aecf..c84098b 100644 --- a/UnityXrefMaps/Commands/BuildCommand.cs +++ b/UnityXrefMaps/Commands/BuildCommand.cs @@ -1,5 +1,6 @@ using System.CommandLine; using System.IO; +using System.Linq; using System.Text.Json; using System.Text.RegularExpressions; using LibGit2Sharp; @@ -11,7 +12,7 @@ namespace UnityXrefMaps.Commands; internal sealed partial class BuildCommand : RootCommand { - public BuildCommand(ILogger logger) + public BuildCommand(ILoggerFactory loggerFactory) { Option repositoryUrlOption = new("--repositoryUrl") { @@ -62,6 +63,11 @@ public BuildCommand(ILogger logger) { Description = "Namespaces for trimming." }; + Option packageRegexOption = new("--packageRegex") + { + Description = "The regular expression to check if it is a package.", + DefaultValueFactory = _ => Constants.DefaultPackageRegex + }; Options.Add(repositoryUrlOption); Options.Add(repositoryBranchOption); @@ -72,11 +78,14 @@ public BuildCommand(ILogger logger) Options.Add(docFxAdditionalArgumentsOption); Options.Add(xrefMapsPathOption); Options.Add(trimNamespacesOption); + Options.Add(packageRegexOption); SetAction(async (parseResult, cancellationToken) => { bool result = true; + XrefMapService xrefMapService = new(loggerFactory.CreateLogger()); + string? repositoryUrl = parseResult.GetValue(repositoryUrlOption); string? repositoryBranch = parseResult.GetValue(repositoryBranchOption); string? repositoryPath = parseResult.GetValue(repositoryPathOption); @@ -86,6 +95,9 @@ public BuildCommand(ILogger logger) string? docFxAdditionalArguments = parseResult.GetValue(docFxAdditionalArgumentsOption); string? xrefMapsPath = parseResult.GetValue(xrefMapsPathOption); string[]? trimNamespaces = parseResult.GetValue(trimNamespacesOption); + string? packageRegex = parseResult.GetValue(packageRegexOption); + + bool isPackage = Regex.IsMatch(apiUrl!, packageRegex!); using Stream docFxStream = File.OpenRead(docFxFilePath!); @@ -94,7 +106,9 @@ public BuildCommand(ILogger logger) string docFxFileDirectoryPath = Path.GetDirectoryName(docFxFilePath)!; string generatedDocsPath = Path.Combine(docFxFileDirectoryPath, docFxConfiguration!.Build!.Destination!); - string generatedXrefMapPath = Path.Combine(generatedDocsPath, Constants.DefaultXrefMapFileName!); + string generatedXrefMapPath = Path.Combine(generatedDocsPath, Constants.DefaultXrefMapFileName!); + + ILogger logger = loggerFactory.CreateLogger(); if (logger.IsEnabled(LogLevel.Information)) { @@ -173,11 +187,11 @@ await Utils.RunCommand( await Utils.CopyFile(generatedXrefMapPath, xrefMapPath, cancellationToken); - XrefMap xrefMap = await XrefMap.Load(xrefMapPath, cancellationToken); - - xrefMap.FixHrefs(string.Format(apiUrl!, shortVersion), trimNamespaces!); + XrefMap xrefMap = await xrefMapService.Load(xrefMapPath, cancellationToken); + + xrefMap.References = [.. xrefMapService.Process(string.Format(apiUrl!, shortVersion), xrefMap.References!, trimNamespaces!, isPackage)]; - await xrefMap.Save(xrefMapPath, cancellationToken); + await xrefMapService.Save(xrefMapPath, xrefMap, cancellationToken); } return result ? 0 : 1; diff --git a/UnityXrefMaps/Commands/TestCommand.cs b/UnityXrefMaps/Commands/TestCommand.cs index a84843e..b321d03 100644 --- a/UnityXrefMaps/Commands/TestCommand.cs +++ b/UnityXrefMaps/Commands/TestCommand.cs @@ -5,7 +5,7 @@ namespace UnityXrefMaps.Commands; internal sealed class TestCommand : Command { - public TestCommand(ILogger logger) : base("test", $"Check that the links in the {Constants.DefaultXrefMapFileName} file are valid.") + public TestCommand(ILoggerFactory loggerFactory) : base("test", $"Check that the links in the {Constants.DefaultXrefMapFileName} file are valid.") { Option xrefPathOption = new("--xrefPath") { @@ -17,11 +17,15 @@ public TestCommand(ILogger logger) : base("test", $"Check that the SetAction(async (parseResult, cancellationToken) => { - bool result = true; + bool result = true; + + XrefMapService xrefMapService = new(loggerFactory.CreateLogger()); + + ILogger logger = loggerFactory.CreateLogger(); string? xrefPath = parseResult.GetValue(xrefPathOption); - XrefMap xrefMap = await XrefMap.Load(xrefPath!, cancellationToken); + XrefMap xrefMap = await xrefMapService.Load(xrefPath!, cancellationToken); foreach (XrefMapReference reference in xrefMap.References!) { diff --git a/UnityXrefMaps/Constants.cs b/UnityXrefMaps/Constants.cs index d49f7b0..c4d0004 100644 --- a/UnityXrefMaps/Constants.cs +++ b/UnityXrefMaps/Constants.cs @@ -37,4 +37,9 @@ internal static class Constants /// The default DocFX config file path. /// public const string DefaultDocFxConfigurationFilePath = "docfx.json"; + + /// + /// The default regular expression to check if it is a package. + /// + public const string DefaultPackageRegex = @"https://docs.unity3d.com/Packages/"; } diff --git a/UnityXrefMaps/Program.cs b/UnityXrefMaps/Program.cs index e413f40..cbdfd37 100644 --- a/UnityXrefMaps/Program.cs +++ b/UnityXrefMaps/Program.cs @@ -7,7 +7,7 @@ builder.AddConsole(); }); -RootCommand rootCommand = new BuildCommand(factory.CreateLogger()); -rootCommand.Subcommands.Add(new TestCommand(factory.CreateLogger())); +RootCommand rootCommand = new BuildCommand(factory); +rootCommand.Subcommands.Add(new TestCommand(factory)); return await rootCommand.Parse(args).InvokeAsync(); diff --git a/UnityXrefMaps/XRefHrefFixer.cs b/UnityXrefMaps/XRefHrefFixer.cs new file mode 100644 index 0000000..8bdfab1 --- /dev/null +++ b/UnityXrefMaps/XRefHrefFixer.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace UnityXrefMaps +{ + internal static partial class XRefHrefFixer + { + public static string Fix(string apiUrl, XrefMapReference xrefMapReference, IEnumerable hrefNamespacesToTrim, bool isPackage) + { + string[] parts = xrefMapReference.CommentId!.Split(':'); + string type = parts[0]; + string uid = parts[1]; + string name = xrefMapReference.Name!; + + if (isPackage) + { + return FixPackage(apiUrl, name, type, uid); + } + else + { + return FixUnity(apiUrl, type, uid, hrefNamespacesToTrim); + } + } + + // https://docs.unity3d.com/Packages/com.unity.package-manager-doctools@3.14/manual/index.html + // https://github.com/dotnet/docfx/blob/main/src/Docfx.Dotnet/SymbolUrlResolver.cs#L37 + private static string FixPackage(string apiUrl, string name, string type, string uid) + { + string classFullName; + + switch (type) + { + case "N": + case "T": + string _namespace = uid; + + return $"{apiUrl}{_namespace}.html"; + case "M": + Match methodNameMatch = MethodNameRegex().Match(name); + + string methodName = name.Substring(0, methodNameMatch.Groups[1].Value.Length); + + classFullName = uid.Substring(0, uid.IndexOf(methodName) - 1); + + return $"{apiUrl}{classFullName}.html#{NonWordCharRegex().Replace(uid, "_")}"; + case "P": + case "F": + default: + classFullName = uid.Substring(0, uid.IndexOf(name) - 1); + + return $"{apiUrl}{classFullName}.html#{NonWordCharRegex().Replace(uid, "_")}"; + } + } + + private static string FixUnity(string apiUrl, string type, string uid, IEnumerable hrefNamespacesToTrim) + { + string href; + + // Namespaces point to documentation index + if (type.Equals("N")) + { + href = "index"; + } + else + { + href = uid; + + foreach (string hrefNamespaceToTrim in hrefNamespacesToTrim) + { + href = href.Replace(hrefNamespaceToTrim + ".", string.Empty); + } + + // Fix href of constructors + href = href.Replace(".#ctor", "-ctor"); + + // Fix href of generics + href = GenericHrefRegex().Replace(href, string.Empty); + href = href.Replace("`", "_"); + + // Fix href of methods + href = MethodHrefPointerRegex().Replace(href, string.Empty); + href = MethodHrefRegex().Replace(href, string.Empty); + + // Fix href of operator + if (type.Equals("M") && uid.Contains(".op_")) + { + href = href.Replace(".op_", ".operator_"); + + href = href.Replace(".operator_Subtraction", ".operator_subtract"); + href = href.Replace(".operator_Multiply", ".operator_multiply"); + href = href.Replace(".operator_Division", ".operator_divide"); + href = href.Replace(".operator_Addition", ".operator_add"); + href = href.Replace(".operator_Equality", ".operator_eq"); + href = href.Replace(".operator_Implicit~", ".operator_"); + } + + // Fix href of properties + if (type.Equals("F") || type.Equals("M") || type.Equals("P")) + { + href = PropertyHrefRegex().Replace(href, "-$1"); + } + } + + return apiUrl + href + ".html"; + } + + [GeneratedRegex(@"`{2}\d")] + private static partial Regex GenericHrefRegex(); + + [GeneratedRegex(@"\*$")] + private static partial Regex MethodHrefPointerRegex(); + + [GeneratedRegex(@"\(.*\)")] + private static partial Regex MethodHrefRegex(); + + [GeneratedRegex(@"\.([a-z].*)$")] + private static partial Regex PropertyHrefRegex(); + + [GeneratedRegex(@"\W")] + private static partial Regex NonWordCharRegex(); + + [GeneratedRegex(@"([^<>]*).*\(")] + private static partial Regex MethodNameRegex(); + } +} diff --git a/UnityXrefMaps/XrefMap.cs b/UnityXrefMaps/XrefMap.cs index a253920..0d8d97f 100644 --- a/UnityXrefMaps/XrefMap.cs +++ b/UnityXrefMaps/XrefMap.cs @@ -1,8 +1,3 @@ -using System.Collections.Generic; -using System.IO; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; using YamlDotNet.Serialization; namespace UnityXrefMaps; @@ -10,65 +5,11 @@ namespace UnityXrefMaps; /// /// Represents a xref map file of Unity. /// -public sealed partial class XrefMap +public sealed class XrefMap { - private static readonly Deserializer s_deserializer = new(); - private static readonly Serializer s_serializer = new(); - [YamlMember(Alias = "sorted")] public bool Sorted { get; set; } [YamlMember(Alias = "references")] public XrefMapReference[]? References { get; set; } - - /// - /// Loads a from a file. - /// - /// The path of the file. - /// The loaded from . - public static async Task Load(string filePath, CancellationToken cancellationToken = default) - { - string xrefMapText = await File.ReadAllTextAsync(filePath, cancellationToken); - - // Remove `0:` strings on the xrefmap that make crash Deserializer - xrefMapText = ZeroStringsRegex().Replace(xrefMapText, "$1"); - - return s_deserializer.Deserialize(xrefMapText); - } - - /// - /// Fix the of of this . - /// - /// The URL of the online API documentation of Unity. - public void FixHrefs(string apiUrl, IEnumerable hrefNamespacesToTrim) - { - var fixedReferences = new List(); - - foreach (XrefMapReference reference in References!) - { - if (!reference.IsValid) - { - continue; - } - - reference.FixHref(apiUrl, hrefNamespacesToTrim); - fixedReferences.Add(reference); - } - - References = [.. fixedReferences]; - } - - /// - /// Saves this to a file. - /// - /// The path of the file. - public async Task Save(string filePath, CancellationToken cancellationToken = default) - { - string xrefMapText = "### YamlMime:XRefMap\n" + s_serializer.Serialize(this); - - await File.WriteAllTextAsync(filePath, xrefMapText, cancellationToken); - } - - [GeneratedRegex(@"(\d):")] - private static partial Regex ZeroStringsRegex(); } diff --git a/UnityXrefMaps/XrefMapReference.cs b/UnityXrefMaps/XrefMapReference.cs index 330f12d..b6073b7 100644 --- a/UnityXrefMaps/XrefMapReference.cs +++ b/UnityXrefMaps/XrefMapReference.cs @@ -1,5 +1,3 @@ -using System.Collections.Generic; -using System.Text.RegularExpressions; using YamlDotNet.Serialization; namespace UnityXrefMaps; @@ -7,7 +5,7 @@ namespace UnityXrefMaps; /// /// Represents a reference item on a . /// -public sealed partial class XrefMapReference +public sealed class XrefMapReference { [YamlMember(Alias = "uid")] public string? Uid { get; set; } @@ -44,70 +42,4 @@ public sealed partial class XrefMapReference /// [YamlIgnore] public bool IsValid => !CommentId!.Contains("Overload:"); - - /// - /// Sets to link to the online API documentation of Unity. - /// - /// The URL of the online API documentation of Unity. - public void FixHref(string apiUrl, IEnumerable hrefNamespacesToTrim) - { - // Namespaces point to documentation index - if (CommentId!.StartsWith("N:")) - { - Href = "index"; - } - else - { - Href = Uid; - - foreach (string hrefNamespaceToTrim in hrefNamespacesToTrim) - { - Href = Href!.Replace(hrefNamespaceToTrim + ".", string.Empty); - } - - // Fix href of constructors - Href = Href!.Replace(".#ctor", "-ctor"); - - // Fix href of generics - Href = GenericHrefRegex().Replace(Href!, string.Empty); - Href = Href.Replace("`", "_"); - - // Fix href of methods - Href = MethodHrefPointerRegex().Replace(Href, string.Empty); - Href = MethodHrefRegex().Replace(Href, string.Empty); - - // Fix href of operator - if (CommentId.StartsWith("M:") && CommentId.Contains(".op_")) - { - Href = Href.Replace(".op_", ".operator_"); - - Href = Href.Replace(".operator_Subtraction", ".operator_subtract"); - Href = Href.Replace(".operator_Multiply", ".operator_multiply"); - Href = Href.Replace(".operator_Division", ".operator_divide"); - Href = Href.Replace(".operator_Addition", ".operator_add"); - Href = Href.Replace(".operator_Equality", ".operator_eq"); - Href = Href.Replace(".operator_Implicit~", ".operator_"); - } - - // Fix href of properties - if (CommentId.StartsWith("F:") || CommentId.StartsWith("M:") || CommentId.StartsWith("P:")) - { - Href = PropertyHrefRegex().Replace(Href, "-$1"); - } - } - - Href = apiUrl + Href + ".html"; - } - - [GeneratedRegex(@"`{2}\d")] - private static partial Regex GenericHrefRegex(); - - [GeneratedRegex(@"\*$")] - private static partial Regex MethodHrefPointerRegex(); - - [GeneratedRegex(@"\(.*\)")] - private static partial Regex MethodHrefRegex(); - - [GeneratedRegex(@"\.([a-z].*)$")] - private static partial Regex PropertyHrefRegex(); } diff --git a/UnityXrefMaps/XrefMapService.cs b/UnityXrefMaps/XrefMapService.cs new file mode 100644 index 0000000..8aeacf1 --- /dev/null +++ b/UnityXrefMaps/XrefMapService.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using YamlDotNet.Serialization; + +namespace UnityXrefMaps +{ + internal sealed partial class XrefMapService + { + private readonly Deserializer _deserializer = new(); + private readonly Serializer _serializer = new(); + + private readonly ILogger _logger; + + public XrefMapService(ILogger logger) + { + _logger = logger; + } + + /// + /// Loads a from a file. + /// + /// The path of the file. + /// The loaded from . + public async Task Load(string filePath, CancellationToken cancellationToken = default) + { + string xrefMapText = await File.ReadAllTextAsync(filePath, cancellationToken); + + // Remove `0:` strings on the xrefmap that make crash Deserializer + xrefMapText = ZeroStringsRegex().Replace(xrefMapText, "$1"); + + return _deserializer.Deserialize(xrefMapText); + } + + /// + /// Fix the of of this . + /// + /// The URL of the online API documentation of Unity. + public IEnumerable Process(string apiUrl, IEnumerable references, IEnumerable hrefNamespacesToTrim, bool isPackage) + { + List fixedReferences = []; + + foreach (XrefMapReference reference in references) + { + if (!reference.IsValid) + { + continue; + } + + try + { + reference.Href = XRefHrefFixer.Fix(apiUrl, reference, hrefNamespacesToTrim, isPackage); + + fixedReferences.Add(reference); + } + catch (Exception e) + { + _logger.LogWarning(e, "Error fixing href: {Uid}", reference.Uid); + } + } + + return fixedReferences; + } + + /// + /// Saves this to a file. + /// + /// The path of the file. + public async Task Save(string filePath, XrefMap xrefMap, CancellationToken cancellationToken = default) + { + string xrefMapText = "### YamlMime:XRefMap\n" + _serializer.Serialize(xrefMap); + + await File.WriteAllTextAsync(filePath, xrefMapText, cancellationToken); + } + + [GeneratedRegex(@"(\d):")] + private static partial Regex ZeroStringsRegex(); + } +} From 639b998c149c7537217347580520afec2d2dc752 Mon Sep 17 00:00:00 2001 From: bdovaz Date: Tue, 23 Dec 2025 16:50:29 +0100 Subject: [PATCH 29/29] wip --- UnityXrefMaps/Commands/BuildCommand.cs | 13 +++++++---- UnityXrefMaps/RepositoryExtensions.cs | 30 +++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/UnityXrefMaps/Commands/BuildCommand.cs b/UnityXrefMaps/Commands/BuildCommand.cs index c84098b..ee73ba6 100644 --- a/UnityXrefMaps/Commands/BuildCommand.cs +++ b/UnityXrefMaps/Commands/BuildCommand.cs @@ -32,9 +32,9 @@ public BuildCommand(ILoggerFactory loggerFactory) }; Option repositoryTagsOption = new("--repositoryTags") { - Description = "The repository tags to use to generate the documentation. " + - "That is, the versions of the Unity editor (6000.0.1f1, 6000.1.1f1) or the versions of the Unity package (1.0.0, 1.1.0).", - Required = true + Description = "The repository tags to use to generate the xrefmap file. " + + "That is, the versions of the Unity editor (6000.0.1f1, 6000.1.1f1) or the versions of the Unity package (1.0.0, 1.1.0). " + + "If not set, it will resolve all tags in the repository and generate a xrefmap file for each x.y (examples: 6000.0, 6000.1) based on the most recent patch." }; Option apiUrlOption = new("--apiUrl") { @@ -124,7 +124,12 @@ public BuildCommand(ILoggerFactory loggerFactory) docFxArguments += ' ' + docFxAdditionalArguments; } - foreach (string repositoryTag in repositoryTags!) + if (repositoryTags == null || repositoryTags.Length == 0) + { + repositoryTags = [.. repository.GetLatestVersions().Select(v => v.release)]; + } + + foreach (string repositoryTag in repositoryTags) { Match versionMatch = VersionRegex().Match(repositoryTag); diff --git a/UnityXrefMaps/RepositoryExtensions.cs b/UnityXrefMaps/RepositoryExtensions.cs index 67a42ab..e3fcda9 100644 --- a/UnityXrefMaps/RepositoryExtensions.cs +++ b/UnityXrefMaps/RepositoryExtensions.cs @@ -1,4 +1,7 @@ using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; using LibGit2Sharp; using Microsoft.Extensions.Logging; @@ -7,8 +10,20 @@ namespace UnityXrefMaps; /// /// Extension methods for . /// -internal static class RepositoryExtensions +internal static partial class RepositoryExtensions { + /// + /// Returns a collection of the latest tags of a specified . + /// + /// The to use. + /// The collection of tags. + public static IEnumerable GetTags(this Repository repository) + { + return repository.Tags + .OrderByDescending(tag => (tag.Target as Commit)!.Author.When) + .Select(tag => tag.FriendlyName); + } + /// /// Hard resets the specified to the specified commit. /// @@ -27,5 +42,18 @@ public static void HardReset(this Repository repository, string commit, ILogger repository.RemoveUntrackedFiles(); } catch (Exception) { } + } + + public static IEnumerable<(string name, string release)> GetLatestVersions(this Repository unityRepository) + { + return unityRepository + .GetTags() + .Select(release => (name: UnityVersionRegex().Match(release).Value, release)) + .GroupBy(version => version.name) + .Select(version => version.First()) + .ToArray(); } + + [GeneratedRegex("\\d{4}\\.\\d")] + private static partial Regex UnityVersionRegex(); }