diff --git a/dotnet/Selenium.sln b/dotnet/Selenium.sln index 59907c6346ca5..49b566611dbb3 100644 --- a/dotnet/Selenium.sln +++ b/dotnet/Selenium.sln @@ -23,6 +23,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Selenium.WebDriver.Safari.T EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Selenium.WebDriver.Support.Tests", "test\support\Selenium.WebDriver.Support.Tests.csproj", "{2136C695-2526-45E0-AE1D-68FBBC6A9DE2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Selenium.WebDriver.ResourceUtilitiesGenerator", "private\Selenium.WebDriver.ResourceUtilitiesGenerator\Selenium.WebDriver.ResourceUtilitiesGenerator.csproj", "{C3650129-9310-F297-D3F8-219678D6F433}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -69,6 +71,10 @@ Global {2136C695-2526-45E0-AE1D-68FBBC6A9DE2}.Debug|Any CPU.Build.0 = Debug|Any CPU {2136C695-2526-45E0-AE1D-68FBBC6A9DE2}.Release|Any CPU.ActiveCfg = Release|Any CPU {2136C695-2526-45E0-AE1D-68FBBC6A9DE2}.Release|Any CPU.Build.0 = Release|Any CPU + {C3650129-9310-F297-D3F8-219678D6F433}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C3650129-9310-F297-D3F8-219678D6F433}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C3650129-9310-F297-D3F8-219678D6F433}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C3650129-9310-F297-D3F8-219678D6F433}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/dotnet/private/Selenium.WebDriver.ResourceUtilitiesGenerator/ResourceUtilitiesGenerator.cs b/dotnet/private/Selenium.WebDriver.ResourceUtilitiesGenerator/ResourceUtilitiesGenerator.cs new file mode 100644 index 0000000000000..b06da29bb87f0 --- /dev/null +++ b/dotnet/private/Selenium.WebDriver.ResourceUtilitiesGenerator/ResourceUtilitiesGenerator.cs @@ -0,0 +1,167 @@ +// +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; +using System; +using System.Collections.Immutable; +using System.IO; +using System.Text; +using System.Threading; + +namespace Selenium.WebDriver.ResourceUtilitiesGenerator; + +[Generator] +public class ResourceUtilitiesGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var jsFiles = context.AdditionalTextsProvider + .Where(static (text) => text.Path.EndsWith(".js") || text.Path.EndsWith(".json")) + .WithTrackingName("ResourceFiles") + .Select(static (data, token) => + { + var name = Path.GetFileName(data.Path); + var code = GenerateAtom(data, token, out string propertyName, out var diagnostics); + + return (name, code, propertyName, diagnostics); + }); + + context.RegisterSourceOutput(jsFiles, static (context, pair) => + { + foreach (var diagnostic in pair.diagnostics) + { + context.ReportDiagnostic(diagnostic); + } + if (pair.code is not null) + { + context.AddSource($"ResourceUtilities.{pair.propertyName}.g.cs", SourceText.From(pair.code, Encoding.UTF8)); + } + }); + } + + private static string? GenerateAtom(AdditionalText additionalText, CancellationToken token, out string propertyName, out ImmutableArray diagnostics) + { + diagnostics = []; + var sourceText = additionalText.GetText(token); + if (sourceText is null) + { + var d = Diagnostic.Create(new DiagnosticDescriptor("WRG1001", "Failed to read atom", "Atom '{0}' could not be read", "WebDriverResourceGenerator", DiagnosticSeverity.Error, true), Location.None, additionalText.Path); + diagnostics = [d]; + propertyName = string.Empty; + return null; + } + + propertyName = GetPropertyNameFromFilePath(additionalText.Path, out var lang, out var diag); + if (diag is not null) + { + diagnostics = diagnostics.Add(diag); + } + + string contents = $$""""""" + // + + namespace OpenQA.Selenium.Internal; + + internal static partial class ResourceUtilities + { + [global::System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("{{lang}}")] + internal const string {{propertyName}} = + """"" + + """""""; + + var builder = new StringBuilder(contents); + + // Normalize line endings for each line + foreach (var line in sourceText.Lines) + { + builder.AppendLine(sourceText.GetSubText(line.Span).ToString()); + } + + builder.AppendLine(""""""" + """""; + } + """""""); + + return builder.ToString(); + } + + private static string GetPropertyNameFromFilePath(string filePath, out string language, out Diagnostic? diagnostic) + { + diagnostic = null; + if (filePath.EndsWith("webdriver.json")) + { + language = "json"; + return "WebDriverPrefsJson"; + } + else if (filePath.EndsWith("is-displayed.js")) + { + language = "javascript"; + return "IsDisplayedAtom"; + } + else if (filePath.EndsWith("mutation-listener.js")) + { + language = "javascript"; + return "MutationListenerAtom"; + } + else if (filePath.EndsWith("get-attribute.js")) + { + language = "javascript"; + return "GetAttributeAtom"; + } + else if (filePath.EndsWith("find-elements.js")) + { + language = "javascript"; + return "FindElementsAtom"; + } + + diagnostic = Diagnostic.Create(new DiagnosticDescriptor("WRG1002", "Unknown resource file", "Unknown file in the resource generator '{0}'", "WebDriverResourceGenerator", DiagnosticSeverity.Warning, true), Location.None, filePath); + + var suffix = filePath.EndsWith(".js") ? "Atom" : "Json"; + + language = string.Empty; + return KebabCaseToPascalCase(Path.GetFileNameWithoutExtension(filePath)) + suffix; + } + + private static string KebabCaseToPascalCase(string v) + { + Span newValues = new char[v.Length]; + int newValuesOffset = 0; + for (int i = 0; i < v.Length; i++) + { + if (i == 0) + { + newValues[i - newValuesOffset] = char.ToUpperInvariant(v[i]); + } + else if (char.IsLetter(v[i])) + { + newValues[i - newValuesOffset] = v[i]; + } + else + { + i++; + newValuesOffset++; + newValues[i - newValuesOffset] = char.ToUpperInvariant(v[i]); + } + } + + return newValues.Slice(0, v.Length - newValuesOffset).ToString(); + } +} diff --git a/dotnet/private/Selenium.WebDriver.ResourceUtilitiesGenerator/Selenium.WebDriver.ResourceUtilitiesGenerator.csproj b/dotnet/private/Selenium.WebDriver.ResourceUtilitiesGenerator/Selenium.WebDriver.ResourceUtilitiesGenerator.csproj new file mode 100644 index 0000000000000..a94a3b0bbc31a --- /dev/null +++ b/dotnet/private/Selenium.WebDriver.ResourceUtilitiesGenerator/Selenium.WebDriver.ResourceUtilitiesGenerator.csproj @@ -0,0 +1,24 @@ + + + + netstandard2.0 + preview + enable + true + + true + false + + + + + + + + + + + + + + diff --git a/dotnet/src/webdriver/Selenium.WebDriver.csproj b/dotnet/src/webdriver/Selenium.WebDriver.csproj index 5d549c6342759..358d5110435ee 100644 --- a/dotnet/src/webdriver/Selenium.WebDriver.csproj +++ b/dotnet/src/webdriver/Selenium.WebDriver.csproj @@ -79,13 +79,20 @@ - - + + + - - - - + + + + + + + +