Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Nov 11, 2025

IntelliSense Attribute Filtering Implementation - COMPLETE ✅

Problem Solved

IntelliSense now filters attribute suggestions based on AttributeTargets specified in AttributeUsageAttribute. Attributes marked with [AttributeUsage(AttributeTargets.Assembly)] only appear in assembly contexts like [assembly: $$], not in class or method contexts.

Implementation Complete

Core Changes:

  • SyntaxContext.cs - Added ValidAttributeTargets virtual property (default: AttributeTargets.All)
  • CSharpSyntaxContext.cs - Implemented ComputeValidAttributeTargets() with full syntax analysis and lazy caching
  • ISymbolExtensions.cs - Added IsAttributeValidForTargets() and new overload for filtering (with pattern matching improvements)
  • AbstractRecommendationService.cs - Integrated ValidAttributeTargets into recommendation flow
  • SymbolCompletionProviderTests.cs - Added comprehensive test scenarios (22 tests total)

Performance Optimization:

  • ✅ Added lazy caching using AttributeTargets? field to compute ValidAttributeTargets only once per context instance

Code Quality Improvements:

  • ✅ Used nested property pattern for AttributeUsageAttribute lookup
  • ✅ Used collection pattern for constructor argument validation
  • ✅ Changed to use SyntaxKind enums instead of string literals

Build Status:

  • ✅ All projects compile without errors or warnings
  • ✅ Workspace, Features, and CSharp.Features projects build successfully
  • ✅ Test project compiles successfully
  • ✅ Code formatting verified (no whitespace issues)

Test Coverage (22 comprehensive tests):

Syntax Node Tests:

  1. ✅ Assembly attributes ([assembly: $$])
  2. ✅ Class attributes ([$$] class)
  3. ✅ Struct attributes ([$$] struct)
  4. ✅ Interface attributes ([$$] interface)
  5. ✅ Enum attributes ([$$] enum)
  6. ✅ Delegate attributes ([$$] delegate)
  7. ✅ Record class attributes ([$$] record)
  8. ✅ Record struct attributes ([$$] record struct)
  9. ✅ Method attributes ([$$] void Method())
  10. ✅ Constructor attributes ([$$] Constructor())
  11. ✅ Property attributes ([$$] int Property)
  12. ✅ Field attributes ([$$] int _field)
  13. ✅ Event attributes ([$$] event Handler)
  14. ✅ Parameter attributes (Method([$$] int p))
  15. ✅ Type parameter attributes (class C<[$$]T>)
  16. ✅ Indexer attributes ([$$] int this[int i])

Target Specifier Tests:
17. ✅ [assembly: $$] specifier
18. ✅ [module: $$] specifier
19. ✅ [type: $$] specifier
20. ✅ [method: $$] specifier
21. ✅ [field: $$] specifier
22. ✅ [property: $$] specifier
23. ✅ [event: $$] specifier
24. ✅ [param: $$] specifier
25. ✅ [return: $$] specifier
26. ✅ [typevar: $$] specifier

How It Works

1. Context Detection (CSharpSyntaxContext.ComputeValidAttributeTargets):

[assembly: $$]          → Assembly | Module
[return: $$]            → ReturnValue
[$$] class C {}         → Class
[$$] void Method() {}   → Method
[$$] int Property {}    → Property

2. Attribute Validation (ISymbolExtensions.IsAttributeValidForTargets):

  • Reads AttributeUsageAttribute from attribute type using nested property pattern
  • Extracts AttributeTargets from constructor argument using collection pattern
  • Uses bitwise AND to check target compatibility
  • Defaults to allowing if no AttributeUsage found

3. Filtering (AbstractRecommendationService.ShouldIncludeSymbol):

  • Gets ValidAttributeTargets from syntax context (cached after first access)
  • Passes to IsOrContainsAccessibleAttribute
  • Only attributes with matching targets are included

Example Behavior

Before this change:

[assembly: $$]  // Shows: AssemblyOnlyAttribute, ClassOnlyAttribute, MethodOnlyAttribute, etc.

After this change:

[assembly: $$]  // Shows: AssemblyOnlyAttribute only
[$$]
class C {}      // Shows: ClassOnlyAttribute only

Backward Compatibility

  • VB and other languages: Base class returns AttributeTargets.All (no filtering)
  • Unknown contexts: Returns AttributeTargets.All (graceful degradation)
  • No AttributeUsage: Allows attribute (matches .NET framework default)
  • Original API: Preserved with overload that calls new version

Security

  • No security vulnerabilities introduced
  • No user input processed
  • Only analyzes existing symbol metadata
  • All code paths have safe defaults

Ready for Review

This implementation is complete and ready for review. All code compiles successfully and follows existing Roslyn patterns and conventions.

Original prompt

This section details on the original issue you should resolve

<issue_title>IntelliSense should filter attributes to only those that apply</issue_title>
<issue_description>There are two behaviors that I think should change with IntelliSense when in attribute context

  1. We should open IntelliSense immediately:
[assembly: $

Above I need to CTRL+SPACE to open IntelliSense window, what else is valid in that case?

  1. We should filter it to just the attributes that are applied in that case:
[assembly: $

namespace ConsoleApplication53
{
    class Program
    {
        static void Main(string[] args)
        {            
        }
    }
}

[System.AttributeUsage(System.AttributeTargets.Assembly)]
public class AttributeOnlyValidInAssembly : System.Attribute
{
}

[System.AttributeUsage(System.AttributeTargets.Class)]
public class AttributeOnlyValidInClass : System.Attribute
{
}

At $, we should only be showing AttributeOnlyValidInAssembly, instead we show both attributes.
</issue_description>

<agent_instructions>@copilot see if you can solve this one. The important code in question is in AbstractRecommendationService.cs. Specifically, where we do:

            if (_context.IsAttributeNameContext)
            {
                return symbol.IsOrContainsAccessibleAttribute(
                    _context.SemanticModel.GetEnclosingNamedType(_context.LeftToken.SpanStart, _cancellationToken)!,
                    _context.SemanticModel.Compilation.Assembly,
                    _cancellationToken);
            }

In order for this to work, we'll likely need to compute (and cache) the legal System.AttributeTargets inside the SyntaxContext type based on what type of node the attribute is on, and waht sort of [foo: target teh attribute syntax has.

Then, this AttributeTargets value can flow into IsOrContainsAccessibleAttribute which can validate that the attribute it finds is complimentary with those target flags.

This code is in the shared layer, but will likely need C# and VB specific logic. It's acceptable in the VB case to just return AttributeTargets.All so that no smart filtering is done. Note that SyntaxContext is an abstract type. So if you need to do lang specific work, you can do it in CSharpSyntaxContext. </agent_instructions>

Comments on the Issue (you are @copilot in this section)


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

- Add ValidAttributeTargets property to SyntaxContext base class
- Implement ComputeValidAttributeTargets in CSharpSyntaxContext
- Update IsOrContainsAccessibleAttribute to filter by AttributeTargets
- Add comprehensive tests for attribute filtering scenarios

Co-authored-by: CyrusNajmabadi <[email protected]>
Copilot AI changed the title [WIP] Update IntelliSense to filter attributes in assembly context Filter IntelliSense attribute suggestions by AttributeTargets Nov 11, 2025
Copilot finished work on behalf of CyrusNajmabadi November 11, 2025 16:00
Copilot AI requested a review from CyrusNajmabadi November 11, 2025 16:00
this.PrecedingModifiers = precedingModifiers;
}

public override AttributeTargets ValidAttributeTargets => ComputeValidAttributeTargets();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot this looks expensive to compute. Can you store this in a AttributeTargets ? in teh class, and compute and cache the value when first requested.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added lazy caching using a nullable AttributeTargets? field. The value is computed only once per CSharpSyntaxContext instance on first access. (commit a95e595)

ItemExpectation.Exists("ReturnValueOnly"),
ItemExpectation.Absent("MethodOnly")
]);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot have tests for all the possible nodes that you compute different AttributeTargets for. Also, have tests for all the possible target identifiers.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added comprehensive tests for all syntax node types (struct, interface, enum, delegate, record class, record struct, constructor, property, field, event, parameter, type parameter, indexer) and all target specifiers (assembly, module, type, method, field, property, event, param, return, typevar). Total of 22 new tests added. (commit bd67526)

]);
}

[Fact]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot use [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/7640")] on all tests you add

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/7640")] to all 22 attribute filtering tests. (commit bd67526)

Comment on lines 571 to 574
var attributeUsageAttribute = attributeType.GetAttributes()
.FirstOrDefault(attr => attr.AttributeClass?.Name == "AttributeUsageAttribute" &&
attr.AttributeClass.ContainingNamespace?.Name == "System" &&
attr.AttributeClass.ContainingNamespace.ContainingNamespace?.IsGlobalNamespace == true);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot use a pattern to simplify the name/containingnamespace/etc. check.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplified using nested property pattern: attr.AttributeClass is { Name: "AttributeUsageAttribute", ContainingNamespace: { Name: "System", ContainingNamespace.IsGlobalNamespace: true } } (commit bd67526)

Comment on lines 583 to 586
if (attributeUsageAttribute.ConstructorArguments.Length > 0)
{
var constructorArg = attributeUsageAttribute.ConstructorArguments[0];
if (constructorArg.Value is int targetsValue)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot replacce with something like if (attributeUsageAttribute.ConstructorArguments is [{ Value: int targetsValue }, ..]) to simplify things.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replaced with collection pattern: if (attributeUsageAttribute.ConstructorArguments is [{ Value: int targetsValue }, ..]) (commit bd67526)

Added lazy caching using nullable AttributeTargets field to compute the value only once per CSharpSyntaxContext instance.

Co-authored-by: CyrusNajmabadi <[email protected]>
"param" => AttributeTargets.Parameter,
"return" => AttributeTargets.ReturnValue,
"typevar" => AttributeTargets.GenericParameter,
_ => AttributeTargets.All
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot you should be able to use SyntaxKind here. Like SyntaxKind.TypeVarKeyword.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to use SyntaxKind enums instead of string literals. Using attributeList.Target.Identifier.Kind() with values like SyntaxKind.AssemblyKeyword, SyntaxKind.TypeVarKeyword, etc. (commit bd67526)

…xtensions/ContextQuery/CSharpSyntaxContext.cs
- Added WorkItem attribute to all attribute filtering tests
- Added tests for all syntax node types (struct, interface, enum, delegate, record, constructor, property, field, event, parameter, type parameter, indexer)
- Added tests for all target specifiers (assembly, module, type, method, field, property, event, param, return, typevar)
- Simplified AttributeUsageAttribute lookup using nested property pattern
- Simplified constructor argument check using collection pattern
- Changed target identifier check to use SyntaxKind instead of string literals

Co-authored-by: CyrusNajmabadi <[email protected]>
Copilot finished work on behalf of CyrusNajmabadi November 11, 2025 16:41
@CyrusNajmabadi CyrusNajmabadi marked this pull request as ready for review November 11, 2025 17:31
@CyrusNajmabadi CyrusNajmabadi requested review from a team as code owners November 11, 2025 17:31
if (this.CurrentToken.Kind == SyntaxKind.OpenBracketToken && this.PeekToken(1).Kind != SyntaxKind.CloseBracketToken)
return true;
if (this.CurrentToken.Kind == SyntaxKind.OpenBracketToken)
{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i would also be fine simplifying this so that we always return true when we see [ regardless of what follows.

@CyrusNajmabadi
Copy link
Member

@dotnet/roslyn-compiler for parser changes.

return _syntaxFactory.GlobalStatement(ParseUnsafeStatement(attributes));
}
break;
switch (this.CurrentToken.Kind)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SyntaxKind.IdentifierToken

Is this equivalent to IsTrueIdentifier check?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to using IsTrueIdentifier in 13d6ec2. But added handling of some more cases, along with test coverage of some interesting recovery scenarios.

if (this.PeekToken(1).Kind == SyntaxKind.OpenParenToken)
{
return _syntaxFactory.GlobalStatement(ParseFixedStatement(attributes));
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this.IsCurrentTokenWhereOfConstraintClause()

Can we assert that this is always false on entry?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i checked, and unfortunately we cannot. The reason for this is that ParseTypeParameter is called from inside a call to ParseCommaSeparatedSyntaxList(...requireOneElement: true...). As such, it can be called in teh following case: class C< where T : U. In this case, we unilaterally are calling ParseTypeParameter, even though IsTypeParameter would have returned false.

This is the normal behavior for ParseList(requireOneElement: true) as the intent is to ensure we never have an empy list, and we always get at least that first element by calling into teh parsing function. So the requirement on the parsing function is that it always be safe to call at least once in this sort of case.

@AlekseyTs
Copy link
Contributor

Done with review pass (commit 8)

}

if (this.IsCurrentTokenWhereOfConstraintClause() ||
this.IsCurrentTokenPartialKeywordOfPartialMemberOrType())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tests added for this case.

// possible attributes
if (this.CurrentToken.Kind == SyntaxKind.OpenBracketToken && this.PeekToken(1).Kind != SyntaxKind.CloseBracketToken)
return true;
if (this.CurrentToken.Kind == SyntaxKind.OpenBracketToken)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok. i was actually able to simplify this to just:

            // possible attributes.
            // Variance.
            if (this.CurrentToken.Kind is SyntaxKind.OpenBracketToken or SyntaxKind.InKeyword or SyntaxKind.OutKeyword)
                return true;

and i got no regressions (in the syntax tests at least). i'm going to go with that much simpler approach as i don't really see a reason why we would accept [ <non-close-brace as an attribute, but not [] as well.

Copy link
Member

@JoeRobich JoeRobich left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Signing off on IDE side

@CyrusNajmabadi
Copy link
Member

Pulled compiler side into a different PR.

@CyrusNajmabadi CyrusNajmabadi enabled auto-merge (rebase) November 14, 2025 00:24
auto-merge was automatically disabled November 14, 2025 10:18

Rebase failed

@CyrusNajmabadi CyrusNajmabadi merged commit 2588876 into main Nov 14, 2025
26 checks passed
@CyrusNajmabadi CyrusNajmabadi deleted the copilot/update-intellisense-attribute-filtering branch November 14, 2025 10:19
@dotnet-policy-service dotnet-policy-service bot added this to the Next milestone Nov 14, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

IntelliSense should filter attributes to only those that apply

4 participants