Brightspace / D2L.CodeStyle

Annotations and analysis tools for D2L C# code style
Apache License 2.0
10 stars 22 forks source link

Visibility analyzers proposal #327

Open j3parker opened 6 years ago

j3parker commented 6 years ago

In #326 @grant-cleary added an analyzer to restrict the visibility of a type that's registered for dependency injection.

This kind of problem has be a common cause for new analyzers (and more have been proposed but not implemented.) I think it's time to start planning a general and easier-to-use approach.

A general solution for visibility could take the form of adding annotations to a class/etc. that specified "who" could use it.

API

Examples

[LimitedVisibility( "This interface is an implementation detail. Please go through blerg blerg instead" )]
[VisibleToType( "D2L.Foo.Bar" )]
[VisibleToType( "D2L.Foo.Baz" )]
internal interface IFoo { /* ... */ }

This limits IFoo to the two listed types. This would be useful in larger assemblies where internal may be too permissive. The diagnostic would include the string from [LimitedVisibility] in its message to the user.

[LimitedVisibility]
[VisibleToAssembly( "D2L.SomeAssembly" )]
public interface IFoo { /* ... */ }

This limits IFoo to things within its assembly (as if it was internal) and to the other listed assembly. This is effectively a more-limited version of [InternalsVisibleTo].

The rationale for also allowing things within IFoos assembly to see/use IFoo is for making this behave consistently/on a continuum with the built in visibility attributes. If we didn't do this, you could always add [VisibleToAssembly( "MyAssembly" )] to get the visibility back. While that seems more flexible I'm not sure it's worth the cognitive overhead.

[LimitedVisibilty( "I want to make this internal, stop using it!" )]
[VisibleToAssembly( "D2L.Something" )]
[VisibleToType( "D2L.Foo" )]
[VisibleToType( "D2L.Bar" )]
[VisibleToNamespace( "D2L.SomeNamespace.Lol" )]
public interface IFoo { /* ... */ }

In this example we're trying to make IFoo internal, but we have to do some work. Different ways to whitelist (with varying degrees of specificity) could help maintain forward momentum.

If this was a very nasty thing (e.g. [JsonParamBinder]) we could register a fixit for the not-visible diagnostic and filter based on the symbol that was supposed to be not visible.

Benefits of a general solution

Existing analyzers

These are the existing analyzers that could have overlap with a general solution:

DangerousMethodUsages

This analyzer allows us to limit the usage of dangerous methods.

Conclusion: The audit/unaudited and the external types are not relevant to the visibility analysis - this should probably stay as it is and continue to be used in the way it's currently used.

JsonParamBinderAnalyzer

This restricts usage of a bad serialization attribute and encourages use of a newer good (but poorly named) replacement.

Conclusion: This could be merged into a general solution.

OldAndBrokenServiceLocatorAnalyzer

Restricts usage of IServiceLocator and friends

Conclusion: In principal this may be movable into a general solution.

CorsHeaderAppenderUsageAnalyzer

Restricts usage of a type that we're worried about being misused but can't exist in one assembly easily.

Conclusion: This could be merged into a general solution

grant-cleary commented 6 years ago

I like the idea of registering a fixit for specific cases (e.g. the [JsonParamBinder]). While we might hope that we rarely need to lock something down that aggressively, I think it's a safe assumption that those cases will continue to arise. In the general case, I definitely think it makes sense for the analyzer to ignore usages of IFoo (presuming IFoo is public) in its parent assembly.

So really, if we can build in the ability to say "Okay, we really don't want X used EVER except in Y, no exceptions," I think that's ideal. I wouldn't want it to get in the way of a decent general solution though.

j3parker commented 6 years ago

Some feedback from IRL conversations:

It'd be good to emphasize this would mostly be used for cleaning stuff up: the standard visibility accessors and their semantics are still preferred, and we should consciously design our assembly/DI boundaries (e.g. the public things should be usable generally.)

EDIT: in other words "what if this is used to make the code worse?" (e.g. switching internal to public, but with restrictions.) We concluded that was likely but we could detect new instances of that in PRs and add hint comments/send passive alerts to us to see how people use it.