Skip to content

Commit e027239

Browse files
authored
Provide a tooling gesture that suggests values for required component parameters are not specified. (#33384)
* Provide a tooling gesture that suggests values for required component parameters are not specified. Fixes #11815
1 parent 6d4b5b7 commit e027239

File tree

70 files changed

+1487
-12
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

70 files changed

+1487
-12
lines changed

src/Components/Components/src/DynamicComponent.cs

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public DynamicComponent()
3232
/// </summary>
3333
[Parameter]
3434
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
35+
[EditorRequired]
3536
public Type Type { get; set; } = default!;
3637

3738
/// <summary>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
6+
namespace Microsoft.AspNetCore.Components
7+
{
8+
/// <summary>
9+
/// Specifies that the component parameter is required to be provided by the user when authoring it in the editor.
10+
/// <para>
11+
/// If a value for this parameter is not provided, editors or build tools may provide warnings indicating the user to
12+
/// specify a value. This attribute is only valid on properties marked with <see cref="ParameterAttribute"/>.
13+
/// </para>
14+
/// </summary>
15+
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
16+
public sealed class EditorRequiredAttribute : Attribute
17+
{
18+
}
19+
}

src/Components/Components/src/PublicAPI.Unshipped.txt

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ Microsoft.AspNetCore.Components.ComponentApplicationState.PersistAsJson<TValue>(
99
Microsoft.AspNetCore.Components.ComponentApplicationState.PersistState(string! key, byte[]! value) -> void
1010
Microsoft.AspNetCore.Components.ComponentApplicationState.TryTakeAsJson<TValue>(string! key, out TValue? instance) -> bool
1111
Microsoft.AspNetCore.Components.ComponentApplicationState.TryTakePersistedState(string! key, out byte[]? value) -> bool
12+
Microsoft.AspNetCore.Components.EditorRequiredAttribute
13+
Microsoft.AspNetCore.Components.EditorRequiredAttribute.EditorRequiredAttribute() -> void
1214
Microsoft.AspNetCore.Components.ErrorBoundaryBase
1315
Microsoft.AspNetCore.Components.ErrorBoundaryBase.ChildContent.get -> Microsoft.AspNetCore.Components.RenderFragment?
1416
Microsoft.AspNetCore.Components.ErrorBoundaryBase.ChildContent.set -> void

src/Components/Components/src/RouteView.cs

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public class RouteView : IComponent
2525
/// displayed and the parameter values that will be supplied to the page.
2626
/// </summary>
2727
[Parameter]
28+
[EditorRequired]
2829
public RouteData RouteData { get; set; }
2930

3031
/// <summary>

src/Components/Components/src/Routing/Router.cs

+9-3
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,9 @@ static readonly IReadOnlyDictionary<string, object> _emptyParametersDictionary
4747
/// <summary>
4848
/// Gets or sets the assembly that should be searched for components matching the URI.
4949
/// </summary>
50-
[Parameter] public Assembly AppAssembly { get; set; }
50+
[Parameter]
51+
[EditorRequired]
52+
public Assembly AppAssembly { get; set; }
5153

5254
/// <summary>
5355
/// Gets or sets a collection of additional assemblies that should be searched for components
@@ -58,12 +60,16 @@ static readonly IReadOnlyDictionary<string, object> _emptyParametersDictionary
5860
/// <summary>
5961
/// Gets or sets the content to display when no match is found for the requested route.
6062
/// </summary>
61-
[Parameter] public RenderFragment NotFound { get; set; }
63+
[Parameter]
64+
[EditorRequired]
65+
public RenderFragment NotFound { get; set; }
6266

6367
/// <summary>
6468
/// Gets or sets the content to display when a match is found for the requested route.
6569
/// </summary>
66-
[Parameter] public RenderFragment<RouteData> Found { get; set; }
70+
[Parameter]
71+
[EditorRequired]
72+
public RenderFragment<RouteData> Found { get; set; }
6773

6874
/// <summary>
6975
/// Get or sets the content to display when asynchronous navigation is in progress.

src/Razor/Microsoft.AspNetCore.Razor.Language/src/BoundAttributeDescriptor.cs

+3-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ protected BoundAttributeDescriptor(string kind)
2929

3030
public bool IsBooleanProperty { get; protected set; }
3131

32+
internal bool IsEditorRequired { get; set; }
33+
3234
public string Name { get; protected set; }
3335

3436
public string IndexerNamePrefix { get; protected set; }
@@ -81,4 +83,4 @@ public override int GetHashCode()
8183
return BoundAttributeDescriptorComparer.Default.GetHashCode(this);
8284
}
8385
}
84-
}
86+
}

src/Razor/Microsoft.AspNetCore.Razor.Language/src/BoundAttributeDescriptorBuilder.cs

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
@@ -28,6 +28,8 @@ public abstract class BoundAttributeDescriptorBuilder
2828

2929
public abstract RazorDiagnosticCollection Diagnostics { get; }
3030

31+
internal bool IsEditorRequired { get; set; }
32+
3133
public virtual IReadOnlyList<BoundAttributeParameterDescriptorBuilder> BoundAttributeParameters { get; }
3234

3335
public virtual void BindAttributeParameter(Action<BoundAttributeParameterDescriptorBuilder> configure)

src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentLoweringPass.cs

+44-3
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentInte
6767
}
6868
}
6969

70-
private ComponentIntermediateNode RewriteAsComponent(TagHelperIntermediateNode node, TagHelperDescriptor tagHelper)
70+
private static ComponentIntermediateNode RewriteAsComponent(TagHelperIntermediateNode node, TagHelperDescriptor tagHelper)
7171
{
7272
var component = new ComponentIntermediateNode()
7373
{
@@ -89,13 +89,54 @@ private ComponentIntermediateNode RewriteAsComponent(TagHelperIntermediateNode n
8989
// because we see the nodes in the wrong order.
9090
foreach (var childContent in component.ChildContents)
9191
{
92-
childContent.ParameterName = childContent.ParameterName ?? component.ChildContentParameterName ?? ComponentMetadata.ChildContent.DefaultParameterName;
92+
childContent.ParameterName ??= component.ChildContentParameterName ?? ComponentMetadata.ChildContent.DefaultParameterName;
9393
}
9494

95+
ValidateRequiredAttributes(node, tagHelper, component);
96+
9597
return component;
9698
}
9799

98-
private MarkupElementIntermediateNode RewriteAsElement(TagHelperIntermediateNode node)
100+
private static void ValidateRequiredAttributes(TagHelperIntermediateNode node, TagHelperDescriptor tagHelper, ComponentIntermediateNode intermediateNode)
101+
{
102+
if (intermediateNode.Children.Any(c => c is TagHelperDirectiveAttributeIntermediateNode node && (node.TagHelper?.IsSplatTagHelper() ?? false)))
103+
{
104+
// If there are any splat attributes, assume the user may have provided all values.
105+
// This pass runs earlier than ComponentSplatLoweringPass, so we cannot rely on the presence of SplatIntermediateNode to make this check.
106+
return;
107+
}
108+
109+
foreach (var requiredAttribute in tagHelper.EditorRequiredAttributes)
110+
{
111+
if (!IsPresentAsAttribute(requiredAttribute.Name, intermediateNode))
112+
{
113+
intermediateNode.Diagnostics.Add(
114+
RazorDiagnosticFactory.CreateComponent_EditorRequiredParameterNotSpecified(
115+
node.Source ?? SourceSpan.Undefined,
116+
intermediateNode.TagName,
117+
requiredAttribute.Name));
118+
}
119+
}
120+
121+
static bool IsPresentAsAttribute(string attributeName, ComponentIntermediateNode intermediateNode)
122+
{
123+
foreach (var child in intermediateNode.Children)
124+
{
125+
if (child is ComponentAttributeIntermediateNode attributeNode && attributeName == attributeNode.AttributeName)
126+
{
127+
return true;
128+
}
129+
else if (child is ComponentChildContentIntermediateNode childContent && attributeName == childContent.AttributeName)
130+
{
131+
return true;
132+
}
133+
}
134+
135+
return false;
136+
}
137+
}
138+
139+
private static MarkupElementIntermediateNode RewriteAsElement(TagHelperIntermediateNode node)
99140
{
100141
var result = new MarkupElementIntermediateNode()
101142
{

src/Razor/Microsoft.AspNetCore.Razor.Language/src/DefaultBoundAttributeDescriptorBuilder.cs

+4-1
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,10 @@ public BoundAttributeDescriptor Build()
125125
CaseSensitive,
126126
parameters,
127127
new Dictionary<string, string>(Metadata),
128-
diagnostics.ToArray());
128+
diagnostics.ToArray())
129+
{
130+
IsEditorRequired = IsEditorRequired,
131+
};
129132

130133
return descriptor;
131134
}

src/Razor/Microsoft.AspNetCore.Razor.Language/src/RazorDiagnosticFactory.cs

+12
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Collections.Generic;
56
using Microsoft.AspNetCore.Razor.Language.Legacy;
67

78
namespace Microsoft.AspNetCore.Razor.Language
@@ -588,6 +589,17 @@ public static RazorDiagnostic CreateTagHelper_InconsistentTagStructure(SourceSpa
588589
return RazorDiagnostic.Create(TagHelper_InconsistentTagStructure, location, firstDescriptor, secondDescriptor, tagName, nameof(TagMatchingRuleDescriptor.TagStructure));
589590
}
590591

592+
internal static readonly RazorDiagnosticDescriptor Component_EditorRequiredParameterNotSpecified =
593+
new RazorDiagnosticDescriptor(
594+
$"{DiagnosticPrefix}2012",
595+
() => Resources.Component_EditorRequiredParameterNotSpecified,
596+
RazorDiagnosticSeverity.Warning);
597+
598+
public static RazorDiagnostic CreateComponent_EditorRequiredParameterNotSpecified(SourceSpan location, string tagName, string parameterName)
599+
{
600+
return RazorDiagnostic.Create(Component_EditorRequiredParameterNotSpecified, location, tagName, parameterName);
601+
}
602+
591603
#endregion
592604

593605
#region TagHelper Errors

src/Razor/Microsoft.AspNetCore.Razor.Language/src/Resources.resx

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<?xml version="1.0" encoding="utf-8"?>
1+
<?xml version="1.0" encoding="utf-8"?>
22
<root>
33
<!--
44
Microsoft ResX Schema
@@ -571,4 +571,7 @@
571571
<data name="ParseError_Unexpected_Identifier_At_Position" xml:space="preserve">
572572
<value>'{0}' is not valid in this position. Valid options are '{1}'</value>
573573
</data>
574-
</root>
574+
<data name="Component_EditorRequiredParameterNotSpecified" xml:space="preserve">
575+
<value>Component '{0}' expects a value for the parameter '{1}', but a value may not have been provided.</value>
576+
</data>
577+
</root>

src/Razor/Microsoft.AspNetCore.Razor.Language/src/TagHelperDescriptor.cs

+26
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ namespace Microsoft.AspNetCore.Razor.Language
1010
public abstract class TagHelperDescriptor : IEquatable<TagHelperDescriptor>
1111
{
1212
private IEnumerable<RazorDiagnostic> _allDiagnostics;
13+
private BoundAttributeDescriptor[] _editorRequiredAttributes;
1314

1415
protected TagHelperDescriptor(string kind)
1516
{
@@ -45,6 +46,14 @@ protected TagHelperDescriptor(string kind)
4546
internal bool? IsComponentFullyQualifiedNameMatchCache { get; set; }
4647
internal bool? IsChildContentTagHelperCache { get; set; }
4748
internal ParsedTypeInformation? ParsedTypeInfo { get; set; }
49+
internal BoundAttributeDescriptor[] EditorRequiredAttributes
50+
{
51+
get
52+
{
53+
_editorRequiredAttributes ??= GetEditorRequiredAttributes(BoundAttributes);
54+
return _editorRequiredAttributes;
55+
}
56+
}
4857

4958
public bool HasErrors
5059
{
@@ -96,6 +105,23 @@ public override int GetHashCode()
96105
return _hashCode.Value;
97106
}
98107

108+
private static BoundAttributeDescriptor[] GetEditorRequiredAttributes(IReadOnlyList<BoundAttributeDescriptor> boundAttributeDescriptors)
109+
{
110+
List<BoundAttributeDescriptor> editorRequiredAttributes = null;
111+
var count = boundAttributeDescriptors.Count;
112+
for (var i = 0; i < count; i++)
113+
{
114+
var attribute = boundAttributeDescriptors[i];
115+
if (attribute.IsEditorRequired)
116+
{
117+
editorRequiredAttributes ??= new();
118+
editorRequiredAttributes.Add(attribute);
119+
}
120+
}
121+
122+
return editorRequiredAttributes?.ToArray() ?? Array.Empty<BoundAttributeDescriptor>();
123+
}
124+
99125
internal readonly struct ParsedTypeInformation
100126
{
101127
public ParsedTypeInformation(bool success, StringSegment @namespace, StringSegment typeName)

0 commit comments

Comments
 (0)