answer420741

There actually is a way to get this information that should work in many cases, using %Studio.Debugger:SourceLine, but it's not really easy. Note that %Studio.Debugger comes with the warning:

This class is used internally by Caché. You should not make direct use of it within your applications. There is no guarantee made about either the behavior or future operation of this class.

That said, here's a quick sample:

Class DC.Demo.SourceLine
{

ClassMethod Run()
{
    #; Here are a few comments to screw things up.
    Try {
        #; More comments!
        Write "Hello world",!
        Write 1/0
    } Catch e {
        Do ..HandleException(e)
    }
}

ClassMethod HandleException(pException As %Exception.AbstractException)
{
    Write "Exception occurred: ",pException.DisplayString(),!
    
    //Example value if e.Location (from above error): zRun+3^DC.Demo.SourceLine.1
    Set tSC = ..GetClassSourceLine(pException.Location,.tClsLocation)
    If $$$ISERR(tSC) {
        Write $System.Status.GetErrorText(tSC),!
    } else {
        Write ".INT error location: "_pException.Location,!
        Write ".CLS error location: "_tClsLocation,!
    }
}

ClassMethod GetClassSourceLine(pIntLocation As %String, Output pClsLocation As %String) As %Status
{
    Set tStatus = $$$OK
    Set pClsLocation = ""
    Try {
        Set tMethodAndLine = $Piece(pIntLocation,"^",1)
        Set tIntName = $Piece(pIntLocation,"^",2)
        Set tTag = $Piece(tMethodAndLine,"+")
        Set tRelativeOffset = $Piece(tMethodAndLine,"+",2)
        
        // Get routine text to find the absolute offset of tTag
        Set tTagOffset = 0
        Set tEndPos = 0
        Set tTextLines = 0
        For {
            Set tLine = $Text(@("+"_$Increment(tTextLines)_"^"_tIntName))
            Quit:tLine=""
            
            // Example:
            // zRun() public {
            // This relies on an assumption that methods will be sorted alphabetically and won't contain labels.
            If $Extract(tLine,1,$Length(tTag)) = tTag {
                Set tTagOffset = tTextLines //tTextLines is the counter.
                Set tEndPos = $Length(tLine)
                Quit
            }
        }
        
        // The absolute offset of the line in the .int file is the tag's offset plus the offset within it.
        Set tOffset = tTagOffset + tRelativeOffset
        Set tStatus = ##class(%Studio.Debugger).SourceLine(tIntName,tOffset,0,tOffset,tEndPos,,.tMap)
        If $$$ISERR(tStatus) {
            Quit
        }
        If $Data(tMap("CLS",1)) {
            Set $ListBuild(tClass,tMethod,tLine,tEndPos,tNamespace) = tMap("CLS",1)
            Set pClsLocation = tClass_":"_tMethod_"+"_tLine
        }
    } Catch e {
        Set tStatus = e.AsStatus()
    }
    Quit tStatus
}

}

And sample output:

USER>d ##class(DC.Demo.SourceLine).Run()
Hello world
Exception occurred: <DIVIDE> 18 zRun+3^DC.Demo.SourceLine.1
.INT error location: zRun+3^DC.Demo.SourceLine.1
.CLS error location: DC.Demo.SourceLine:Run+5

If an exception occurs in generated code, I'd expect this to be useless.

Perfect! Thank you Timothy.

Don't you know by chance how can I get source line from CLS code location? $TEXT seems to work on INT code only.

Here's one way, using some macros from %occKeyword.inc/%occReference.inc (for tClass/tMethod/tLine in the above example):

Set tClsCode = $$$comMemberArrayGet(tClass,$$$cCLASSmethod,tMethod,$$$cMETHimplementation,tLine)

Note that this ends up looking at the class definition, which may not actually be the same as what's compiled. (However, used in this context, there are bigger problems in that case; it seems that the class->routine code mapping from %Studio.Debugger:SourceLine is invalidated if the class is changed but not compiled. This makes sense, but is definitely something to keep in mind if you're using it.)

I'm curious - what are you trying to do with this? smiley

I am implementing my own minimalistic test runner that runs all defined tests every time the class is compiled without any further interaction needed from the user. Test failures are currently being reported with reference to INT code where the error occurred, which means I have to backtrack every failure to the original code. It would be easier if I could reference directly the CLS file :)

Thank you! That's very useful.

Is there a way to return real implementation class?

For example, consider these classes:

Class App.Use {

ClassMethod Test()
{
    w 1/0
}
}

and:

Class App.Use2 Extends App.Use
{
}

If I call:

do ##class(App.Use2).Test()

I get  the following CLS source line:

App.Use2:Test+1

Yet, the relevant code is actually implemented in

App.Use:Test+1

One approach I see is checking %Dictionary.MethodDefinition recursively till I find a method definition, but there's bound to be many problems multiple inheritance.

Yes - this is in the compiled class metadata.

$$$comMemberKeyGet("App.Use2",$$$cCLASSmethod,"Test",$$$cMETHorigin)

Or see the Origin property in %Dictionary.CompiledMethod (same for other class members as well).

Also, if I plug:

do ##class(App.Use2).Test()

into the original code sample, I see App.Use:Test+1 in the Location property of the exception. How are you ending up with App.Use2:Test+1?