public override void VisitMarkupElement(MarkupElementIntermediateNode node) { // Disallow <script> in components as per #552 if (string.Equals(node.TagName, "script", StringComparison.OrdinalIgnoreCase)) { for (var i = 0; i < node.Children.Count; i++) { // We allow you to suppress this error like: // <script suppress-error="BL9992" /> var attribute = node.Children[i] as HtmlAttributeIntermediateNode; if (attribute != null && attribute.AttributeName == "suppress-error" && attribute.Children.Count == 1 && attribute.Children[0] is HtmlAttributeValueIntermediateNode value && value.Children.Count == 1 && value.Children[0] is IntermediateToken token && token.IsHtml && string.Equals(token.Content, "BL9992", StringComparison.Ordinal)) { node.Children.RemoveAt(i); return; } } var diagnostic = ComponentDiagnosticFactory.Create_DisallowedScriptTag(node.Source); node.Diagnostics.Add(diagnostic); } base.VisitDefault(node); }
private static IReadOnlyList <IntermediateToken> GetAttributeContent(IntermediateNode node) { var nodes = node.FindDescendantNodes <TemplateIntermediateNode>(); var template = nodes.Count > 0 ? nodes[0] : default; if (template != null) { // See comments in TemplateDiagnosticPass node.Diagnostics.Add(ComponentDiagnosticFactory.Create_TemplateInvalidLocation(template.Source)); return(new[] { new IntermediateToken() { Kind = TokenKind.CSharp, Content = string.Empty, }, }); } if (node.Children.Count == 1 && node.Children[0] is HtmlContentIntermediateNode htmlContentNode) { // This case can be hit for a 'string' attribute. We want to turn it into // an expression. var tokens = htmlContentNode.FindDescendantNodes <IntermediateToken>(); var content = "\"" + string.Join(string.Empty, tokens.Select(t => t.Content.Replace("\"", "\\\""))) + "\""; return(new[] { new IntermediateToken() { Content = content, Kind = TokenKind.CSharp, } }); } else { return(node.FindDescendantNodes <IntermediateToken>()); } }
public override void VisitComponentChildContent(ComponentChildContentIntermediateNode node) { // Check that each child content has a unique parameter name within its scope. This is important // because the parameter name can be implicit, and it doesn't work well when nested. if (node.IsParameterized) { for (var i = 0; i < Ancestors.Count - 1; i++) { var ancestor = Ancestors[i] as ComponentChildContentIntermediateNode; if (ancestor != null && ancestor.IsParameterized && string.Equals(node.ParameterName, ancestor.ParameterName, StringComparison.Ordinal)) { // Duplicate name. We report an error because this will almost certainly also lead to an error // from the C# compiler that's way less clear. node.Diagnostics.Add(ComponentDiagnosticFactory.Create_ChildContentRepeatedParameterName( node.Source, node, (ComponentIntermediateNode)Ancestors[0], // Enclosing component ancestor, // conflicting child content node (ComponentIntermediateNode)Ancestors[i + 1])); // Enclosing component of conflicting child content node } } } base.VisitDefault(node); }
protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode) { if (!IsComponentDocument(documentNode)) { return; } var @namespace = documentNode.FindPrimaryNamespace(); var @class = documentNode.FindPrimaryClass(); if (@namespace == null || @class == null) { // Nothing to do, bail. We can't function without the standard structure. return; } // For each component *usage* we need to rewrite the tag helper node to map to the relevant component // APIs. var references = documentNode.FindDescendantReferences <TagHelperIntermediateNode>(); for (var i = 0; i < references.Count; i++) { var reference = references[i]; var node = (TagHelperIntermediateNode)reference.Node; if (node.TagHelpers.Any(t => t.IsChildContentTagHelper())) { // This is a child content tag helper. This will be rewritten when we visit its parent. continue; } // The element didn't match any child content descriptors. Look for any matching component descriptors. var count = 0; for (var j = 0; j < node.TagHelpers.Count; j++) { if (node.TagHelpers[j].IsComponentTagHelper()) { // Only allow a single component tag helper per element. If there are multiple, we'll just consider // the first one and ignore the others. if (++count > 1) { node.Diagnostics.Add(ComponentDiagnosticFactory.Create_MultipleComponents(node.Source, node.TagName, node.TagHelpers)); break; } } } if (count >= 1) { reference.Replace(RewriteAsComponent(node, node.TagHelpers.First(t => t.IsComponentTagHelper()))); } else { reference.Replace(RewriteAsElement(node)); } } }
public sealed override void WriteCSharpCodeAttributeValue(CodeRenderingContext context, CSharpCodeAttributeValueIntermediateNode node) { // We used to support syntaxes like <elem onsomeevent=@{ /* some C# code */ } /> but this is no longer the // case. // // We provide an error for this case just to be friendly. var content = string.Join("", node.Children.OfType <IntermediateToken>().Select(t => t.Content)); context.Diagnostics.Add(ComponentDiagnosticFactory.Create_CodeBlockInAttribute(node.Source, content)); return; }
public override void VisitComponent(ComponentIntermediateNode node) { // Check for properties that are set by both element contents (body) and the attribute itself. foreach (var childContent in node.ChildContents) { foreach (var attribute in node.Attributes) { if (attribute.AttributeName == childContent.AttributeName) { node.Diagnostics.Add(ComponentDiagnosticFactory.Create_ChildContentSetByAttributeAndBody( attribute.Source, attribute.AttributeName)); } } } base.VisitDefault(node); }
public override void VisitComponent(ComponentIntermediateNode node) { for (var i = 0; i < node.Children.Count; i++) { // Note that we don't handle ChildContent cases here. Those have their own pass for diagnostics. if (node.Children[i] is ComponentAttributeIntermediateNode attribute && attribute.AttributeName != null) { if (_attributes.TryGetValue(attribute.AttributeName, out var other)) { // As a special case we want to point it out explicitly where a directive or other construct // has emitted an attribute that causes a conflict. We're already looking at the lowered version // of this construct, so it's easy to detect. We just need the original name to report the issue. // // Example: `bind-Value` will set `Value` and `ValueChanged`. var originalAttributeName = attribute.Annotations[ComponentMetadata.Common.OriginalAttributeName] as string ?? other.node.Annotations[ComponentMetadata.Common.OriginalAttributeName] as string; if (originalAttributeName != null) { other.node.Diagnostics.Add(ComponentDiagnosticFactory.Create_DuplicateComponentParameterDirective( other.name, originalAttributeName, other.node.Source ?? node.Source)); } else { // This is a conflict in the code the user wrote. other.node.Diagnostics.Add(ComponentDiagnosticFactory.Create_DuplicateComponentParameter( other.name, other.node.Source ?? node.Source)); } } // Replace the attribute we were previously tracking. Then if you have three, the two on the left will have // diagnostics. _attributes[attribute.AttributeName] = (attribute.AttributeName, attribute); } } _attributes.Clear(); base.VisitComponent(node); }
protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode) { if (!IsComponentDocument(documentNode)) { return; } var visitor = new Visitor(); visitor.Visit(documentNode); for (var i = 0; i < visitor.Candidates.Count; i++) { var candidate = visitor.Candidates[i]; candidate.Parent.Diagnostics.Add(ComponentDiagnosticFactory.Create_TemplateInvalidLocation(candidate.Node.Source)); // Remove the offending node since we don't know how to render it. This means that the user won't get C# // completion at this location, which is fine because it's inside an HTML attribute. candidate.Remove(); } }
private void ProcessAttributes(TagHelperIntermediateNode node) { for (var i = node.Children.Count - 1; i >= 0; i--) { if (node.Children[i] is TagHelperPropertyIntermediateNode propertyNode) { if (!TrySimplifyContent(propertyNode) && node.TagHelpers.Any(t => t.IsComponentTagHelper())) { node.Diagnostics.Add(ComponentDiagnosticFactory.Create_UnsupportedComplexContent( propertyNode, propertyNode.AttributeName)); node.Children.RemoveAt(i); continue; } } else if (node.Children[i] is TagHelperHtmlAttributeIntermediateNode htmlNode) { if (!TrySimplifyContent(htmlNode) && node.TagHelpers.Any(t => t.IsComponentTagHelper())) { node.Diagnostics.Add(ComponentDiagnosticFactory.Create_UnsupportedComplexContent( htmlNode, htmlNode.AttributeName)); node.Children.RemoveAt(i); continue; } } else if (node.Children[i] is TagHelperDirectiveAttributeIntermediateNode directiveAttributeNode) { if (!TrySimplifyContent(directiveAttributeNode)) { node.Diagnostics.Add(ComponentDiagnosticFactory.Create_UnsupportedComplexContent( directiveAttributeNode, directiveAttributeNode.OriginalAttributeName)); node.Children.RemoveAt(i); continue; } } } }
private static bool ValidateTypeArguments(ComponentIntermediateNode node, Dictionary <string, Binding> bindings) { var missing = new List <BoundAttributeDescriptor>(); foreach (var binding in bindings) { if (binding.Value.Node == null || string.IsNullOrWhiteSpace(binding.Value.Content)) { missing.Add(binding.Value.Attribute); } } if (missing.Count > 0) { // We add our own error for this because its likely the user will see other errors due // to incorrect codegen without the types. Our errors message will pretty clearly indicate // what to do, whereas the other errors might be confusing. node.Diagnostics.Add(ComponentDiagnosticFactory.Create_GenericComponentMissingTypeArgument(node.Source, node, missing)); return(false); } return(true); }
public override void VisitRazorDirective(RazorDirectiveSyntax node) { var descendantLiterals = node.DescendantNodes(); foreach (var child in descendantLiterals) { if (child is not CSharpStatementLiteralSyntax literal) { continue; } var context = literal.GetSpanContext(); if (context == null) { // We can't find a chunk generator. continue; } else if (context.ChunkGenerator is AddTagHelperChunkGenerator addTagHelper) { // Make sure this node exists in the file we're parsing and not in its imports. if (_filePath.Equals(_source.FilePath, StringComparison.Ordinal)) { addTagHelper.Diagnostics.Add( ComponentDiagnosticFactory.Create_UnsupportedTagHelperDirective(node.GetSourceSpan(_source))); } } else if (context.ChunkGenerator is RemoveTagHelperChunkGenerator removeTagHelper) { // Make sure this node exists in the file we're parsing and not in its imports. if (_filePath.Equals(_source.FilePath, StringComparison.Ordinal)) { removeTagHelper.Diagnostics.Add( ComponentDiagnosticFactory.Create_UnsupportedTagHelperDirective(node.GetSourceSpan(_source))); } } else if (context.ChunkGenerator is TagHelperPrefixDirectiveChunkGenerator tagHelperPrefix) { // Make sure this node exists in the file we're parsing and not in its imports. if (_filePath.Equals(_source.FilePath, StringComparison.Ordinal)) { tagHelperPrefix.Diagnostics.Add( ComponentDiagnosticFactory.Create_UnsupportedTagHelperDirective(node.GetSourceSpan(_source))); } } else if (context.ChunkGenerator is AddImportChunkGenerator usingStatement && !usingStatement.IsStatic) { // Get the namespace from the using statement. var @namespace = usingStatement.ParsedNamespace; if (@namespace.IndexOf('=') != -1) { // We don't support usings with alias. continue; } for (var i = 0; _notFullyQualifiedComponents is not null && i < _notFullyQualifiedComponents.Count; i++) { var tagHelper = _notFullyQualifiedComponents[i]; Debug.Assert(!tagHelper.IsComponentFullyQualifiedNameMatch(), "We've already processed these."); if (tagHelper.IsChildContentTagHelper()) { // If this is a child content tag helper, we want to add it if it's original type is in scope of the given namespace. // E.g, if the type name is `Test.MyComponent.ChildContent`, we want to add it if `Test.MyComponent` is in this namespace. TrySplitNamespaceAndType(tagHelper, out var typeName); if (!typeName.IsEmpty && IsTypeInNamespace(typeName, @namespace)) { Matches.Add(tagHelper); } } else if (IsTypeInNamespace(tagHelper, @namespace)) { // If the type is at the top-level or if the type's namespace matches the using's namespace, add it. Matches.Add(tagHelper); } } } } }
private void ProcessDuplicates(IntermediateNode parent) { // Reverse order because we will remove nodes. // // Each 'property' node could be duplicated if there are multiple tag helpers that match that // particular attribute. This is likely to happen when a component also defines something like // OnClick. We want to remove the 'onclick' and let it fall back to be handled by the component. for (var i = parent.Children.Count - 1; i >= 0; i--) { var eventHandler = parent.Children[i] as TagHelperPropertyIntermediateNode; if (eventHandler != null && eventHandler.TagHelper != null && eventHandler.TagHelper.IsEventHandlerTagHelper()) { for (var j = 0; j < parent.Children.Count; j++) { var componentAttribute = parent.Children[j] as ComponentAttributeIntermediateNode; if (componentAttribute != null && componentAttribute.TagHelper != null && componentAttribute.TagHelper.IsComponentTagHelper() && componentAttribute.AttributeName == eventHandler.AttributeName) { // Found a duplicate - remove the 'fallback' in favor of the component's own handling. parent.Children.RemoveAt(i); break; } } } } // If we still have duplicates at this point then they are genuine conflicts. var duplicates = parent.Children .OfType <TagHelperDirectiveAttributeIntermediateNode>() .Where(p => p.TagHelper?.IsEventHandlerTagHelper() ?? false) .GroupBy(p => p.AttributeName) .Where(g => g.Count() > 1); foreach (var duplicate in duplicates) { parent.Diagnostics.Add(ComponentDiagnosticFactory.CreateEventHandler_Duplicates( parent.Source, duplicate.Key, duplicate.ToArray())); foreach (var property in duplicate) { parent.Children.Remove(property); } } var parameterDuplicates = parent.Children .OfType <TagHelperDirectiveAttributeParameterIntermediateNode>() .Where(p => p.TagHelper?.IsEventHandlerTagHelper() ?? false) .GroupBy(p => p.AttributeName) .Where(g => g.Count() > 1); foreach (var duplicate in parameterDuplicates) { parent.Diagnostics.Add(ComponentDiagnosticFactory.CreateEventHandlerParameter_Duplicates( parent.Source, duplicate.Key, duplicate.ToArray())); foreach (var property in duplicate) { parent.Children.Remove(property); } } }
/// <inheritdoc /> protected override void OnDocumentStructureCreated( RazorCodeDocument codeDocument, NamespaceDeclarationIntermediateNode @namespace, ClassDeclarationIntermediateNode @class, MethodDeclarationIntermediateNode method) { if (!codeDocument.TryComputeNamespace(fallbackToRootNamespace: true, out var computedNamespace) || !TryComputeClassName(codeDocument, out var computedClass)) { // If we can't compute a nice namespace (no relative path) then just generate something // mangled. computedNamespace = FallbackRootNamespace; var checksum = Checksum.BytesToString(codeDocument.Source.GetChecksum()); computedClass = $"AspNetCore_{checksum}"; } var documentNode = codeDocument.GetDocumentIntermediateNode(); if (char.IsLower(computedClass, 0)) { // We don't allow component names to start with a lowercase character. documentNode.Diagnostics.Add( ComponentDiagnosticFactory.Create_ComponentNamesCannotStartWithLowerCase(computedClass, documentNode.Source)); } if (MangleClassNames) { computedClass = ComponentMetadata.MangleClassName(computedClass); } @namespace.Content = computedNamespace; @class.ClassName = computedClass; @class.Modifiers.Clear(); @class.Modifiers.Add("public"); @class.Modifiers.Add("partial"); if (FileKinds.IsComponentImport(codeDocument.GetFileKind())) { // We don't want component imports to be considered as real component. // But we still want to generate code for it so we can get diagnostics. @class.BaseType = typeof(object).FullName; method.ReturnType = "void"; method.MethodName = "Execute"; method.Modifiers.Clear(); method.Modifiers.Add("protected"); method.Parameters.Clear(); } else { @class.BaseType = ComponentsApi.ComponentBase.FullTypeName; // Constrained type parameters are only supported in Razor language versions v6.0 var razorLanguageVersion = codeDocument.GetParserOptions()?.Version ?? RazorLanguageVersion.Latest; var directiveType = razorLanguageVersion.CompareTo(RazorLanguageVersion.Version_6_0) >= 0 ? ComponentConstrainedTypeParamDirective.Directive : ComponentTypeParamDirective.Directive; var typeParamReferences = documentNode.FindDirectiveReferences(directiveType); for (var i = 0; i < typeParamReferences.Count; i++) { var typeParamNode = (DirectiveIntermediateNode)typeParamReferences[i].Node; if (typeParamNode.HasDiagnostics) { continue; } @class.TypeParameters.Add(new TypeParameter() { ParameterName = typeParamNode.Tokens.First().Content, Constraints = typeParamNode.Tokens.Skip(1).FirstOrDefault()?.Content }); } method.ReturnType = "void"; method.MethodName = ComponentsApi.ComponentBase.BuildRenderTree; method.Modifiers.Clear(); method.Modifiers.Add("protected"); method.Modifiers.Add("override"); method.Parameters.Clear(); method.Parameters.Add(new MethodParameter() { ParameterName = ComponentsApi.RenderTreeBuilder.BuilderParameter, TypeName = ComponentsApi.RenderTreeBuilder.FullTypeName, }); } }
private void Process(ComponentIntermediateNode node) { // First collect all of the information we have about each type parameter // // Listing all type parameters that exist var bindings = new Dictionary <string, Binding>(); var componentTypeParameters = node.Component.GetTypeParameters().ToList(); var supplyCascadingTypeParameters = componentTypeParameters .Where(p => p.IsCascadingTypeParameterProperty()) .Select(p => p.Name) .ToList(); foreach (var attribute in componentTypeParameters) { bindings.Add(attribute.Name, new Binding() { Attribute = attribute, }); } // Listing all type arguments that have been specified. var hasTypeArgumentSpecified = false; foreach (var typeArgumentNode in node.TypeArguments) { hasTypeArgumentSpecified = true; var binding = bindings[typeArgumentNode.TypeParameterName]; binding.Node = typeArgumentNode; binding.Content = GetContent(typeArgumentNode); // Offer this explicit type argument to descendants too if (supplyCascadingTypeParameters.Contains(typeArgumentNode.TypeParameterName)) { node.ProvidesCascadingGenericTypes ??= new(); node.ProvidesCascadingGenericTypes[typeArgumentNode.TypeParameterName] = new CascadingGenericTypeParameter { GenericTypeNames = new[] { typeArgumentNode.TypeParameterName }, ValueType = typeArgumentNode.TypeParameterName, ValueExpression = $"default({binding.Content})", }; } } if (hasTypeArgumentSpecified) { // OK this means that the developer has specified at least one type parameter. // Either they specified everything and its OK to rewrite, or its an error. if (ValidateTypeArguments(node, bindings)) { var mappings = bindings.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Content); RewriteTypeNames(_pass.TypeNameFeature.CreateGenericTypeRewriter(mappings), node); } return; } // OK if we get here that means that no type arguments were specified, so we will try to infer // the type. // // The actual inference is done by the C# compiler, we just emit an a method that represents the // use of this component. // Since we're generating code in a different namespace, we need to 'global qualify' all of the types // to avoid clashes with our generated code. RewriteTypeNames(_pass.TypeNameFeature.CreateGlobalQualifiedTypeNameRewriter(bindings.Keys), node); // // We need to verify that an argument was provided that 'covers' each type parameter. // // For example, consider a repeater where the generic type is the 'item' type, but the developer has // not set the items. We won't be able to do type inference on this and so it will just be nonsense. foreach (var attribute in node.Attributes) { if (attribute != null && TryFindGenericTypeNames(attribute.BoundAttribute, out var typeParameters)) { var attributeValueIsLambda = _pass.TypeNameFeature.IsLambda(GetContent(attribute)); var provideCascadingGenericTypes = new CascadingGenericTypeParameter { GenericTypeNames = typeParameters, ValueType = attribute.BoundAttribute.TypeName, ValueSourceNode = attribute, }; foreach (var typeName in typeParameters) { if (supplyCascadingTypeParameters.Contains(typeName)) { // Advertise that this particular inferred generic type is available to descendants. // There might be multiple sources for each generic type, so pick the one that has the // fewest other generic types on it. For example if we could infer from either List<T> // or Dictionary<T, U>, we prefer List<T>. node.ProvidesCascadingGenericTypes ??= new(); if (!node.ProvidesCascadingGenericTypes.TryGetValue(typeName, out var existingValue) || existingValue.GenericTypeNames.Count > typeParameters.Count) { node.ProvidesCascadingGenericTypes[typeName] = provideCascadingGenericTypes; } } if (attributeValueIsLambda) { // For attributes whose values are lambdas, we don't know whether or not the value // covers the generic type - it depends on the content of the lambda. // For example, "() => 123" can cover Func<T>, but "() => null" cannot. So we'll // accept cascaded generic types from ancestors if they are compatible with the lambda, // hence we don't remove it from the list of uncovered generic types until after // we try matching against ancestor cascades. if (bindings.TryGetValue(typeName, out var binding)) { binding.CoveredByLambda = true; } } else { bindings.Remove(typeName); } } } } // For any remaining bindings, scan up the hierarchy of ancestor components and try to match them // with a cascaded generic parameter that can cover this one List <CascadingGenericTypeParameter> receivesCascadingGenericTypes = null; foreach (var uncoveredBindingKey in bindings.Keys.ToList()) { var uncoveredBinding = bindings[uncoveredBindingKey]; foreach (var candidateAncestor in Ancestors.OfType <ComponentIntermediateNode>()) { if (candidateAncestor.ProvidesCascadingGenericTypes != null && candidateAncestor.ProvidesCascadingGenericTypes.TryGetValue(uncoveredBindingKey, out var genericTypeProvider)) { // If the parameter value is an expression that includes multiple generic types, we only want // to use it if we want *all* those generic types. That is, a parameter of type MyType<T0, T1> // can supply types to a Child<T0, T1>, but not to a Child<T0>. // This is purely to avoid blowing up the complexity of the implementation here and could be // overcome in the future if we want. We'd need to figure out which extra types are unwanted, // and rewrite them to some unique name, and add that to the generic parameters list of the // inference methods. if (genericTypeProvider.GenericTypeNames.All(GenericTypeIsUsed)) { bindings.Remove(uncoveredBindingKey); receivesCascadingGenericTypes ??= new(); receivesCascadingGenericTypes.Add(genericTypeProvider); // It's sufficient to identify the closest provider for each type parameter break; } bool GenericTypeIsUsed(string typeName) => componentTypeParameters .Select(t => t.Name) .Contains(typeName, StringComparer.Ordinal); } } } // There are two remaining sources of possible generic type info which we consider // lower-priority than cascades from ancestors. Since these two sources *may* actually // resolve generic type ambiguities in some cases, we treat them as covering. // // [1] Attributes given as lambda expressions. These are lower priority than ancestor // cascades because in most cases, lambdas don't provide type info foreach (var entryToRemove in bindings.Where(e => e.Value.CoveredByLambda).ToList()) { // Treat this binding as covered, because it's possible that the lambda does provide // enough info for type inference to succeed. bindings.Remove(entryToRemove.Key); } // [2] Child content parameters, which are nearly always defined as untyped lambdas // (at least, that's what the Razor compiler produces), but can technically be // hardcoded as a RenderFragment<Something> and hence actually give type info. foreach (var attribute in node.ChildContents) { if (TryFindGenericTypeNames(attribute.BoundAttribute, out var typeParameters)) { foreach (var typeName in typeParameters) { bindings.Remove(typeName); } } } // If any bindings remain then this means we would never be able to infer the arguments of this // component usage because the user hasn't set properties that include all of the types. if (bindings.Count > 0) { // However we still want to generate 'type inference' code because we want the errors to be as // helpful as possible. So let's substitute 'object' for all of those type parameters, and add // an error. var mappings = bindings.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Content); RewriteTypeNames(_pass.TypeNameFeature.CreateGenericTypeRewriter(mappings), node); node.Diagnostics.Add(ComponentDiagnosticFactory.Create_GenericComponentTypeInferenceUnderspecified(node.Source, node, node.Component.GetTypeParameters())); } // Next we need to generate a type inference 'method' node. This represents a method that we will codegen that // contains all of the operations on the render tree building. Calling a method to operate on the builder // will allow the C# compiler to perform type inference. var documentNode = (DocumentIntermediateNode)Ancestors[Ancestors.Count - 1]; CreateTypeInferenceMethod(documentNode, node, receivesCascadingGenericTypes); }
protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode) { if (codeDocument == null) { throw new ArgumentNullException(nameof(codeDocument)); } if (documentNode == null) { throw new ArgumentNullException(nameof(documentNode)); } var @namespace = documentNode.FindPrimaryNamespace(); var @class = documentNode.FindPrimaryClass(); if (@namespace == null || @class == null) { return; } var directives = documentNode.FindDirectiveReferences(ComponentPageDirective.Directive); if (directives.Count == 0) { return; } // We don't allow @page directives in imports for (var i = 0; i < directives.Count; i++) { var directive = directives[i]; if (FileKinds.IsComponentImport(codeDocument.GetFileKind()) || directive.Node.IsImported()) { directive.Node.Diagnostics.Add(ComponentDiagnosticFactory.CreatePageDirective_CannotBeImported(directive.Node.Source.Value)); } } // Insert the attributes 'on-top' of the class declaration, since classes don't directly support attributes. var index = 0; for (; index < @namespace.Children.Count; index++) { if (object.ReferenceEquals(@class, @namespace.Children[index])) { break; } } for (var i = 0; i < directives.Count; i++) { var pageDirective = (DirectiveIntermediateNode)directives[i].Node; // The parser also adds errors for invalid syntax, we just need to not crash. var routeToken = pageDirective.Tokens.FirstOrDefault(); if (routeToken != null && routeToken.Content.Length >= 3 && routeToken.Content[0] == '\"' && routeToken.Content[1] == '/' && routeToken.Content[routeToken.Content.Length - 1] == '\"') { var template = new StringSegment(routeToken.Content, 1, routeToken.Content.Length - 2); @namespace.Children.Insert(index++, new RouteAttributeExtensionNode(template)); } else { pageDirective.Diagnostics.Add(ComponentDiagnosticFactory.CreatePageDirective_MustSpecifyRoute(pageDirective.Source)); } } }