Reporting XSLT compilation errors in .NET

| 4 Comments | No TrackBacks

Reporting errors in XSLT stylesheets is a task that almost nobody gets done right. Including me - error reporting in nxslt sucks in a big way. Probably that's because I'm just lazy bastard. But also lets face it - XslCompiledTransform API doesn't help here.

Whenever there are XSLT loading (compilation) errors XslCompiledTransform.Load() method throws an XsltException containing description of the first error encountered by the compiler. But as a matter of fact internally XslCompiledTransform holds list of all errors and warnings (internal Errors property). It's just kept internal who knows why. Even Microsoft own products such as Visual Studio don't use this important information when reporting XSLT errors - Visual Studio's XML editor also displays only first error. That sucks.

Anyway here is a piece of code written by Anton Lapounov, one of the guys behind XslCompiledTransform. It shows how to use internal Errors list via reflection (just remember you would need FullTrust for that) to report all XSLT compilation errors and warnings. The code is in the public domain - feel free to use it.  I'm going to incorporate it into the next nxslt release. I'd modify it a little bit though - when for some reason (e.g. insufficient permissions) errors info isn't available you still have XsltException with at least first error info.

private void Run(string[] args) {
    XslCompiledTransform xslt = new XslCompiledTransform();
    try {
        xslt.Load(args[0]);
    }
    catch (XsltException) {
        string errors = GetCompileErrors(xslt);
        if (errors == null) {
            // Failed to obtain list of compile errors
            throw;
        }
        Console.Write(errors);
    }
}

// True to output full file names, false to output user-friendly file names
private bool fullPaths = false;

// Cached value of Environment.CurrentDirectory
private string currentDir = null;

/// 
/// Returns user-friendly file name. First, it tries to obtain a file name
/// from the given uriString.
/// Then, if fullPaths == false, and the file name starts with the current
/// directory path, it removes that path from the file name.
/// 
private string GetFriendlyFileName(string uriString) {
    Uri uri;

    if (uriString == null ||
        uriString.Length == 0 ||
        !Uri.TryCreate(uriString, UriKind.Absolute, out uri) ||
        !uri.IsFile
    ) {
        return uriString;
    }

    string fileName = uri.LocalPath;

    if (!fullPaths) {
        if (currentDir == null) {
            currentDir = Environment.CurrentDirectory;
            if (currentDir[currentDir.Length - 1] != Path.DirectorySeparatorChar) {
                currentDir += Path.DirectorySeparatorChar;
            }
        }

        if (fileName.StartsWith(currentDir, StringComparison.OrdinalIgnoreCase)) {
            fileName = fileName.Substring(currentDir.Length);
        }
    }

    return fileName;
}

private string GetCompileErrors(XslCompiledTransform xslt) {
    try {
        MethodInfo methErrors = typeof(XslCompiledTransform).GetMethod(
            "get_Errors", BindingFlags.NonPublic | BindingFlags.Instance);

        if (methErrors == null) {
            return null;
        }

        CompilerErrorCollection errorColl = 
            (CompilerErrorCollection) methErrors.Invoke(xslt, null);
        StringBuilder sb = new StringBuilder();

        foreach (CompilerError error in errorColl) {
            sb.AppendFormat("{0}({1},{2}) : {3} {4}: {5}",
                GetFriendlyFileName(error.FileName),
                error.Line,
                error.Column,
                error.IsWarning ? "warning" : "error",
                error.ErrorNumber,
                error.ErrorText
            );
            sb.AppendLine();
        }
        return sb.ToString();
    }
    catch {
        // MethodAccessException or SecurityException may happen 
        //if we do not have enough permissions
        return null;
    }
}

Feel the difference - here is nxslt2 output:

An error occurred while compiling stylesheet 'file:///D:/projects2005/Test22/Test22/test.xsl': 
System.Xml.Xsl.XslLoadException: Name cannot begin with the '1' character, hexadecimal value 0x31.

And here is Anton's code output:

test.xsl(11,5) : error : Name cannot begin with the '1' character, hexadecimal value 0x31.
test.xsl(12,5) : error : Name cannot begin with the '0' character, hexadecimal value 0x30.
test.xsl(13,5) : error : The empty string '' is not a valid name.
test.xsl(14,5) : error : The ':' character, hexadecimal value 0x3A, cannot be included in a name.
test.xsl(15,5) : error : Name cannot begin with the '-' character, hexadecimal value 0x2D.

Related Blog Posts

No TrackBacks

TrackBack URL: http://www.tkachenko.com/cgi-bin/mt-tb.cgi/638

4 Comments

(Sorry in advance if this is a double-post -- I tried posting the comment earlier from within the news reader, and I never can tell if it works or not.)

I suppose I don't have to mention how inherently unstable such a solution is - I mean, it's nice to be able to access all errors, but as Torsten found out, it's a very implementation-specific solution that's bound to break between versions, or even merely because of unexpected circumstances.

I suggest a feature request to Microsoft instead, however being the cynic that I am I find it difficult to believe that it'll actually be implemented...

I got figured out: the erroColl seems to be populated only at .Transform() call time, and only sometimes at load time. Also, XML errors while loading (unclosed elements, etc.) are not populated in the errColl, there I get the usual exceptions...

Hmmm. That might be security issue. Try to modify last lines to
catch (Exception ex){
Console.WriteLine(ex);
return null;
}

And inspect what kind of exception do you get.

I tried to get the code work - however, the errorColl is always null. Do I miss something important? I also tried to create the XsltCompiledTransform with debugEnabled = true, no difference.

Leave a comment