Querying Pharo code with GT filters
TL;DR
Filters are composable queries.
This page shows you how to compose GT queries and filters to find methods and classes in the system that satisfy complex search criteria.
Introduction
Glamorous Toolkit is implemented in Pharo (see Glamorous Toolkit and Pharo). Of course, it also comes with the tools necessary for making sense of Pharo code as well. One mechanism is related to querying code. This comes in the form of a fluent API that can used for scripting.
What are queries?
One of the design principles behind Glamorous Toolkit is that search must be universal.
Queries are snippets of code, written in plain Pharo, that we can use to get information about a package, a class, a method or an object that satisfies one or more conditions.
Let’s see an example: a search for all the methods annotated with the #gtView
pragma.
#gtView gtPragmas.
Executing the query opens up an inspector on the result, which in this case is an instance of GtSearchPragmasFilter
. The filter instance knows how to present itself through a view that shows method widgets. The method widgets know how to match the input query to the abstract syntax tree nodes to produce the orange highlighting of the matched nodes.
But what if we want to search for multiple conditions at the same time? Well, queries can be composed using &
and |
messages, making this easily extensible.
Let’s see an example: a search for all the methods that are annotated with the #gtView
pragma and implement a tree view.
#gtView gtPragmas & #tree gtReferences.
Now let’s search for those methods above, in addition to those that implement a columnedTree view.
#gtView gtPragmas & (#tree gtReferences | #columnedTree gtReferences).
Since queries are just plain Pharo code, we can combine them with Pharo methods like select:
, collect:
and others.
For example, this code snippet returns all example methods from packages starting with 'GToolkit'
.
#gtExample gtPragmas select: [ :method | method package name beginsWith: 'GToolkit'].
Earlier we mentioned filters helping us to work with queries.
What are filters?
Filters are predicates that work as conditions in a query.
For example, let's search for certain methods in a certain package. The result will be an instance of GtSearchIntersectionFilter
, being the intersection of two filters: GtSearchReferencesFilter
and GtSearchMethodsWithPackagePatternFilter
.
#todo gtReferences & 'Bloc' gtPackageMatches.
A number of such predicates are readily available. Below is a diagram of the GtSearchFilter
class hierarchy.
More Filters
Now let's take a closer look at the filters in the diagram above.
Binary
The intersection filter is a result of sending the &
message. The result is a collection of elements that satisfy both the left side and the right side of the query.
intersectionDifferentFilters <gtExample> | filter1 filter2 intersection | filter1 := #gtExample gtPragmas. filter2 := #filter gtReferences. intersection := filter1 & filter2. self assert: (intersection matches: thisContext method). ^ intersection.
The union filter is a result of sending the |
message. The result is a collection of elements that satisfy at least one of the query parts.
unionDifferentMethods <gtExample> | filter1 filter2 union | filter1 := thisContext method selector gtImplementors. filter2 := #foo gtImplementors. union := filter1 | filter2. self assert: union size = (filter1 size + filter2 size). self assert: (union matches: thisContext method). ^ union
Packages
These filters allow us to see all the packages in the current image:
packagesInImageFilter <gtExample> | filter | filter := GtSearchImagePackagesFilter new. self assert: (filter matches: 'GToolkit-Coder-Examples' asPackage). self assert: (filter matches: (RPackage named: 'A-Package-ThatDoesNotExist')) not. self assert: filter defaultFilterScope equals: AsyncImagePackagesStream new. self assertPackageResultTypeForFilter: filter. ^ filter
and the packages that are deprecated.
deprecatedPackagesFilter <gtExample> | filter | filter := GtSearchDeprecatedPackagesFilter new. "self assert: (filter matches: (RPackageOrganizer default packages select: [ :aPackage | aPackage name beginsWith: 'Spec2-ObservableSlot']) anyOne)." self assert: (filter matches: 'GToolkit-Docs' asPackage) not. ^ filter.
Classes
Using these we can search the classes in the current image,
classesInImageFilter <gtExample> | filter | filter := GtSearchImageClassesFilter new. self assert: (filter matches: GtSystemS1SubclassC2). self assert: (filter matches: Object). ^ filter
in a certain package,
classesInCurrentPackageFilter <gtExample> | filter | filter := GtSearchClassesInPackageFilter forPackageNamed: 'GToolkit-Coder-Examples-SystemS1'. self assert: (filter matches: GtSystemS1SubclassC2). self assert: (filter matches: Object) not. ^ filter
and the deprecated classes.
deprecatedClassesFilter <gtExample> | filter | filter := GtSearchDeprecatedClassesFilter new. self assert: (filter matches: GtSystemS1AnotherClassC3). self assert: (filter matches: GtSystemS1RootClassC1) not. ^ filter
Methods
These allow us to perform various searches related to methods. For example, we can search for methods in a certain package,
methodsInPackageFilter <gtExample> | filter | filter := GtSearchMethodsInPackageFilter new package: (thisContext method package). self assert: filter notEmpty. self assert: (filter matches: thisContext method). ^ filter.
within a certain category,
methodsInCategoryFilter <gtExample> | filter | filter := GtSearchMethodCategoryFilter forCategory: 'examples'. self assert: filter notEmpty. self assert: (filter matches: thisContext method). ^ filter.
or methods that are deprecated.
deprecatedMethodsFilter <gtExample> | filter | filter := GtSearchDeprecatedMethodsFilter new. self assert: filter notEmpty. self assert: (filter matches: BlElement>>#extent:). self assert: (filter matches: BlElement>>#margin) not. ^ filter.
Inside a class, we can also search for instance side methods,
methodsInClassFilter <gtExample> ^ self testFilter: (GtSearchMethodsInClassFilter forClass: GtSystemS1SubclassC2) shouldInclude: {GtSystemS1SubclassC2 >> #stubMethodInClassC2S1} shouldExclude: {GtSystemS1SubclassC2 class >> #stubMethodOnClassSideOfC2S1}
for both instance and class side methods,
methodsInClassOnBothSidesFilter <gtExample> ^ self testFilter: (GtSearchMethodsInInstanceAndClassSideFilter forClass: GtSystemS1SubclassC2) shouldInclude: {GtSystemS1SubclassC2 >> #stubMethodInClassC2S1. GtSystemS1SubclassC2 class >> #stubMethodOnClassSideOfC2S1} shouldExclude: {}
or for methods that use a certain instance variable.
instanceSlotsFilter <gtExample> | aFilter | aFilter := GtSearchInstanceSlotReferenceFilter forClass: HashedCollection andVariable: 'array'. self assert: (aFilter matches: HashedCollection >> #grow). self assert: (aFilter matches: HashedCollection >> #union:) not. self assert: (aFilter matches: HashedCollection class >> #empty) not. ^ aFilter
Through these method filters we can search different patterns. For example, we can search for methods that have a certain pattern in the source code,
astPatternFilter <gtExample> | filter | filter := 'filter matches: ``@a' gtASTMatches. self assert: filter notEmpty. self assert: (filter matches: thisContext method). ^ filter
or in the name.
implementorsPatternFilter <gtExample> | filter | filter := GtSearchImplementorPatternFilter pattern: #implementorsPattern. self assert: filter notEmpty. self assert: (filter matches: thisContext method). ^ filter.
At the same time we can search for methods that are in a class with a certain pattern,
methodsWithClassPatternFilter <gtExample> | filter | filter := GtSearchMethodsWithClassPatternFilter pattern: #FilterExamples. self assert: filter notEmpty. self assert: (filter matches: thisContext method). ^ filter.
or in a package with a certain pattern.
packagePatternFilter <gtExample> | filter | filter := 'coder-examples' gtPackageMatches. self assert: filter notEmpty. self assert: (filter matches: thisContext method). ^ filter
It is also possible to search methods by a name,
implementorsOfThisMessage <gtExample> | filter | filter := #implementorsOfThisMessage gtImplementors. self assert: filter notEmpty. self assert: (filter matches: thisContext method). ^ filter
or by the annotation
gtExamplePragmas <gtExample> | filter | filter := #gtExample gtPragmas. self assert: filter notEmpty. self assert: (filter matches: thisContext method). ^ filter
and to search all the methods that reference something: a piece of code, another method or a class.
referencesTo42 <gtExample> | filter | filter := 42 gtReferences. self assert: filter notEmpty. self assert: (filter matches: GtFilterStubSampleB >> #methodReferencing42). ^ filter
Negation
This filter takes another filter and does the opposite of that filter. For example, if we search for all deprecated packages and apply this filter, we end up with all the packages that are not deprecated.
negationSearchFilter <gtExample> | basicFilter negationFilter | basicFilter := GtSearchDeprecatedPackagesFilter new. negationFilter := basicFilter not. self assert: (negationFilter notEmpty). self assert: (negationFilter matches: 'GToolkit-Coder-Examples' asPackage). ^ negationFilter.
Null
The Null filter always returns false, not matching anything.
nullSearchFilter <gtExample> | filter | filter := GtSearchNullFilter new. self assert: (filter matches: thisContext method) not. self assert: (filter matches: 'GToolkit-Coder' asPackage) not. self assert: (filter isEmpty). ^ filter.