Documentation of exceptions - Discussion thread

Jul 6, 2011 at 6:39 AM
Edited Jul 6, 2011 at 1:57 PM

Primarily for me and oleg, but others might give feedback. Original thread: http://sshnet.codeplex.com/workitem/752

Note to self:

  1. X Check if it is possible to create StyleCop/FxCop rule for this.
  2. Check if one can use attributes on methods, ie. [Throws(typeof(ArgumentNullException),"path is null")], and use reflection on them.
  3. Check if it is possible to use source code for ILSpy to create a plugin to trace thrown exceptions
  4. Check for published guidelines for exceptions for libraries (maybe something in StyleCop/FxCop).

PS: It's amazing what you think of while taking a shower :P

Jul 6, 2011 at 9:50 AM

So I gave up on StyleCop...

But reflection might work. I created 3 assemblies, using reference: http://oreilly.com/catalog/progcsharp/chapter/ch18.html

  1. Console app
  2. Library containing a class using the attribute
  3. Library containing the attribute

Console app:

 public class ReflectionAttributeReader
    {
        public static void Main()
        {
            new ReflectionAttributeReader().Test(typeof(ExceptionThrows));
        }

        public void Test(Type type)
        {
            BindingFlags flags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.GetProperty | BindingFlags.SetProperty | BindingFlags.CreateInstance | BindingFlags.DeclaredOnly | BindingFlags.ExactBinding | BindingFlags.FlattenHierarchy | BindingFlags.GetField | BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.OptionalParamBinding;
            var members = new List<MemberInfo>(type.GetMembers(flags));
           
            foreach (var member in members)
            {
                foreach(var attribute in member.GetCustomAttributes(typeof(ThrowsExceptionAttribute), false))
                {
                    if (attribute is ThrowsExceptionAttribute)
                    {
                        ThrowsExceptionAttribute attr = attribute as ThrowsExceptionAttribute;
                        Debug.WriteLine(attr.ExceptionType.Name, attr.Description);
                    }
                }
            }
           
        }
    }

Library containing the attribute:

 [AttributeUsage(AttributeTargets.Constructor | AttributeTargets.Property | AttributeTargets.Method, AllowMultiple = true)]
    public class ThrowsExceptionAttribute : System.Attribute
    {
        public ThrowsExceptionAttribute(Type exceptionType, string description)
        {
            this.ExceptionType = exceptionType;
            this.Description = description;
        }

        public Type ExceptionType { get; set; }

        public string Description { get; set; }

    }

The class using the attribute

public class ExceptionThrows
    {
        [ThrowsException(typeof(NotImplementedException), "Public method")]
        public ExceptionThrows()
        {
            Debug.WriteLine("Public");
        }

        [ThrowsException(typeof(InvalidProgramException), "Private method.")]
        private void PrivateThrower()
        {
            Debug.WriteLine("Private");
        }

        [ThrowsException(typeof(ArgumentNullException), "Static method")]
        static void StaticThrower()
        {
            Debug.WriteLine("Static");
        }

        [ThrowsException(typeof(ArgumentException),"Internal method")]
        internal void InternalThrower()
        {
            Debug.WriteLine("Internal");
        }

        [ThrowsException(typeof(ArgumentOutOfRangeException),"Public property")]
        public string PublicPropertyThrower { get; set; }

        [ThrowsException(typeof(InvalidCastException),"Private property")]
        private int PrivatePropertyThrower { get; set; }

        [ThrowsException(typeof(ArithmeticException),"Internal property")]
        internal bool InternalPropertyThrower { get; set; }
    }

 When running the console app, you should get the following from the Output window:

Private method.: InvalidProgramException
Static method: ArgumentNullException
Internal method: ArgumentException
Public method: NotImplementedException
Public property: ArgumentOutOfRangeException
Private property: InvalidCastException
Internal property: ArithmeticException

So it definately should be possible to flag methods/properties that throws exceptions.

The next step are:

  1. Find the methods that are throwing exceptions.
  2. A call stack inspector to walk the methods that is called, and use step 1 on them.
Jul 6, 2011 at 12:16 PM

Seeing a logic failure in the post above here, but nevermind that.

I now I have Mono.Cecil and ICSharpCode.Decompiler working to disassemble the assembly created for the class that throws exceptions (with the attribute). And here is the code:

class Program
    {
        static void Main(string[] args)
        {
            string assemblyPath = Path.GetFullPath("ExceptionThrower.dll");

            AssemblyDefinition definition = AssemblyDefinition.ReadAssembly(assemblyPath);

            DecompilerContext decompilerContext = new DecompilerContext(definition.MainModule);
            AstBuilder astBuilder = new AstBuilder(decompilerContext);
            astBuilder.AddAssembly(definition);

            // Note: astBuilder.CodeMappings is a Tuple<string,List<MemberMapping>>.
            foreach (MemberMapping memberMapping in astBuilder.CodeMappings.Item2)
            {
                Debug.WriteLine("In assembly " + astBuilder.CodeMappings.Item1 + " these exceptions were found:" + Environment.NewLine);
                if(memberMapping.MemberReference is TypeDefinition)
                {
                    TypeDefinition typeDefinition = memberMapping.MemberReference as TypeDefinition;
                    foreach (MethodDefinition method in typeDefinition.Methods)
                    {
                        foreach (Instruction instruction in method.Body.Instructions)
                        {
                            if (instruction.OpCode == OpCodes.Throw)
                            {
                                Debug.WriteLine("Method [" + method.ToString() + "]"+ Environment.NewLine + "\t\tthrows exception [" + (instruction.Previous.Operand as MethodReference).DeclaringType.FullName + "]");
                                Debug.WriteLine(string.Empty);
                                continue;
                            }
                        }
                    }
                }
                break; // just 1 iteration, or we'll get alot of duplicate stuff, not researched further as to why.
            }

            return;
        }
    }

 

This outputs:

In assembly ExceptionThrower.ExceptionThrows these exceptions were found:

Method [System.Void ExceptionThrower.ExceptionThrows::.ctor()]
		throws exception [System.NotImplementedException]

Method [System.Void ExceptionThrower.ExceptionThrows::PrivateThrower()]
		throws exception [System.InvalidProgramException]

Method [System.Void ExceptionThrower.ExceptionThrows::StaticThrower()]
		throws exception [System.ArgumentNullException]

Method [System.Void ExceptionThrower.ExceptionThrows::InternalThrower()]
		throws exception [System.ArgumentException]

Method [System.String ExceptionThrower.ExceptionThrows::get_PublicPropertyThrower()]
		throws exception [System.ArgumentOutOfRangeException]

Method [System.Void ExceptionThrower.ExceptionThrows::set_PublicPropertyThrower(System.String)]
		throws exception [System.ArgumentOutOfRangeException]

Method [System.Int32 ExceptionThrower.ExceptionThrows::get_PrivatePropertyThrower()]
		throws exception [System.InvalidCastException]

Method [System.Void ExceptionThrower.ExceptionThrows::set_PrivatePropertyThrower(System.Int32)]
		throws exception [System.InvalidCastException]

Method [System.Boolean ExceptionThrower.ExceptionThrows::get_InternalPropertyThrower()]
		throws exception [System.ArithmeticException]

Method [System.Void ExceptionThrower.ExceptionThrows::set_InternalPropertyThrower(System.Boolean)]
		throws exception [System.ArithmeticException]

This should be enough information to use the attribute reflector locator to find the method signatures and cross-check attributes with ilcode, and then generate a report.

I started off by reading this post: http://community.sharpdevelop.net/forums/t/13452.aspx and http://community.sharpdevelop.net/blogs/danielgrunwald/archive/2011/04/26/ilspy-decompiler-architecture-overview.aspx but neither gave me the needed info to start, without digging through ILSpy's assemblies.

I guess I can make a tool that combines it all, usable on any assembly. But it should have been done via StyleCop, but it was such a hassle to debug.

Coordinator
Jul 6, 2011 at 2:18 PM

Thanks for doing this research,

I dont know if I could ever get to it myself.

Just a suggestion, when you think about this tool, think if its possible to do as a add-in to VS.NET so it could anaylize current assembly and add comments to the method, this will eliminate any hand work we need to do later to copy this information into documention

and also you could have nice and useful by other VS.NET add-in. Currently I am using GHostDoc add in which adds documentation automatically to the source code, so it could be something like that for example.

 

Also, in the future, when you create a discussion, you can check this option "Discussion will only be visible to project team members." then it only will be visible by project developers.

 

Thanks,

Oleg

Jul 6, 2011 at 4:16 PM

Hi

Yes, like to come up with these weird tools from time to time :o

I'm not familiar with creating a VS addin, but I will look into it, but sounds like a very good idea.

And I purposefully marked this public, as it might be relevant for others seeking a similar soloution - being open source and all ;-)

Jul 7, 2011 at 5:39 PM
Edited Jul 7, 2011 at 5:42 PM

BREAKING NEWS! :D

Fyi, the method, specifically....

foreach (MemberMapping memberMapping in astBuilder.CodeMappings.Item2)

...to do traversal doesnt work. Had to iterate via astBuilder.ComplationUnit. Anyway, after that hold-up I got some actual (and so far reliable) result data. I used the method in Session.Connect() as it was a reliable method, since there were no docs or anything, and it throws alot.  So without wasting a minute longer, this is what I have cooked together for this specific method (the whole file is 40KB., for all SSH.Net methods/props.)

  <ExceptionDocumentationResult>
    <Method>System.Void Renci.SshNet.Session::Connect()</Method>
    <DocumentedException />
    <ActualException>
      <string>System.ArgumentNullException</string>
      <string>System.InvalidOperationException</string>
      <string>Renci.SshNet.Common.SshConnectionException</string>
      <string>Renci.SshNet.Common.SshException</string>
      <string>Renci.SshNet.Common.SshAuthenticationException</string>
    </ActualException>
  </ExceptionDocumentationResult>

All of the above exceptions are thrown only in Connect(). And it flags them because the documentation does not include <exception>-tags for each respective exception type. This will help greatly finding missing documentation for exceptions thrown, so that devs using this library can rely on our docs.

For those who doesnt bother look through the code, here is that very method.

        /// <summary>
        /// Connects to the server.
        /// </summary>
        public void Connect()
        {
            if (this.ConnectionInfo == null)
            {
                throw new ArgumentNullException("connectionInfo");
            }

            if (this.IsConnected)
                return;

            try
            {
                _authenticationConnection.Wait();

                if (this.IsConnected)
                    return;

                lock (this)
                {
                    //  If connected don't connect again
                    if (this.IsConnected)
                        return;

                    var ep = new IPEndPoint(Dns.GetHostAddresses(this.ConnectionInfo.Host)[0], this.ConnectionInfo.Port);
                    this._socket = new Socket(ep.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

                    var socketBufferSize = 2 * MAXIMUM_PACKET_SIZE;
                    this._socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, true);
                    this._socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.SendBuffer, socketBufferSize);
                    this._socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveBuffer, socketBufferSize);

                    //  Connect socket with 5 seconds timeout
                    var connectResult = this._socket.BeginConnect(ep, null, null);

                    connectResult.AsyncWaitHandle.WaitOne(this.ConnectionInfo.Timeout);

                    //  Build list of available messages while connecting
                    this._messagesMetadata = (from type in this.GetType().Assembly.GetTypes()
                                              from messageAttribute in type.GetCustomAttributes(false).OfType<MessageAttribute>()
                                              select new MessageMetadata
                                              {
                                                  Name = messageAttribute.Name,
                                                  Number = messageAttribute.Number,
                                                  Enabled = false,
                                                  Activated = false,
                                                  Type = type,
                                              }).ToList();

                    this._socket.EndConnect(connectResult);

                    Match versionMatch = null;
                    //  Get server version from the server,
                    //  ignore text lines which are sent before if any
                    using (var ns = new NetworkStream(this._socket))
                    {
                        using (var sr = new StreamReader(ns))
                        {
                            while (true)
                            {
                                this.ServerVersion = sr.ReadLine();
                                if (string.IsNullOrEmpty(this.ServerVersion))
                                {
                                    throw new InvalidOperationException("Server string is null or empty.");
                                }

                                versionMatch = _serverVersionRe.Match(this.ServerVersion);

                                if (versionMatch.Success)
                                {
                                    break;
                                }
                            }
                        }
                    }

                    //  Get server SSH version
                    var version = versionMatch.Result("${protoversion}");

                    if (!(version.Equals("2.0") || version.Equals("1.99")))
                    {
                        throw new SshConnectionException(string.Format(CultureInfo.CurrentCulture, "Server version '{0}' is not supported.", version), DisconnectReason.ProtocolVersionNotSupported);
                    }

                    this.Write(Encoding.ASCII.GetBytes(string.Format(CultureInfo.InvariantCulture, "{0}\x0D\x0A", this.ClientVersion)));

                    //  Register Transport response messages
                    this.RegisterMessage("SSH_MSG_DISCONNECT");
                    this.RegisterMessage("SSH_MSG_IGNORE");
                    this.RegisterMessage("SSH_MSG_UNIMPLEMENTED");
                    this.RegisterMessage("SSH_MSG_DEBUG");
                    this.RegisterMessage("SSH_MSG_SERVICE_ACCEPT");
                    this.RegisterMessage("SSH_MSG_KEXINIT");
                    this.RegisterMessage("SSH_MSG_NEWKEYS");

                    //  Some server implementations might sent this message first, prior establishing encryption algorithm
                    this.RegisterMessage("SSH_MSG_USERAUTH_BANNER");

                    //  Start incoming request listener
                    this._messageListener = Task.Factory.StartNew(() => { this.MessageListener(); }, TaskCreationOptions.LongRunning);

                    //  Wait for key exchange to be completed
                    this.WaitHandle(this._keyExchangeCompletedWaitHandle);

                    //  If sessionId is not set then its not connected
                    if (this.SessionId == null)
                    {
                        this.Disconnect();
                        return;
                    }

                    //  Request user authorization service
                    this.SendMessage(new ServiceRequestMessage(ServiceName.UserAuthentication));

                    //  Wait for service to be accepted
                    this.WaitHandle(this._serviceAccepted);

                    if (string.IsNullOrEmpty(this.ConnectionInfo.Username))
                    {
                        throw new SshException("Username is not specified.");
                    }

                    //  Try authenticate using none method
                    using (var noneConnectionInfo = new NoneConnectionInfo(this.ConnectionInfo.Host, this.ConnectionInfo.Port, this.ConnectionInfo.Username))
                    {
                        noneConnectionInfo.Authenticate(this);

                        this._isAuthenticated = noneConnectionInfo.IsAuthenticated;

                        if (!this._isAuthenticated)
                        {
                            //  Ensure that authentication method is allowed
                            if (!noneConnectionInfo.AllowedAuthentications.Contains(this.ConnectionInfo.Name))
                            {
                                throw new SshAuthenticationException("User authentication method is not supported.");
                            }

                            //  In future, if more then one authentication methods are supported perform the check here.
                            //  Authenticate using provided connection info object
                            this.ConnectionInfo.Authenticate(this);

                            this._isAuthenticated = this.ConnectionInfo.IsAuthenticated;
                        }
                    }

                    if (!this._isAuthenticated)
                    {
                        throw new SshAuthenticationException("User cannot be authenticated.");
                    }

                    //  Register Connection messages
                    this.RegisterMessage("SSH_MSG_GLOBAL_REQUEST");
                    this.RegisterMessage("SSH_MSG_REQUEST_SUCCESS");
                    this.RegisterMessage("SSH_MSG_REQUEST_FAILURE");
                    this.RegisterMessage("SSH_MSG_CHANNEL_OPEN_CONFIRMATION");
                    this.RegisterMessage("SSH_MSG_CHANNEL_OPEN_FAILURE");
                    this.RegisterMessage("SSH_MSG_CHANNEL_WINDOW_ADJUST");
                    this.RegisterMessage("SSH_MSG_CHANNEL_EXTENDED_DATA");
                    this.RegisterMessage("SSH_MSG_CHANNEL_REQUEST");
                    this.RegisterMessage("SSH_MSG_CHANNEL_SUCCESS");
                    this.RegisterMessage("SSH_MSG_CHANNEL_FAILURE");
                    this.RegisterMessage("SSH_MSG_CHANNEL_DATA");
                    this.RegisterMessage("SSH_MSG_CHANNEL_EOF");
                    this.RegisterMessage("SSH_MSG_CHANNEL_CLOSE");

                    Monitor.Pulse(this);
                }
            }
            finally
            {
                _authenticationConnection.Release();
            }
        }
Jul 12, 2011 at 3:10 PM
Edited Jul 12, 2011 at 5:02 PM

So I soon have a "working" test version, here are some screenshots of it hacking away on Renci.Ssh.dll

http://goo.gl/SlQ3v (skydrive)

Nov 10, 2011 at 8:04 AM

I would love it if you would post your work on codeplex. I need a tool like this and would be happy to contribute to it. Is it possible?