Skip to content

Commit

Permalink
feat: UnoInitializeComponentAnalyzer
Browse files Browse the repository at this point in the history
(cherry picked from commit 33df5dc)
  • Loading branch information
Youssef1313 authored and mergify[bot] committed Jun 12, 2024
1 parent 81ec091 commit 583210a
Show file tree
Hide file tree
Showing 2 changed files with 243 additions and 0 deletions.
157 changes: 157 additions & 0 deletions src/Uno.Analyzers/SourceTextStream.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
// https://github.com/dotnet/roslyn/blob/e1e7d07474a64f4da5c8b0b7f57082de5ce6a22b/src/Compilers/Core/Portable/Text/SourceTextStream.cs
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Diagnostics;
using System.IO;
using System.Text;

namespace Microsoft.CodeAnalysis.Text
{
/// <summary>
/// A read-only, non-seekable <see cref="Stream"/> over a <see cref="SourceText"/>.
/// </summary>
internal sealed class SourceTextStream : Stream
{
private readonly SourceText _source;
private readonly Encoding _encoding;
private readonly Encoder _encoder;

private readonly int _minimumTargetBufferCount;
private int _position;
private int _sourceOffset;
private readonly char[] _charBuffer;
private int _bufferOffset;
private int _bufferUnreadChars;
private bool _preambleWritten;

private static readonly Encoding s_utf8EncodingWithNoBOM = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: false);

public SourceTextStream(SourceText source, int bufferSize = 2048, bool useDefaultEncodingIfNull = false)
{
Debug.Assert(source.Encoding != null || useDefaultEncodingIfNull);

_source = source;
_encoding = source.Encoding ?? s_utf8EncodingWithNoBOM;
_encoder = _encoding.GetEncoder();
_minimumTargetBufferCount = _encoding.GetMaxByteCount(charCount: 1);
_sourceOffset = 0;
_position = 0;
_charBuffer = new char[Math.Min(bufferSize, _source.Length)];
_bufferOffset = 0;
_bufferUnreadChars = 0;
_preambleWritten = false;
}

public override bool CanRead
{
get { return true; }
}

public override bool CanSeek
{
get { return false; }
}

public override bool CanWrite
{
get { return false; }
}

public override void Flush()
{
throw new NotSupportedException();
}

public override long Length
{
get { throw new NotSupportedException(); }
}

public override long Position
{
get { return _position; }
set { throw new NotSupportedException(); }
}

public override int Read(byte[] buffer, int offset, int count)
{
if (count < _minimumTargetBufferCount)
{
// The buffer must be able to hold at least one character from the
// SourceText stream. Returning 0 for that case isn't correct because
// that indicates end of stream vs. insufficient buffer.
throw new ArgumentException($"{nameof(count)} must be greater than or equal to {_minimumTargetBufferCount}", nameof(count));
}

int originalCount = count;

if (!_preambleWritten)
{
int bytesWritten = WritePreamble(buffer, offset, count);
offset += bytesWritten;
count -= bytesWritten;
}

while (count >= _minimumTargetBufferCount && _position < _source.Length)
{
if (_bufferUnreadChars == 0)
{
FillBuffer();
}

int charsUsed, bytesUsed;
bool ignored;
_encoder.Convert(_charBuffer, _bufferOffset, _bufferUnreadChars, buffer, offset, count, flush: false, charsUsed: out charsUsed, bytesUsed: out bytesUsed, completed: out ignored);
_position += charsUsed;
_bufferOffset += charsUsed;
_bufferUnreadChars -= charsUsed;
offset += bytesUsed;
count -= bytesUsed;
}

// Return value is the number of bytes read
return originalCount - count;
}

private int WritePreamble(byte[] buffer, int offset, int count)
{
_preambleWritten = true;
byte[] preambleBytes = _encoding.GetPreamble();
if (preambleBytes == null)
{
return 0;
}

int length = Math.Min(count, preambleBytes.Length);
Array.Copy(preambleBytes, 0, buffer, offset, length);
return length;
}

private void FillBuffer()
{
int charsToRead = Math.Min(_charBuffer.Length, _source.Length - _sourceOffset);
_source.CopyTo(_sourceOffset, _charBuffer, 0, charsToRead);
_sourceOffset += charsToRead;
_bufferOffset = 0;
_bufferUnreadChars = charsToRead;
}

public override long Seek(long offset, SeekOrigin origin)
{
throw new NotSupportedException();
}

public override void SetLength(long value)
{
throw new NotSupportedException();
}

public override void Write(byte[] buffer, int offset, int count)
{
throw new NotSupportedException();
}
}
}
86 changes: 86 additions & 0 deletions src/Uno.Analyzers/UnoInitializeComponentAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#nullable enable

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Xml;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
using Microsoft.CodeAnalysis.Text;

namespace Uno.Analyzers;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class UnoInitializeComponentAnalyzer : DiagnosticAnalyzer
{
internal const string Title = "Call 'InitializeComponent()' from code-behind";
internal const string MessageFormat = "Type '{0}' should call 'InitializeComponent()' from its constructor";
internal const string Category = "Correctness";

internal static DiagnosticDescriptor Rule = new DiagnosticDescriptor(
#pragma warning disable RS2008 // Enable analyzer release tracking
"Uno0006",
#pragma warning restore RS2008 // Enable analyzer release tracking
Title,
MessageFormat,
Category,
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
helpLinkUri: "https://aka.platform.uno/UNO0006"
);

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(Rule);

public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);

context.RegisterCompilationStartAction(context =>
{
// .NET doesn't have ConcurrentHashSet. https://github.com/dotnet/runtime/issues/39919
var xClasses = new ConcurrentDictionary<string, byte>();
foreach (var xamlFile in context.Options.AdditionalFiles)
{
if (!xamlFile.Path.EndsWith(".xaml", StringComparison.OrdinalIgnoreCase))
{
continue;
}
var stream = new SourceTextStream(xamlFile.GetText(context.CancellationToken));
using (XmlReader reader = XmlReader.Create(stream))
{
reader.MoveToContent();
var xClass = reader.GetAttribute("x:Class");
if (!string.IsNullOrEmpty(xClass))
{
xClasses.TryAdd(xClass, 0);
}
}
}
context.RegisterOperationAction(context =>
{
var invocation = (IInvocationOperation)context.Operation;
if (invocation.TargetMethod.Name == "InitializeComponent" && invocation.TargetMethod.Parameters.Length == 0 &&
context.ContainingSymbol is IMethodSymbol { MethodKind: MethodKind.Constructor } constructor && constructor.Parameters.Length == 0)
{
xClasses.TryRemove(context.ContainingSymbol.ContainingType.ToDisplayString(), out _);
}
}, OperationKind.Invocation);
context.RegisterCompilationEndAction(context =>
{
foreach (var xClass in xClasses)
{
context.ReportDiagnostic(Diagnostic.Create(Rule, Location.None, xClass.Key));
}
});
});
}
}

0 comments on commit 583210a

Please sign in to comment.