Skip to content

Instantly share code, notes, and snippets.

@mossheim
Last active May 18, 2018 03:00
Show Gist options
  • Save mossheim/8dec64df924224913b4f60dbc5c3641d to your computer and use it in GitHub Desktop.
Save mossheim/8dec64df924224913b4f60dbc5c3641d to your computer and use it in GitHub Desktop.
SuperCollider Annotations example
// Annotations concept sketch
// Brian Heim, May 2018
// Annotations are source-level metadata about a function or class.
// The Annotations class provides a hacky way of getting runtime reflection on annotations
// by parsing source code directly. Also provided are convenience functions on Method and Class.
Annotations {
// Get the annotations for a given Method or Class object, by parsing the relevant directly.
// Conveniently, Method and Class have the same interface here, so we don't need separate methods.
// Returns a Set of annotations.
*of { |obj|
var fileText, lines, index, annotations;
try {
fileText = File.readAllString(obj.filenameSymbol.asString);
} { |e|
Error("Could not read source file for '%': %".format(obj, e.what)).throw
};
^this.prGetAnnotations(fileText, obj.charPos).as(Set);
}
// Get the annotations from source text.
// This method begins searching backwards from the line before `pos` until it reaches a line that is
// not an annotation.
*prGetAnnotations { |text, pos|
var index, lines;
var annotations = [], note;
lines = text.split($\n);
index = this.prLineBefore(lines, pos);
while { index >= 0 } {
note = this.prGetAnnotation(lines[index]);
if (note.notNil) {
annotations = annotations.add(note)
} {
^annotations
};
index = index - 1
};
^annotations
}
// Index of line before the one containing pos.
// Lines is an array of lines of text, pos is an integer index into the entire text.
*prLineBefore { |lines, pos|
var lineIndices = (lines.collect(_.size) + 1).integrate;
^lineIndices.indexInBetween(pos).floor.asInteger;
}
// Returns an annotation as a symbol if str has an annotation; otherwise, returns nil
// An annotated line begins with optional leading whitespace and a comment. The first
// non-space character in the comment must be @. The remainder of the line is the annotation.
*prGetAnnotation { |str|
var match;
match = str.findRegexp("^\\s*//\\s*@");
if (match.isEmpty) { ^nil } { ^str[match[0][1].size..].asSymbol };
}
}
// Use to test Annotations concept:
// AnnotationExample.methods.collect(_.annotations)
// AnnotationExample.annotations
//
// @AnnotatedClass
AnnotationExample {
// @Melodrama
// @JunkyNonsense
// @ExampleMethod
isAnnotated { ^thisMethod.annotations }
// @ExpectedFailure
worldPeace { ^Error("...").throw }
unannotated { ^thisMethod.annotations.postln }
}
TestAnnotations : UnitTest {
test_class {
this.assertEquals(AnnotationExample.annotations, Set['AnnotatedClass'])
}
test_method_oneAnnotation {
this.assertEquals(AnnotationExample.findMethod('worldPeace').annotations, Set['ExpectedFailure'])
}
test_method_multiAnnotation {
var expected = Set['Melodrama', 'JunkyNonsense', 'ExampleMethod'];
this.assertEquals(AnnotationExample.findMethod('isAnnotated').annotations, expected)
}
test_method_unannotated {
this.assertEquals(AnnotationExample.findMethod('unannotated').annotations, Set[])
}
}
+ Method {
// List of annotations for this method
annotations {
^Annotations.of(this)
}
// Whether this method has the given annotation
hasAnnotation { |annotation|
^this.annotations.includes(annotation);
}
}
+ Class {
// List of annotations for this method
annotations {
^Annotations.of(this)
}
// Whether this class has the given annotation
hasAnnotation { |annotation|
^this.annotations.includes(annotation);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment