TSpec.Architecture.CSharp
1.3.0
Prefix Reserved
See the version list below for details.
dotnet add package TSpec.Architecture.CSharp --version 1.3.0
NuGet\Install-Package TSpec.Architecture.CSharp -Version 1.3.0
<PackageReference Include="TSpec.Architecture.CSharp" Version="1.3.0" />
<PackageVersion Include="TSpec.Architecture.CSharp" Version="1.3.0" />
<PackageReference Include="TSpec.Architecture.CSharp" />
paket add TSpec.Architecture.CSharp --version 1.3.0
#r "nuget: TSpec.Architecture.CSharp, 1.3.0"
#:package TSpec.Architecture.CSharp@1.3.0
#addin nuget:?package=TSpec.Architecture.CSharp&version=1.3.0
#tool nuget:?package=TSpec.Architecture.CSharp&version=1.3.0
TSpec.Architecture.CSharp
Architecture rules on the code inside the projects of a TSpec build: their source files and folders, the types those declare and their Roslyn syntax. The projects and their references are TSpec's own, in its README's Architecture section.
Install the package in your architecture spec project:
dotnet add package TSpec.Architecture.CSharp
With using TSpec.Architecture.CSharp;, a project's Files are those it compiles from its own folder,
leaving out what the build generates under obj/ and files linked in from elsewhere. A file has its
Path in the project, written with /, its Lines and its Raw text, and a failure names it by
project and path: Data.Auth/Stores/UserStore.cs. A file's Folder, Name and Extension are parts
of its Path: for Stores/Cache/UserStore.cs, Stores, the folder directly in the project's, empty at
the project's root; UserStore; and cs. Its Types are those declared at its top level,
each with its Name (Repository), its FullName (Data.Repository<T>), Namespace, Kind,
IsPublic, the Files it is declared in, several for a partial type, and its Nested types, declared
inside it. A nested type's IsPublic is in effect: false inside a type that is not public. A failure
names a type by its full name. A project's Types are those of all its files, a partial type once. A
project's or a file's AllTypes are its types and those nested in them however deep, so a rule on every
public type reads them, not Types. A rule of one public type per file, adopted with today's breaches
listed in Allowed:
[Fact]
public void Only_the_listed_files_declare_more_than_one_public_type()
=> Source.Projects.SelectMany(project => project.Files)
.Select(file => (file.ToString(), Public: file.Types.Count(type => type.IsPublic)))
.Where(file => file.Public > 1)
.Are(Allowed);
A file's Root is its Roslyn syntax as the build parsed it, so code under an #if the build left
out is not in it, and its Model is the compiler's view of that syntax: what a name refers to, the type
of an expression. A type the spec project does not run with, or one a source generator wrote, is
unresolved there. SpanOf(node) is where a node or token starts and ends, its lines and columns
counted from 1:
[Fact]
public void No_arrow_starts_a_line()
=> Source.Projects.SelectMany(project => project.Files)
.SelectMany(file => file.Root.DescendantTokens()
.Where(token => token.IsKind(SyntaxKind.EqualsGreaterThanToken))
.Where(arrow => file.SpanOf(arrow).Start.Line > file.SpanOf(arrow.GetPreviousToken()).End.Line)
.Select(arrow => $"{file}:{file.SpanOf(arrow).Start.Line}"))
.Is().Empty();
A project's or a file's Nodes<T>() are its syntax nodes of one kind, in the order they are written: a
directive such as #pragma among them, nothing inside a documentation comment. Each has its File, its
Syntax and its Span, whose Lines are the lines it covers, and a failure names it by file and line:
Data.Auth/Stores/UserStore.cs:12.
[Fact]
public void No_query_is_written_in_query_syntax()
=> Source.Projects["Core"].Nodes<QueryExpressionSyntax>().Is().Empty();
A file's Members are those it declares, a nested type's and local functions too, and a project's are
those of all its files. A record's positional parameters are its constructor's and its properties. A
member's Accessibility is the one it has in effect, no more than the types around it allow: a public
member of an internal class is internal. Its Type and Parameters are syntax, as written, and a
failure names it by file, line and name: Data.Auth/Stores/UserStore.cs:12 Find. A member
IsOriginal when it sets its own name and signature: it neither implements an interface's member,
explicitly or not, nor overrides a base type's. A naming rule reads only those:
[Fact]
public void No_method_name_ends_in_Async()
=> Source.Projects.SelectMany(project => project.Members)
.Where(member => member.Kind == MemberKind.Method && member.IsOriginal)
.Has().None(method => method.Name.EndsWith("Async"));
A member Gives a type when the type it returns or holds is that type, or has it written inside: as a
type argument, Task<T>'s too, a tuple's element or an array's. It Takes one when a parameter's type
does. A named type's own members are not inside it, so a Booking with a Status gives no enum, and a
type is not one it only implements: List<int> gives no IEnumerable<>. Each takes a kind, a type
whatever its type arguments, or a condition, and without one lists the types, each once:
[Fact]
public void No_contract_member_gives_an_enum_or_takes_a_list()
=> Source.Projects["Contract"].Members
.Has().None(member => member.Gives(TypeKind.Enum) || member.Takes(typeof(List<>)));
A file's Uses are the build's types its code uses: those it names, and those whose members it names,
such as an extension method's class or a constant's type; a nested type is one of its own. A
type Exposes the build's types its signature names where other assemblies see it: its base type,
interfaces and constraints, and the types of its public and protected members, itself included. These,
Gives, Takes and DependsOn compile the project with Roslyn, once in a run, and a use by reflection
is not one the compiler sees.
[Fact]
public void The_domain_uses_nothing_of_persistence()
=> Source.Projects["Core"].Files.Where(file => file.Path.StartsWith("Domain/"))
.Has().None(file => file.Uses.Any(type => type.Namespace.EndsWith(".Persistence")));
A file DependsOn() the projects and packages whose types its code uses, named as the project rules name
them: Contract, P:Dapper. Its own project and the framework are none, and a project it reaches only
through another counts, unlike in its project's Dependencies. DependsOn(name) asks for one, and a name
ending in * matches as in Within:
[Fact]
public void Only_the_connection_calls_Dapper()
=> Source.Projects["Data"].Files.Where(file => file.Path != "Store/Connection.cs")
.Has().None(file => file.DependsOn("P:Dapper"));
A project's Folders are the folders directly in its own that hold its files, each with the others it
uses: one of its files uses a type a file of the other declares. A file at the project's root is in no
folder. They are a TSpec DependencyGraph, so Within takes rules on them as on the projects, and the
generated specification draws what the rules allow:
[Fact]
public void A_subdomain_uses_only_those_below_it()
=> Source.Projects["Application"].Folders.Is().Within(folder => folder switch
{
"Generation" or "Assistant" => ["Chat"],
"Workbench" => ["Generation", "Chat"],
_ => [],
});
Without rules, Folders.Is().Acyclic() keeps a project's folders one way.
A file is the same object every time, so an analysis TSpec doesn't have can be added to it as an
extension member that keeps what it computes per file in a ConditionalWeakTable<SourceFile, T>.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.CodeAnalysis.CSharp (>= 5.0.0)
- TSpec (>= 5.4.0)
-
net8.0
- Microsoft.CodeAnalysis.CSharp (>= 5.0.0)
- TSpec (>= 5.4.0)
-
net9.0
- Microsoft.CodeAnalysis.CSharp (>= 5.0.0)
- TSpec (>= 5.4.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
NEW
* Nodes<T>(): a project's or a file's syntax nodes of one kind, each named by its file and line
* SourceSpan.Lines: the lines a span covers
* A member's Gives and Takes: the types its return type and its parameters' types name, or whether one is of a kind, is a type or meets a condition
* A file's DependsOn: the projects and packages whose types its code uses, as the rules name them
* A file's Model: the compiler's view of its syntax
* AllTypes on a project or a file: its types with those nested in them
* A member's IsOriginal: whether it sets its own name and signature, neither implementing nor overriding
* A file's Folder, Name and Extension
CHANGED
* Requires TSpec 5.4.0 or later
OBSOLETE
* OpensInternalsTo: TSpec's own SourceProject.OpensInternalsTo takes its place, read without Roslyn