diff --git a/.gitignore b/.gitignore index 0e121215810..41ec32f22cb 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ docs/api *.cache .settings .DS_Store +.idea launchSettings.json package-lock.json *.sublime-workspace diff --git a/src/Microsoft.DocAsCode.Build.Common/MarkdownReader.cs b/src/Microsoft.DocAsCode.Build.Common/MarkdownReader.cs index a04fb0ef237..d477d28dab0 100644 --- a/src/Microsoft.DocAsCode.Build.Common/MarkdownReader.cs +++ b/src/Microsoft.DocAsCode.Build.Common/MarkdownReader.cs @@ -128,7 +128,7 @@ private static Dictionary RemoveRequiredProperties(ImmutableDict private static bool CheckRequiredProperties(ImmutableDictionary properties, IEnumerable requiredKeys, out string message) { var notExistsKeys = (from key in requiredKeys - where !properties.Keys.Contains(key) + where !properties.ContainsKey(key) select key).ToList(); if (notExistsKeys.Count > 0) { diff --git a/src/Microsoft.DocAsCode.Build.ManagedReference/BuildOutputs/ApiBuildOutput.cs b/src/Microsoft.DocAsCode.Build.ManagedReference/BuildOutputs/ApiBuildOutput.cs index 8b78c81f80b..13dff1f0ea0 100644 --- a/src/Microsoft.DocAsCode.Build.ManagedReference/BuildOutputs/ApiBuildOutput.cs +++ b/src/Microsoft.DocAsCode.Build.ManagedReference/BuildOutputs/ApiBuildOutput.cs @@ -242,7 +242,7 @@ private static ApiBuildOutput FromModel(ItemViewModel model, Dictionary !model.Metadata.Keys.Contains(p.Key))).ToDictionary(p => p.Key, p => p.Value), + Metadata = model.Metadata.Concat(metadata.Where(p => !model.Metadata.ContainsKey(p.Key))).ToDictionary(p => p.Key, p => p.Value), }; output.DerivedClasses = GetReferenceList(model.DerivedClasses, references, model.SupportedLanguages, true, output.Level + 1); return output; diff --git a/src/Microsoft.DocAsCode.Build.OverwriteDocuments/OverwriteDocumentModelCreater.cs b/src/Microsoft.DocAsCode.Build.OverwriteDocuments/OverwriteDocumentModelCreater.cs index d3b8854805f..c17e3e49686 100644 --- a/src/Microsoft.DocAsCode.Build.OverwriteDocuments/OverwriteDocumentModelCreater.cs +++ b/src/Microsoft.DocAsCode.Build.OverwriteDocuments/OverwriteDocumentModelCreater.cs @@ -52,7 +52,7 @@ internal static Dictionary ConvertYamlCodeBlock(string yamlCodeB catch (Exception ex) { throw new MarkdownFragmentsException( - $"Encountered an invalid YAML code block: {ex.Message}", + $"Encountered an invalid YAML code block: {ex.ToString()}", yamlCodeBlockSource.Line, ex); } @@ -116,8 +116,8 @@ private void FindOrCreateObject(Dictionary currentObject, Block { object value; var goodItems = (from item in listObject - where item is Dictionary - && ((Dictionary)item).TryGetValue(segment.Key, out value) + where item is Dictionary + && ((Dictionary)item).TryGetValue(segment.Key, out value) && ((string)value).Equals(segment.Value) select (Dictionary)item).ToList(); if (goodItems.Count > 0) diff --git a/src/Microsoft.DocAsCode.Build.TableOfContents/TocHelper.cs b/src/Microsoft.DocAsCode.Build.TableOfContents/TocHelper.cs index acbc9c3b820..903a1f9ab99 100644 --- a/src/Microsoft.DocAsCode.Build.TableOfContents/TocHelper.cs +++ b/src/Microsoft.DocAsCode.Build.TableOfContents/TocHelper.cs @@ -111,7 +111,7 @@ public static TocItemViewModel LoadYamlToc(string file) } catch (Exception ex) { - throw new NotSupportedException($"{file} is not a valid TOC file, detail: {ex.Message}.", ex); + throw new NotSupportedException($"{file} is not a valid TOC file, detail: {ex.ToString()}.", ex); } if (obj is TocViewModel vm) { diff --git a/src/Microsoft.DocAsCode.Build.UniversalReference/ModelConverter.cs b/src/Microsoft.DocAsCode.Build.UniversalReference/ModelConverter.cs index 11a82fd2465..dcd369fe7f6 100644 --- a/src/Microsoft.DocAsCode.Build.UniversalReference/ModelConverter.cs +++ b/src/Microsoft.DocAsCode.Build.UniversalReference/ModelConverter.cs @@ -256,7 +256,7 @@ ApiBuildOutput ToItemApiBuildOutput(ItemViewModel src) Conceptual = src.Conceptual, Platform = ToApiListInDevLangs(src.Platform, src.PlatformInDevLangs, supportedLanguages), - Metadata = model.Metadata?.Concat(src.Metadata.Where(p => !model.Metadata.Keys.Contains(p.Key))).ToDictionary(p => p.Key, p => p.Value) ?? src.Metadata, + Metadata = model.Metadata?.Concat(src.Metadata.Where(p => !model.Metadata.ContainsKey(p.Key))).ToDictionary(p => p.Key, p => p.Value) ?? src.Metadata, }; } diff --git a/src/Microsoft.DocAsCode.YamlSerialization/Microsoft.DocAsCode.YamlSerialization.csproj b/src/Microsoft.DocAsCode.YamlSerialization/Microsoft.DocAsCode.YamlSerialization.csproj index df6645cfa97..d5e9dbd99a0 100644 --- a/src/Microsoft.DocAsCode.YamlSerialization/Microsoft.DocAsCode.YamlSerialization.csproj +++ b/src/Microsoft.DocAsCode.YamlSerialization/Microsoft.DocAsCode.YamlSerialization.csproj @@ -1,5 +1,5 @@  - + diff --git a/src/Microsoft.DocAsCode.YamlSerialization/ObjectGraphVisitors/ExclusiveObjectGraphVisitor.cs b/src/Microsoft.DocAsCode.YamlSerialization/ObjectGraphVisitors/ExclusiveObjectGraphVisitor.cs new file mode 100644 index 00000000000..823f6342bfc --- /dev/null +++ b/src/Microsoft.DocAsCode.YamlSerialization/ObjectGraphVisitors/ExclusiveObjectGraphVisitor.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.DocAsCode.YamlSerialization.ObjectGraphVisitors +{ + using System; + using System.ComponentModel; + using YamlDotNet.Core; + using YamlDotNet.Serialization; + using YamlDotNet.Serialization.ObjectGraphVisitors; + + /// + /// YamlDotNet behavior has changed since 6.x so a custom version which doesn't check on EnterMapping(IObjectDescriptor). + /// + internal sealed class ExclusiveObjectGraphVisitor : ChainedObjectGraphVisitor + { + public ExclusiveObjectGraphVisitor(IObjectGraphVisitor nextVisitor) + : base(nextVisitor) + { + } + + private static object GetDefault(Type type) + { + return type.IsValueType ? Activator.CreateInstance(type) : null; + } + + public override bool EnterMapping(IPropertyDescriptor key, IObjectDescriptor value, IEmitter context) + { + var defaultValueAttribute = key.GetCustomAttribute(); + object defaultValue = defaultValueAttribute != null + ? defaultValueAttribute.Value + : GetDefault(key.Type); + + return !Equals(value.Value, defaultValue) && base.EnterMapping(key, value, context); + } + } +} diff --git a/src/Microsoft.DocAsCode.YamlSerialization/SerializationOptions.cs b/src/Microsoft.DocAsCode.YamlSerialization/SerializationOptions.cs new file mode 100644 index 00000000000..9e840ee27c9 --- /dev/null +++ b/src/Microsoft.DocAsCode.YamlSerialization/SerializationOptions.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.DocAsCode.YamlSerialization +{ + using System; + + /// Options that control the serialization process. + [Flags] + public enum SerializationOptions + { + /// Serializes using the default options + None = 0, + /// + /// Ensures that it will be possible to deserialize the serialized objects. + /// + Roundtrip = 1, + /// + /// If this flag is specified, if the same object appears more than once in the + /// serialization graph, it will be serialized each time instead of just once. + /// + /// + /// If the serialization graph contains circular references and this flag is set, + /// a StackOverflowException will be thrown. + /// If this flag is not set, there is a performance penalty because the entire + /// object graph must be walked twice. + /// + DisableAliases = 2, + /// + /// Forces every value to be serialized, even if it is the default value for that type. + /// + EmitDefaults = 4, + /// + /// Ensures that the result of the serialization is valid JSON. + /// + JsonCompatible = 8, + /// + /// Use the static type of values instead of their actual type. + /// + DefaultToStaticType = 16, // 0x00000010 + } +} diff --git a/src/Microsoft.DocAsCode.YamlSerialization/TypeInspectors/EmitTypeInspector.cs b/src/Microsoft.DocAsCode.YamlSerialization/TypeInspectors/EmitTypeInspector.cs index b4f14322459..cd01594b2d3 100644 --- a/src/Microsoft.DocAsCode.YamlSerialization/TypeInspectors/EmitTypeInspector.cs +++ b/src/Microsoft.DocAsCode.YamlSerialization/TypeInspectors/EmitTypeInspector.cs @@ -18,8 +18,8 @@ namespace Microsoft.DocAsCode.YamlSerialization.TypeInspectors public class EmitTypeInspector : ExtensibleTypeInspectorSkeleton { - private static readonly ConcurrentDictionary _cache = - new ConcurrentDictionary(); + private static readonly ConcurrentDictionary _cache = new(); + private static readonly ConcurrentDictionary> _propertyDescriptorCache = new(); private readonly ITypeResolver _resolver; public EmitTypeInspector(ITypeResolver resolver) @@ -34,17 +34,22 @@ public override IEnumerable GetProperties(Type type, object { throw item.Error; } - var result = from p in item.Properies select (IPropertyDescriptor)new EmitPropertyDescriptor(p, _resolver); if (container == null || item.ExtensibleProperies.Count == 0) { - return result; + // all static information + return _propertyDescriptorCache.GetOrAdd(type, GetPropertyDescriptors(item).ToList()); } - return result.Concat( + return GetPropertyDescriptors(item).Concat( from ep in item.ExtensibleProperies from key in ep.GetAllKeys(container) ?? Enumerable.Empty() select new ExtensiblePropertyDescriptor(ep, ep.Prefix + key, _resolver)); } + private IEnumerable GetPropertyDescriptors(CachingItem item) + { + return from p in item.Properies select new EmitPropertyDescriptor(p, _resolver); + } + public override IPropertyDescriptor GetProperty(Type type, object container, string name) { var item = _cache.GetOrAdd(type, CachingItem.Create); diff --git a/src/Microsoft.DocAsCode.YamlSerialization/YamlDeserializer.cs b/src/Microsoft.DocAsCode.YamlSerialization/YamlDeserializer.cs index cea35d59896..538fe807031 100644 --- a/src/Microsoft.DocAsCode.YamlSerialization/YamlDeserializer.cs +++ b/src/Microsoft.DocAsCode.YamlSerialization/YamlDeserializer.cs @@ -28,21 +28,19 @@ namespace Microsoft.DocAsCode.YamlSerialization /// public sealed class YamlDeserializer { - private static Dictionary PredefinedTagMappings { get; } = - new Dictionary - { - { "tag:yaml.org,2002:map", typeof(Dictionary) }, - { "tag:yaml.org,2002:bool", typeof(bool) }, - { "tag:yaml.org,2002:float", typeof(double) }, - { "tag:yaml.org,2002:int", typeof(int) }, - { "tag:yaml.org,2002:str", typeof(string) }, - { "tag:yaml.org,2002:timestamp", typeof(DateTime) }, - }; - - private readonly Dictionary _tagMappings; + private static Dictionary PredefinedTagMappings { get; } = new Dictionary + { + { "tag:yaml.org,2002:map", typeof(Dictionary) }, + { "tag:yaml.org,2002:bool", typeof(bool) }, + { "tag:yaml.org,2002:float", typeof(double) }, + { "tag:yaml.org,2002:int", typeof(int) }, + { "tag:yaml.org,2002:str", typeof(string) }, + { "tag:yaml.org,2002:timestamp", typeof(DateTime) }, + }; + + private readonly Dictionary _tagMappings; private readonly List _converters; - private readonly TypeDescriptorProxy _typeDescriptor = - new TypeDescriptorProxy(); + private readonly TypeDescriptorProxy _typeDescriptor = new TypeDescriptorProxy(); private readonly IValueDeserializer _valueDeserializer; public IList NodeDeserializers { get; private set; } @@ -103,7 +101,7 @@ public YamlDeserializer( new EnumerableNodeDeserializer(), new ExtensibleObjectNodeDeserializer(objectFactory, _typeDescriptor, ignoreUnmatched) }; - _tagMappings = new Dictionary(PredefinedTagMappings); + _tagMappings = new Dictionary(PredefinedTagMappings); TypeResolvers = new List { new TagNodeTypeResolver(_tagMappings), @@ -219,7 +217,7 @@ public LooseAliasValueDeserializer(IValueDeserializer innerDeserializer) _innerDeserializer = innerDeserializer ?? throw new ArgumentNullException("innerDeserializer"); } - private sealed class AliasState : Dictionary, IPostDeserializationCallback + private sealed class AliasState : Dictionary, IPostDeserializationCallback { public void OnDeserialization() { @@ -295,10 +293,10 @@ public object DeserializeValue(IParser reader, Type expectedType, SerializerStat return valuePromise.HasValue ? valuePromise.Value : valuePromise; } - string anchor = null; + AnchorName? anchor = null; var nodeEvent = reader.Peek(); - if (nodeEvent != null && !string.IsNullOrEmpty(nodeEvent.Anchor)) + if (nodeEvent != null && !nodeEvent.Anchor.IsEmpty) { anchor = nodeEvent.Anchor; } @@ -309,9 +307,9 @@ public object DeserializeValue(IParser reader, Type expectedType, SerializerStat { var aliasState = state.Get(); - if (!aliasState.TryGetValue(anchor, out ValuePromise valuePromise)) + if (!aliasState.TryGetValue(anchor.Value, out ValuePromise valuePromise)) { - aliasState.Add(anchor, new ValuePromise(value)); + aliasState.Add(anchor.Value, new ValuePromise(value)); } else if (!valuePromise.HasValue) { @@ -319,7 +317,7 @@ public object DeserializeValue(IParser reader, Type expectedType, SerializerStat } else { - aliasState[anchor] = new ValuePromise(value); + aliasState[anchor.Value] = new ValuePromise(value); } } @@ -327,4 +325,4 @@ public object DeserializeValue(IParser reader, Type expectedType, SerializerStat } } } -} \ No newline at end of file +} diff --git a/src/Microsoft.DocAsCode.YamlSerialization/YamlSerializer.cs b/src/Microsoft.DocAsCode.YamlSerialization/YamlSerializer.cs index 3189f5a4111..2e1ce116b4c 100644 --- a/src/Microsoft.DocAsCode.YamlSerialization/YamlSerializer.cs +++ b/src/Microsoft.DocAsCode.YamlSerialization/YamlSerializer.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using Microsoft.DocAsCode.YamlSerialization.ObjectGraphVisitors; + namespace Microsoft.DocAsCode.YamlSerialization { using System; @@ -90,14 +92,14 @@ private IObjectGraphVisitor CreateEmittingVisitor(IEmitter emitter, IO if (!IsOptionSet(SerializationOptions.DisableAliases)) { var anchorAssigner = new AnchorAssigner(Converters); - traversalStrategy.Traverse(graph, anchorAssigner, null); + traversalStrategy.Traverse(graph, anchorAssigner, default); emittingVisitor = new AnchorAssigningObjectGraphVisitor(emittingVisitor, eventEmitter, anchorAssigner); } if (!IsOptionSet(SerializationOptions.EmitDefaults)) { - emittingVisitor = new DefaultExclusiveObjectGraphVisitor(emittingVisitor); + emittingVisitor = new ExclusiveObjectGraphVisitor(emittingVisitor); } return emittingVisitor; @@ -130,7 +132,7 @@ private IEventEmitter CreateEventEmitter() } else { - return new TypeAssigningEventEmitter(writer, IsOptionSet(SerializationOptions.Roundtrip), new Dictionary()); + return new TypeAssigningEventEmitter(writer, IsOptionSet(SerializationOptions.Roundtrip), new Dictionary()); } } @@ -156,3 +158,4 @@ private IObjectGraphTraversalStrategy CreateTraversalStrategy() } } } + diff --git a/test/Microsoft.DocAsCode.Build.TableOfContents.Tests/TocDocumentProcessorTest.cs b/test/Microsoft.DocAsCode.Build.TableOfContents.Tests/TocDocumentProcessorTest.cs index 7ef39645b91..1e7ed264533 100644 --- a/test/Microsoft.DocAsCode.Build.TableOfContents.Tests/TocDocumentProcessorTest.cs +++ b/test/Microsoft.DocAsCode.Build.TableOfContents.Tests/TocDocumentProcessorTest.cs @@ -771,7 +771,7 @@ public void LoadBadTocYamlFileShouldGiveLineNumber() href: x2.md"; var toc = _fileCreator.CreateFile(content, FileType.YamlToc); var ex = Assert.Throws(() => TocHelper.LoadSingleToc(toc)); - Assert.Equal("toc.yml is not a valid TOC File: toc.yml is not a valid TOC file, detail: (Line: 3, Col: 10, Idx: 22) - (Line: 3, Col: 10, Idx: 22): Mapping values are not allowed in this context..", ex.Message); + Assert.Equal("toc.yml is not a valid TOC File: toc.yml is not a valid TOC file, detail: (Line: 3, Col: 10, Idx: 22) - (Line: 3, Col: 10, Idx: 22): While scanning a plain scalar value, found invalid mapping..", ex.Message); } [Fact]