This is an annotation package for TclOO. See the example code at the end for how to use and a bit above that for how to derive new annotations.
Still to do:
package require Tcl 8.6
package require TclOO
package provide TclOO::annotate
namespace eval ::oo::Annotations {
variable classInfo
array set classInfo {}
variable objInfo
array set objInfo {}
proc ::oo::InfoClass::annotations {className {annotationName {}} args} {
variable ::oo::Annotations::classInfo
set className [uplevel 1 [list namespace which $className]]
if {[llength [info level 0]] == 2} {
if {![info exists classInfo($className)]} {
return {}
}
return [dict keys $classInfo($className)]
}
if {
![info exists classInfo($className)]
|| ![dict exists $classInfo($className) $annotationName]
} then {
return -code error "unknown annotation \"$annotationName\""
}
set result {}
foreach handlerObj [dict get $classInfo($className) $annotationName] {
try {
$handlerObj describe result {*}$args
} on error msg {
return -code error $msg
}
}
return $result
}
proc ::oo::InfoObject::annotations {objectName {annotationName {}} args} {
variable ::oo::Annotations::objInfo
set objectName [uplevel 1 [list namespace which $objectName]]
if {$annotationName eq ""} {
if {![info exists objInfo($objectName)]} {
return {}
}
return [dict keys $objInfo($objectName)]
}
if {
![info exists objInfo($objectName)]
|| ![dict exists $objInfo($objectName) $annotationName]
} then {
return -code error "unknown annotation \"$annotationName\""
}
set result {}
foreach handlerObj [dict get $objInfo($objectName) $annotationName] {
try {
$handlerObj describe result {*}$args
} on error msg {
return -code error $msg
}
}
return $result
}
variable globalAnnotations {}
proc ::unknown {args} [concat {
if {[string match @* [lindex $args 0]]} {
lappend ::oo::Annotations::globalAnnotations $args
return
};
} [info body ::unknown]]
::oo::define ::oo::class constructor {{definitionScript {}}} {\
set script [list ::oo::define [self] $definitionScript];\
::oo::Annotations::AttachGlobalAnnotations [self];\
lassign [::oo::UpCatch $script] msg opts
if {[dict get $opts -code] == 1} {
dict set opts -errorline 0xDeadBeef
}
return -options $opts $msg
}
proc AttachGlobalAnnotations cls {
variable globalAnnotations
variable classInfo
set ga $globalAnnotations
set globalAnnotations {}
set clsAnnotations {}
try {
foreach annotationSpec $ga {
set annotationType \
[string range [lindex $annotationSpec 0] 1 end]
set args [lrange $annotationSpec 1 end]
lappend clsAnnotations \
[Annotation.$annotationType new class {*}$args]
}
} on error {msg opt} {
foreach a $clsAnnotations {
$a destroy
}
return -options $opt $msg
}
foreach a $clsAnnotations {
set name [$a name]
$a register class {}
if {
![info exists classInfo($cls)]
|| ![dict exists $classInfo($cls) $name]
} then {
dict set classInfo($cls) $name {}
}
dict lappend classInfo($cls) $name $a
}
}
proc DefineUnknown {cmd args} {
if {[string match @* $cmd]} {
try {
AnnotateClass [lindex [info level -1] 1] \
[string range $cmd 1 end] {*}$args
return
} on error msg {
return -code error $msg
}
}
tailcall ::oo::UnknownDefinition $cmd {*}$args
}
proc ObjdefineUnknown {cmd args} {
if {[string match @* $cmd]} {
if {[lindex [info level -1] 0] eq "self"} {
set obj [lindex [info level -2] 1]
} else {
set obj [lindex [info level -1] 1]
}
try {
AnnotateObject $obj [string range $cmd 1 end] {*}$args
return
} on error msg {
return -code error $msg
}
}
tailcall ::oo::UnknownDefinition $cmd {*}$args
}
namespace eval ::oo::define [list \
namespace unknown [namespace which DefineUnknown]]
namespace eval ::oo::objdefine [list \
namespace unknown [namespace which ObjdefineUnknown]]
namespace eval RealDefines {}
namespace eval RealObjDefines {}
apply [list {} {
foreach cmd [info commands ::oo::define::*] {
set tail [namespace tail $cmd]
set target ::oo::Annotations::RealDefines::$tail
rename $cmd $target
proc $cmd args "
::oo::Annotations::ClassDefinition $tail {*}\$args
tailcall [list $target] {*}\$args
"
}
foreach cmd [info commands ::oo::objdefine::*] {
set tail [namespace tail $cmd]
set target ::oo::Annotations::RealObjDefines::$tail
rename $cmd $target
proc $cmd args "
::oo::Annotations::ObjectDefinition $tail {*}\$args
tailcall [list $target] {*}\$args
"
}
} [namespace current]]
variable subject
variable currentAnnotators
proc AnnotateClass {subjectClass annotationType args} {
variable subject $subjectClass
variable currentAnnotators
lappend currentAnnotators \
[Annotation.$annotationType new class {*}$args]
}
proc AnnotateObject {subjectObject annotationType args} {
variable subject $subjectObject
variable currentAnnotators
lappend currentAnnotators \
[Annotation.$annotationType new object {*}$args]
}
proc ClassDefinition {operation args} {
variable currentAnnotators
if {![info exists currentAnnotators]} {
return
}
variable subject
variable classInfo
try {
foreach a $currentAnnotators {
set name [$a name]
$a register $operation {*}$args
if {
![info exists classInfo($subject)]
|| ![dict exists $classInfo($subject) $name]
} then {
dict set classInfo($subject) $name {}
}
dict lappend classInfo($subject) $name $a
set currentAnnotators [lrange $currentAnnotators 1 end]
}
} on error msg {
foreach a $currentAnnotators {
$a destroy
}
return -level 2 $msg
} finally {
unset currentAnnotators
}
}
proc ObjectDefinition {operation args} {
variable currentAnnotators
if {![info exists currentAnnotators]} {
return
}
variable subject
variable objInfo
try {
foreach a $currentAnnotators {
set name [$a name]
$a register $operation {*}$args
if {
![info exists objInfo($subject)]
|| ![dict exists $objInfo($subject) $name]
} then {
dict set objInfo($subject) $name {}
}
dict lappend objInfo($subject) $name $a
set currentAnnotators [lrange $currentAnnotators 1 end]
}
} on error msg {
foreach a $currentAnnotators {
$a destroy
}
return -level 2 $msg
} finally {
unset currentAnnotators
}
}
::oo::class create annotation {
unexport create
variable annotation Type Operation
constructor {type args} {
set Type $type
my MayApplyToType $type
my RememberAnnotationArguments $args
}
method MayApplyToType type {
throw ANNOTATION {may not apply this annotation to that type}
}
method MayApplyToOperation operation {
throw ANNOTATION {may not apply that annotation to this operation}
}
method RememberAnnotationArguments values {
set annotation $values
}
method QualifyAnnotation args {
# Do nothing by default
}
method name {} {
set name [namespace tail [info object class [self]]]
return [regsub {^Annotation.} $name @]
}
method register {operation args} {
set Operation $operation
my MayApplyToOperation $operation
my QualifyAnnotation {*}$args
}
method describe {varName} {
upvar 1 $varName v
lappend v $annotation
}
}
::oo::class create classannotation {
superclass annotation
method MayApplyToType type {
if {$type ne "class"} {next $type}
}
}
::oo::class create objectannotation {
superclass annotation
method MayApplyToType type {
if {$type ne "object"} {next $type}
}
}
}
namespace eval ::oo::Annotations {
oo::class create Annotation.Describe {
superclass oo::Annotations::classannotation \
oo::Annotations::objectannotation
variable annotation Operation method
method MayApplyToOperation operation {
if {$operation ni {method forward constructor class}} {
next $operation
}
}
method QualifyAnnotation {name args} {
if {$Operation eq "constructor"} {
set method <<constructor>>
} else {
set method $name
}
}
method describe {varName {name {}}} {
upvar 1 $varName v
if {[llength [info level 0]] == 3} {
dict set v $method [join $annotation]
} elseif {$method eq $name} {
set v [join $annotation]
return -code break
}
}
}
oo::class create Annotation.Result {
superclass Annotation.Describe
method MayApplyToOperation operation {
if {$operation eq "constructor"} {
throw ANNOTATE "not on a constructor"
}
next $operation
}
}
oo::class create Annotation.SideEffects {
superclass Annotation.Describe
}
oo::class create Annotation.Argument {
superclass oo::Annotations::classannotation \
oo::Annotations::objectannotation
variable annotation Operation method argument
constructor {type argName args} {
set argument $argName
next $type {*}$args
}
method MayApplyToOperation operation {
if {$operation ni {method forward constructor}} {next $operation}
}
method QualifyAnnotation {name args} {
if {$Operation eq "constructor"} {
set method <<constructor>>
} else {
set method $name
}
}
method describe {varName {name {}} {argname {}}} {
upvar 1 $varName v
if {[llength [info level 0]] == 3} {
lappend v $method
} elseif {$method eq $name} {
if {[llength [info level 0]] == 4} {
lappend v $argument
} elseif {$argument eq $argname} {
set v [join $annotation]
return -code break
}
}
}
}
}@Describe This is a whole class annotation!
oo::class create example {
@Describe This is the foo method. It has multiple annotations \
attached to it.
@Argument x The first argument to the method.
@Argument args The list of remaining arguments to the method.
@Result None.
@SideEffects Prints to stdout.
method foo {x args} { puts foo }
@Describe This is the bar method.
method bar args { puts bar }
}
puts annotations:\t[info class annotation example]
puts example-desc:\t[info class annotation example @Describe {}]
puts desc-foo:\t[info class annotation example @Describe]
puts desc-foo:\t[info class annotation example @Describe foo]
puts args-foo:\t[info class annotation example @Argument foo]
foreach a [info class annotation example @Argument foo] {
puts args-foo-${a}:\t[info class annotation example @Argument foo $a]
}
puts result-foo:\t[info class annotation example @Result foo]
puts effects-foo:\t[info class annotation example @SideEffects foo]The output from this is:
annotations: @Describe @Argument @Result @SideEffects
example-desc: This is a whole class annotation!
desc-foo: {} {This is a whole class annotation!} foo {This is the foo method. It has multiple annotations attached to it.} bar {This is the bar method.}
desc-foo: This is the foo method. It has multiple annotations attached to it.
args-foo: x args
args-foo-x: The first argument to the method.
args-foo-args: The list of remaining arguments to the method.
result-foo: None.
effects-foo: Prints to stdout.