From 28611ab7418c2121edef755c5a14ba2e32d7cb0d Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Mon, 30 Oct 2023 14:49:11 +0200 Subject: [PATCH] Improve property name escaping (#1638) * don't allow "init", "fromJS" or "toJSON" for TypeScript member name * improve performance by not doing string.Replace unless necessary --- .../CSharpPropertyNameGenerator.cs | 38 ++++++++---- .../PropertyNameTests.cs | 32 ++++++++++ ...d_properties_they_are_escaped.verified.txt | 58 +++++++++++++++++++ .../TypeScriptPropertyNameGenerator.cs | 37 +++++++----- .../Generation/SampleJsonDataGenerator.cs | 12 ++-- 5 files changed, 146 insertions(+), 31 deletions(-) create mode 100644 src/NJsonSchema.CodeGeneration.TypeScript.Tests/PropertyNameTests.cs create mode 100644 src/NJsonSchema.CodeGeneration.TypeScript.Tests/Snapshots/PropertyNameTests.When_class_has_restricted_properties_they_are_escaped.verified.txt diff --git a/src/NJsonSchema.CodeGeneration.CSharp/CSharpPropertyNameGenerator.cs b/src/NJsonSchema.CodeGeneration.CSharp/CSharpPropertyNameGenerator.cs index cc9cbad8b..2c61c752f 100644 --- a/src/NJsonSchema.CodeGeneration.CSharp/CSharpPropertyNameGenerator.cs +++ b/src/NJsonSchema.CodeGeneration.CSharp/CSharpPropertyNameGenerator.cs @@ -9,15 +9,21 @@ namespace NJsonSchema.CodeGeneration.CSharp { /// Generates the property name for a given CSharp . - public class CSharpPropertyNameGenerator : IPropertyNameGenerator + public sealed class CSharpPropertyNameGenerator : IPropertyNameGenerator { + private static readonly char[] _reservedFirstPassChars = { '"', '\'', '@', '?', '!', '$', '[', ']', '(', ')', '.', '=', '+' }; + private static readonly char[] _reservedSecondPassChars = { '*', ':', '-', '#', '&' }; + /// Generates the property name. /// The property. /// The new name. - public virtual string Generate(JsonSchemaProperty property) + public string Generate(JsonSchemaProperty property) { - return ConversionUtilities.ConvertToUpperCamelCase(property.Name - .Replace("\"", string.Empty) + var name = property.Name; + + if (name.IndexOfAny(_reservedFirstPassChars) != -1) + { + name = name.Replace("\"", string.Empty) .Replace("'", string.Empty) .Replace("@", string.Empty) .Replace("?", string.Empty) @@ -29,12 +35,22 @@ public virtual string Generate(JsonSchemaProperty property) .Replace(")", string.Empty) .Replace(".", "-") .Replace("=", "-") - .Replace("+", "plus"), true) - .Replace("*", "Star") - .Replace(":", "_") - .Replace("-", "_") - .Replace("#", "_") - .Replace("&", "And"); + .Replace("+", "plus"); + } + + name = ConversionUtilities.ConvertToUpperCamelCase(name, true); + + if (name.IndexOfAny(_reservedSecondPassChars) != -1) + { + name = name + .Replace("*", "Star") + .Replace(":", "_") + .Replace("-", "_") + .Replace("#", "_") + .Replace("&", "And"); + } + + return name; } } -} +} \ No newline at end of file diff --git a/src/NJsonSchema.CodeGeneration.TypeScript.Tests/PropertyNameTests.cs b/src/NJsonSchema.CodeGeneration.TypeScript.Tests/PropertyNameTests.cs new file mode 100644 index 000000000..5a3199fdf --- /dev/null +++ b/src/NJsonSchema.CodeGeneration.TypeScript.Tests/PropertyNameTests.cs @@ -0,0 +1,32 @@ +using System.Threading.Tasks; +using NJsonSchema.Annotations; +using NJsonSchema.NewtonsoftJson.Generation; +using VerifyXunit; +using Xunit; + +using static NJsonSchema.CodeGeneration.TypeScript.Tests.VerifyHelper; + +namespace NJsonSchema.CodeGeneration.TypeScript.Tests; + +[UsesVerify] +public class PropertyNameTests +{ + private class TypeWithRestrictedProperties + { + public string Constructor { get; set; } + public string Init { get; set; } + public string FromJS { get; set; } + public string ToJSON { get; set; } + } + + [Fact] + public async Task When_class_has_restricted_properties_they_are_escaped() + { + var schema = NewtonsoftJsonSchemaGenerator.FromType(); + + var generator = new TypeScriptGenerator(schema, new TypeScriptGeneratorSettings { TypeScriptVersion = 4.3m }); + var output = generator.GenerateFile(nameof(TypeWithRestrictedProperties)); + + await Verify(output); + } +} \ No newline at end of file diff --git a/src/NJsonSchema.CodeGeneration.TypeScript.Tests/Snapshots/PropertyNameTests.When_class_has_restricted_properties_they_are_escaped.verified.txt b/src/NJsonSchema.CodeGeneration.TypeScript.Tests/Snapshots/PropertyNameTests.When_class_has_restricted_properties_they_are_escaped.verified.txt new file mode 100644 index 000000000..9a716687c --- /dev/null +++ b/src/NJsonSchema.CodeGeneration.TypeScript.Tests/Snapshots/PropertyNameTests.When_class_has_restricted_properties_they_are_escaped.verified.txt @@ -0,0 +1,58 @@ +//---------------------- +// +// +//---------------------- + + + + + + + +export class TypeWithRestrictedProperties implements ITypeWithRestrictedProperties { + constructor_!: string | undefined; + init_!: string | undefined; + fromJS_!: string | undefined; + toJSON_!: string | undefined; + + constructor(data?: ITypeWithRestrictedProperties) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data?: any) { + if (_data) { + this.constructor_ = _data["Constructor"]; + this.init_ = _data["Init"]; + this.fromJS_ = _data["FromJS"]; + this.toJSON_ = _data["ToJSON"]; + } + } + + static fromJS(data: any): TypeWithRestrictedProperties { + data = typeof data === 'object' ? data : {}; + let result = new TypeWithRestrictedProperties(); + result.init(data); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["Constructor"] = this.constructor_; + data["Init"] = this.init_; + data["FromJS"] = this.fromJS_; + data["ToJSON"] = this.toJSON_; + return data; + } +} + +export interface ITypeWithRestrictedProperties { + constructor_: string | undefined; + init_: string | undefined; + fromJS_: string | undefined; + toJSON_: string | undefined; +} \ No newline at end of file diff --git a/src/NJsonSchema.CodeGeneration.TypeScript/TypeScriptPropertyNameGenerator.cs b/src/NJsonSchema.CodeGeneration.TypeScript/TypeScriptPropertyNameGenerator.cs index cf3203144..6f92c8e84 100644 --- a/src/NJsonSchema.CodeGeneration.TypeScript/TypeScriptPropertyNameGenerator.cs +++ b/src/NJsonSchema.CodeGeneration.TypeScript/TypeScriptPropertyNameGenerator.cs @@ -6,32 +6,43 @@ // Rico Suter, mail@rsuter.com //----------------------------------------------------------------------- +using System; using System.Collections.Generic; -using System.Linq; namespace NJsonSchema.CodeGeneration.TypeScript { /// Generates the property name for a given TypeScript . - public class TypeScriptPropertyNameGenerator : IPropertyNameGenerator + public sealed class TypeScriptPropertyNameGenerator : IPropertyNameGenerator { + private static readonly char[] _reservedFirstPassChars = { '"', '@', '?', '.', '=', '+' }; + private static readonly char[] _reservedSecondPassChars = { '*', ':', '-' }; + /// Gets or sets the reserved names. - public IEnumerable ReservedPropertyNames { get; set; } = new List { "constructor" }; + public HashSet ReservedPropertyNames { get; set; } = new(StringComparer.Ordinal) { "constructor", "init", "fromJS", "toJSON" }; - /// Generates the property name. - /// The property. - /// The new name. - public virtual string Generate(JsonSchemaProperty property) + /// + public string Generate(JsonSchemaProperty property) { - var name = ConversionUtilities.ConvertToLowerCamelCase(property.Name - .Replace("\"", string.Empty) + var name = property.Name; + + if (name.IndexOfAny(_reservedFirstPassChars) != -1) + { + name = name.Replace("\"", string.Empty) .Replace("@", string.Empty) .Replace("?", string.Empty) .Replace(".", "-") .Replace("=", "-") - .Replace("+", "plus"), true) - .Replace("*", "Star") - .Replace(":", "_") - .Replace("-", "_"); + .Replace("+", "plus"); + } + + name = ConversionUtilities.ConvertToLowerCamelCase(name, true); + + if (name.IndexOfAny(_reservedSecondPassChars) != -1) + { + name = name.Replace("*", "Star") + .Replace(":", "_") + .Replace("-", "_"); + } if (ReservedPropertyNames.Contains(name)) { diff --git a/src/NJsonSchema/Generation/SampleJsonDataGenerator.cs b/src/NJsonSchema/Generation/SampleJsonDataGenerator.cs index 48a2ec6cb..5b519e131 100644 --- a/src/NJsonSchema/Generation/SampleJsonDataGenerator.cs +++ b/src/NJsonSchema/Generation/SampleJsonDataGenerator.cs @@ -7,10 +7,8 @@ //----------------------------------------------------------------------- using Newtonsoft.Json.Linq; -using NJsonSchema; using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; namespace NJsonSchema.Generation @@ -133,19 +131,19 @@ private JToken Generate(JsonSchema schema, Stack schemaStack) } } - private JToken HandleNumberType(JsonSchema schema) + private static JToken HandleNumberType(JsonSchema schema) { if (schema.ExclusiveMinimumRaw?.Equals(true) == true && schema.Minimum != null) { - return JToken.FromObject(decimal.Parse(schema.Minimum.Value.ToString(CultureInfo.InvariantCulture)) + 0.1m); + return JToken.FromObject(schema.Minimum.Value + 0.1m); } else if (schema.ExclusiveMinimum != null) { - return JToken.FromObject(decimal.Parse(schema.ExclusiveMinimum.Value.ToString(CultureInfo.InvariantCulture))); + return JToken.FromObject(schema.ExclusiveMinimum.Value); } else if (schema.Minimum.HasValue) { - return decimal.Parse(schema.Minimum.ToString()!); + return schema.Minimum.Value; } return JToken.FromObject(0.0); } @@ -167,7 +165,7 @@ private JToken HandleIntegerType(JsonSchema schema) return JToken.FromObject(0); } - private JToken HandleStringType(JsonSchema schema, JsonSchemaProperty? property) + private static JToken HandleStringType(JsonSchema schema, JsonSchemaProperty? property) { if (schema.Format == JsonFormatStrings.Date) {