Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 34 additions & 10 deletions src/Nullean.PublicApi/ApiParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -728,6 +728,11 @@ private static ApiMember Declaration(
{
var conversion = cursor.Word() == "implicit" ? "op_Implicit" : "op_Explicit";
cursor.ExpectWord("operator");
if (conversion == "op_Explicit" && cursor.PeekWord() == "checked")
{
cursor.Word();
conversion = "op_CheckedExplicit";
}
var conversionType = scope.Type(cursor);
var conversionParameters = Parameters(cursor, scope, '(', ')', out _);
var conversionBody = Body(cursor);
Expand Down Expand Up @@ -977,6 +982,12 @@ bool isSealedInInterface
}
var hasBody = Body(cursor);
cursor.End();
// a readonly property's accessors are all readonly
if ((modifiers & ApiMemberModifiers.ReadOnly) != 0)
{
getter = getter is null ? null : getter with { IsReadOnly = true };
setter = setter is null ? null : setter with { IsReadOnly = true };
}
return new ApiProperty(
name,
access,
Expand Down Expand Up @@ -1021,18 +1032,19 @@ private static List<ApiParameter> Parameters(Cursor cursor, Scope scope, char op
cursor.Word();
isExtension = true;
}
var isScoped = false;
if (cursor.PeekWord() == "scoped")
{
cursor.Word();
isScoped = true;
}
// params scoped, as C# writes it, and scoped alone
var isParams = false;
if (cursor.PeekWord() == "params")
{
cursor.Word();
isParams = true;
}
var isScoped = false;
if (cursor.PeekWord() == "scoped")
{
cursor.Word();
isScoped = true;
}
var refKind = cursor.PeekWord() switch
{
"out" => ApiRefKind.Out,
Expand Down Expand Up @@ -1103,20 +1115,32 @@ private static string OperatorName(string symbol, int parameters, Cursor cursor)
">=" => "op_GreaterThanOrEqual",
"!" => "op_LogicalNot",
"~" => "op_OnesComplement",
"++" => "op_Increment",
"--" => "op_Decrement",
// without a parameter, the instance operator that changes the value it is called on
"++" => parameters == 0 ? "op_IncrementAssignment" : "op_Increment",
"--" => parameters == 0 ? "op_DecrementAssignment" : "op_Decrement",
"true" => "op_True",
"false" => "op_False",
"checked +" => "op_CheckedAddition",
"checked -" => unary ? "op_CheckedUnaryNegation" : "op_CheckedSubtraction",
"checked *" => "op_CheckedMultiply",
"checked /" => "op_CheckedDivision",
"checked ++" => "op_CheckedIncrement",
"checked --" => "op_CheckedDecrement",
"checked ++" => parameters == 0 ? "op_CheckedIncrementAssignment" : "op_CheckedIncrement",
"checked --" => parameters == 0 ? "op_CheckedDecrementAssignment" : "op_CheckedDecrement",
"+=" => "op_AdditionAssignment",
"-=" => "op_SubtractionAssignment",
"*=" => "op_MultiplicationAssignment",
"/=" => "op_DivisionAssignment",
"%=" => "op_ModulusAssignment",
"&=" => "op_BitwiseAndAssignment",
"|=" => "op_BitwiseOrAssignment",
"^=" => "op_ExclusiveOrAssignment",
"<<=" => "op_LeftShiftAssignment",
">>=" => "op_RightShiftAssignment",
">>>=" => "op_UnsignedRightShiftAssignment",
"checked +=" => "op_CheckedAdditionAssignment",
"checked -=" => "op_CheckedSubtractionAssignment",
"checked *=" => "op_CheckedMultiplicationAssignment",
"checked /=" => "op_CheckedDivisionAssignment",
_ when symbol.StartsWith("op_", StringComparison.Ordinal) => symbol,
_ => null,
};
Expand Down
22 changes: 17 additions & 5 deletions src/Nullean.PublicApi/ApiReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -776,15 +776,15 @@ bool isStruct
return null;
var isInit = isSetter &&
m.DecodeSignature(_types, context).ReturnType is ApiModifiedType { Modifier.FullName: CompilerServices + "IsExternalInit" };
var isReadOnly = isStruct && !isSetter && HasAttribute(m.GetCustomAttributes(), CompilerServices + "IsReadOnlyAttribute");
var isReadOnly = isStruct && HasAttribute(m.GetCustomAttributes(), CompilerServices + "IsReadOnlyAttribute");
return new ApiAccessor(access.Value, isInit, isReadOnly);
}

return new ApiProperty(
_reader.GetString(property.Name),
accessibility,
Attributes(property.GetCustomAttributes()),
Modifiers(primary, declaring),
PropertyModifiers(Modifiers(primary, declaring), getter, setter, isStruct),
propertyType,
refKind,
parameters,
Expand All @@ -794,6 +794,17 @@ bool isStruct
);
}

/// <summary>A property is readonly when every accessor is: the getter alone being readonly is the getter's.</summary>
private ApiMemberModifiers PropertyModifiers(ApiMemberModifiers modifiers, MethodDefinition? getter, MethodDefinition? setter, bool isStruct)
{
bool ReadOnly(MethodDefinition? accessor) => accessor is not { } m || HasAttribute(m.GetCustomAttributes(), CompilerServices + "IsReadOnlyAttribute");

return isStruct && ReadOnly(getter) && ReadOnly(setter) ? modifiers | ApiMemberModifiers.ReadOnly : modifiers & ~ApiMemberModifiers.ReadOnly;
}

private bool IsParams(IEnumerable<CustomAttributeHandle> attributes) =>
HasAttribute(attributes, "System.ParamArrayAttribute") || HasAttribute(attributes, CompilerServices + "ParamCollectionAttribute");

private static ApiAccessibility Most(ApiAccessibility? first, ApiAccessibility? second)
{
if (first == ApiAccessibility.Public || second == ApiAccessibility.Public)
Expand Down Expand Up @@ -822,7 +833,7 @@ private static ApiAccessibility Most(ApiAccessibility? first, ApiAccessibility?
var special = (method.Attributes & MethodAttributes.SpecialName) != 0;
var kind = name == ".ctor"
? ApiMethodKind.Constructor
: special && name is "op_Implicit" or "op_Explicit"
: special && name is "op_Implicit" or "op_Explicit" or "op_CheckedExplicit"
? ApiMethodKind.Conversion
: special && name.StartsWith("op_", StringComparison.Ordinal) ? ApiMethodKind.Operator : ApiMethodKind.Method;

Expand Down Expand Up @@ -961,8 +972,9 @@ private IReadOnlyList<ApiParameter> ReadParameters(MethodDefinition method, Immu
row is { } named ? _reader.GetString(named.Name) : "arg" + i,
Annotate(rawType, attributes, methodContext, offset),
refKind,
HasAttribute(attributes, "System.ParamArrayAttribute") || HasAttribute(attributes, CompilerServices + "ParamCollectionAttribute"),
HasAttribute(attributes, CompilerServices + "ScopedRefAttribute"),
IsParams(attributes),
// a params span is scoped without saying so; only a scoped the source wrote is the API's
HasAttribute(attributes, CompilerServices + "ScopedRefAttribute") && !IsParams(attributes),
row is { } optional ? DefaultValue(optional, rawType) : null,
Attributes(attributes)
)
Expand Down
37 changes: 30 additions & 7 deletions src/Nullean.PublicApi/ApiWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -415,8 +415,9 @@ public void Member(ApiMember member, ApiType type, int depth)
else
builder.Append(property.Name);
builder.Append(" {");
Accessor(property.Getter, property.Accessibility, "get");
Accessor(property.Setter, property.Accessibility, property.Setter is { IsInit: true } ? "init" : "set");
var allReadOnly = (property.Modifiers & ApiMemberModifiers.ReadOnly) != 0;
Accessor(property.Getter, property.Accessibility, "get", allReadOnly);
Accessor(property.Setter, property.Accessibility, property.Setter is { IsInit: true } ? "init" : "set", allReadOnly);
builder.Append(" }");
if (inInterface && HasBody(property.Modifiers))
builder.Append(" { … }");
Expand All @@ -437,7 +438,12 @@ public void Member(ApiMember member, ApiType type, int depth)
switch (method.Kind)
{
case ApiMethodKind.Conversion:
builder.Append(method.Name == "op_Implicit" ? "implicit" : "explicit").Append(" operator ").Append(returnType);
builder.Append(method.Name switch
{
"op_Implicit" => "implicit operator ",
"op_CheckedExplicit" => "explicit operator checked ",
_ => "explicit operator ",
}).Append(returnType);
break;
case ApiMethodKind.Operator:
builder.Append(returnType).Append(" operator ").Append(Operator(method.Name));
Expand Down Expand Up @@ -505,14 +511,14 @@ private void Extension(ApiExtension extension, ApiType type, int depth)
private static bool HasBody(ApiMemberModifiers modifiers) =>
(modifiers & ApiMemberModifiers.Virtual) != 0 || (modifiers & (ApiMemberModifiers.Static | ApiMemberModifiers.Abstract)) == 0;

private void Accessor(ApiAccessor? accessor, ApiAccessibility propertyAccessibility, string keyword)
private void Accessor(ApiAccessor? accessor, ApiAccessibility propertyAccessibility, string keyword, bool propertyReadOnly)
{
if (accessor is null)
return;
builder.Append(' ');
if (accessor.Accessibility != propertyAccessibility)
builder.Append(Access(accessor.Accessibility)).Append(' ');
if (accessor.IsReadOnly)
if (accessor.IsReadOnly && !propertyReadOnly)
builder.Append("readonly ");
builder.Append(keyword).Append(';');
}
Expand All @@ -535,10 +541,11 @@ private void ParameterList(IReadOnlyList<ApiParameter> parameters, bool isExtens
builder.Append(Attribute(attribute, null)).Append(' ');
if (i == 0 && isExtension)
builder.Append("this ");
if (parameter.IsScoped)
builder.Append("scoped ");
// params comes first: C# rejects scoped params
if (parameter.IsParams)
builder.Append("params ");
if (parameter.IsScoped)
builder.Append("scoped ");
builder.Append(parameter.RefKind switch
{
ApiRefKind.Ref => "ref ",
Expand Down Expand Up @@ -748,6 +755,22 @@ public string Reference(ApiTypeReference type)
"op_SubtractionAssignment" => "-=",
"op_MultiplicationAssignment" => "*=",
"op_DivisionAssignment" => "/=",
"op_ModulusAssignment" => "%=",
"op_BitwiseAndAssignment" => "&=",
"op_BitwiseOrAssignment" => "|=",
"op_ExclusiveOrAssignment" => "^=",
"op_LeftShiftAssignment" => "<<=",
"op_RightShiftAssignment" => ">>=",
"op_UnsignedRightShiftAssignment" => ">>>=",
// an instance ++ changes the value it is called on: void operator ++()
"op_IncrementAssignment" => "++",
"op_DecrementAssignment" => "--",
"op_CheckedAdditionAssignment" => "checked +=",
"op_CheckedSubtractionAssignment" => "checked -=",
"op_CheckedMultiplicationAssignment" => "checked *=",
"op_CheckedDivisionAssignment" => "checked /=",
"op_CheckedIncrementAssignment" => "checked ++",
"op_CheckedDecrementAssignment" => "checked --",
_ => name,
};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
assembly Lib
using System: Action
using struct System: ReadOnlySpan<>
using interface System: IDisposable, IEquatable<>
using System.Collections.Generic: IEnumerable<>
using System.Diagnostics.CodeAnalysis: ExperimentalAttribute
using System.Runtime.CompilerServices: InlineArrayAttribute, OverloadResolutionPriorityAttribute

namespace Lib
class Api
Api()
int Field { get; set; }
void Add(params IEnumerable<int> values)
void Keep(scoped ReadOnlySpan<string> parts)
void Log(params ReadOnlySpan<string> parts)
ref readonly int Peek(ref readonly int value)
[OverloadResolutionPriority(1)]
void Prefer(ReadOnlySpan<char> s)
void Prefer(string s)
void Take<T>(T value) where T : allows ref struct
[Experimental("LIB001")]
void Trial()

[InlineArray(4)]
struct Buffer4

ref struct Cursor : IDisposable
void Dispose()

interface ICounter
static int One { get; } { … }
static abstract int Zero { get; }
void Tick() { … }
static int Two()

struct Money
decimal Amount
int Auto { readonly get; set; }
int Mutating { get; set; }
int Setter { get; readonly set; }
readonly int Whole { get; set; }
readonly int Sum()
static Money operator +(Money a, Money b)
void operator +=(Money other)
static Money operator checked +(Money a, Money b)
void operator checked +=(Money other)
void operator checked --()
void operator --()
static Money operator ++(Money m)
void operator ++()
void operator %=(Money other)
void operator >>>=(int count)
static explicit operator checked int(Money m)
static explicit operator int(Money m)

class Parts
Parts()
int Size { get; }
event Action? Changed

struct Point : IEquatable<Point>
Point(int X, int Y)
int X { readonly get; set; }
int Y { readonly get; set; }
readonly void Deconstruct(out int X, out int Y)
readonly bool Equals(Point other)
override readonly bool Equals(object obj)
override readonly int GetHashCode()
override readonly string ToString()
static bool operator ==(Point left, Point right)
static bool operator !=(Point left, Point right)

class Primary
Primary(int seed)
int Seed { get; }
22 changes: 13 additions & 9 deletions tests/Nullean.PublicApi.Tests/RoslynOracleTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,9 @@ private static void Ours(ApiMember member, string name, Dictionary<string, strin
if (property.IsRequired)
facts.Add("required");
if (property.Getter is { } get)
facts.Add("get:" + Access(get.Accessibility));
facts.Add("get:" + Access(get.Accessibility) + (get.IsReadOnly ? ":readonly" : ""));
if (property.Setter is { } set)
facts.Add((set.IsInit ? "init:" : "set:") + Access(set.Accessibility));
facts.Add((set.IsInit ? "init:" : "set:") + Access(set.Accessibility) + (set.IsReadOnly ? ":readonly" : ""));
result[name + "::" + property.Name + Key(property.Parameters.Select(p => p.Type))] = string.Join(" ", facts) + " " + RefKind(
property.RefKind
) + Render(property.Type) + Parameters(property.Parameters);
Expand All @@ -147,9 +147,9 @@ private static void Ours(ApiMember member, string name, Dictionary<string, strin
method.Parameters.Select(p => p.Type),
method.Parameters.Select(p => p.RefKind)
)] = Access(method.Accessibility) + " " + (IsFinalizer(method) ? "[finalizer]" : Modifiers(method.Modifiers)) +
(method.IsExtension ? " extension" : "") + TypeParameters(method.TypeParameters) + " " + RefKind(method.ReturnRefKind) + Render(
method.ReturnType
) + Parameters(method.Parameters);
((method.Modifiers & ApiMemberModifiers.ReadOnly) != 0 ? " readonly" : "") + (method.IsExtension ? " extension" : "") + TypeParameters(
method.TypeParameters
) + " " + RefKind(method.ReturnRefKind) + Render(method.ReturnType) + Parameters(method.Parameters);
break;
}
}
Expand Down Expand Up @@ -402,9 +402,9 @@ private static void Theirs(ISymbol member, string name, bool extendable, Diction
if (property.IsRequired)
facts.Add("required");
if (get is not null)
facts.Add("get:" + Access(get.DeclaredAccessibility));
facts.Add("get:" + Access(get.DeclaredAccessibility) + (Readonly(get) ? ":readonly" : ""));
if (set is not null)
facts.Add((set.IsInitOnly ? "init:" : "set:") + Access(set.DeclaredAccessibility));
facts.Add((set.IsInitOnly ? "init:" : "set:") + Access(set.DeclaredAccessibility) + (Readonly(set) ? ":readonly" : ""));
result[name + "::" + property.MetadataName + Key(property.Parameters)] = string.Join(" ", facts) + " " +
(property.RefKind == Microsoft.CodeAnalysis.RefKind.RefReadOnly ? "ref readonly " : RefKind(property.RefKind)) + Render(
property.Type,
Expand All @@ -425,7 +425,7 @@ private static void Theirs(ISymbol member, string name, bool extendable, Diction
result[name + "::" + method.MetadataName + "`" + method.TypeParameters.Length + Key(method.Parameters)] =
// Roslyn shows a finalizer as a destructor, which is never virtual; metadata says it is
Access(method.DeclaredAccessibility) + " " + (method.MethodKind == MethodKind.Destructor ? "[finalizer]" : Modifiers(method)) +
(method.IsExtensionMethod ? " extension" : "") + TypeParameters(method.TypeParameters) + " " +
(Readonly(method) ? " readonly" : "") + (method.IsExtensionMethod ? " extension" : "") + TypeParameters(method.TypeParameters) + " " +
(method.RefKind == Microsoft.CodeAnalysis.RefKind.RefReadOnly ? "ref readonly " : RefKind(method.RefKind)) + Render(
method.ReturnType,
method.ReturnNullableAnnotation
Expand All @@ -434,6 +434,10 @@ private static void Theirs(ISymbol member, string name, bool extendable, Diction
}
}

/// <summary>A struct member declared or made readonly: in a readonly struct every member is, so none says so.</summary>
private static bool Readonly(IMethodSymbol method) =>
method.IsReadOnly && method.ContainingType is { TypeKind: TypeKind.Struct, IsReadOnly: false } && !method.IsStatic;

private static bool PublicAllTheWay(INamedTypeSymbol type) =>
type.DeclaredAccessibility == Accessibility.Public && (type.ContainingType is not { } containing || PublicAllTheWay(containing));

Expand All @@ -447,7 +451,7 @@ private static string Parameters(ImmutableArray<IParameterSymbol> parameters) =>
", ",
parameters.Select(
p => (p.IsParams ? "params " : "") +
(p.ScopedKind != ScopedKind.None && p.RefKind != Microsoft.CodeAnalysis.RefKind.Out && !p.IsThis ? "scoped " : "") + RefKind(
(p.ScopedKind != ScopedKind.None && p.RefKind != Microsoft.CodeAnalysis.RefKind.Out && !p.IsThis && !p.IsParams ? "scoped " : "") + RefKind(
p.RefKind
) + Render(p.Type, p.NullableAnnotation) + " " + p.MetadataName + (p.IsOptional ? " = ?" : "")
)
Expand Down
Loading
Loading