Introducing the C# libraries

Overview

The C# QL libraries are a data model for analysis of C# code. QL is an object-oriented language, so the data model is represented as QL classes, which are organized into QL libraries. The QL classes are a layer of logic built on top of an underlying database.

The core library is imported at the top of each query using:

import csharp

Since this is required for all C# queries, it is omitted from QL snippets below.

The core library contains all the program elements, including files, types, methods, variables, statements and expressions. This is sufficient for most queries, however additional libraries can be imported for bespoke functionality such as control flow and data flow. See QL for C# for information about these additional libraries.

Class hierarchies

Each section contains a QL class hierarchy, showing the inheritance structure between QL classes. For example:

  • Expr
    • Operation
      • ArithmeticOperation
        • UnaryArithmeticOperation
          • UnaryMinusExpr, UnaryPlusExpr
          • MutatorOperation
            • IncrementOperation
              • PreIncrExpr, PostIncrExpr
            • DecrementOperation
              • PreDecrExpr, PostDecrExpr
        • BinaryArithmeticOperation
          • AddExpr, SubExpr, MulExpr, DivExpr, RemExpr

This means that the class AddExpr extends class BinaryArithmeticOperation, which in turn extends class ArithmeticOperation and so on. If you want to query any arithmetic operation, then use the class ArithmeticOperation, but if you specifically want to limit the query to addition operations, then use the class AddExpr.

QL classes can also be considered to be sets, and the extends relation between classes defines a subset. Every member of class AddExpr is also in the class BinaryArithmeticOperation. In general, QL classes overlap and an entity can be a member of several classes.

This overview omits some of the less important or intermediate classes from the class hierarchy.

Each class has “predicates”, which are logical propositions about that class. They also define navigable relationships between classes. Predicates are inherited, so for example the AddExpr class inherits the predicates getLeftOperand() and getRightOperand() from BinaryArithmeticOperation, and getType() from class Expr. This is similar to how methods are inherited in object-oriented programming languages.

In this overview, we present the most common and useful predicates. Consult the reference, QL source code, and autocomplete in the editor for the complete list of predicates available on each class.

Exercises

Each section in this topic contains exercises to check your understanding.

Exercise 1: Simplify the following query:

from BinaryArithmeticOperation op
where op instanceof AddExpr
select op

(Answer)

Files

Files are represented by the QL class File, and directories by the QL class Folder. The database contains all of the source files and assemblies used during the compilation.

Class hierarchy

  • File - any file in the database (including source files, XML and assemblies)
    • SourceFile - a file containing source code
  • Folder - a directory

Predicates

  • getName() - gets the full path of the file (for example, C:\Temp\test.cs)
  • getNumberOfLines() - gets the number of lines (for source files only)
  • getShortName() - gets the name of the file without the extension (for example, test)
  • getBaseName() - gets the name and extension of the file (for example, test.cs)
  • getParent() - gets the parent directory

Examples

Count the number of source files:

select count(SourceFile f)

Count the number of lines of code, excluding the directory external:

select sum(SourceFile f |
  not exists(Folder external | external.getShortName() = "external" |
             external.getAFolder*().getAFile() = f) |
  f.getNumberOfLines())

Exercises

Exercise 2: Write a query to find the source file with the largest number of lines. Hint: Find the source file with the same number of lines as the max number of lines in any file. (Answer)

Elements

The class Element is the base class for all parts of a C# program, and it is the root of the element class hierarchy. All program elements (such as types, methods, statements and expressions) ultimately derive from this common base class.

Element forms a hierarchical structure of the program, which can be navigated using the getParent() and getChild() predicates. This is much like an abstract syntax tree, and also applies to elements in assemblies.

Predicates

The Element class provides common functionality for all program elements, including:

  • getLocation() - gets the text span in the source code
  • getFile() - gets the File containing the Element
  • getParent() - gets the parent Element, if any
  • getAChild() - gets a child Element of this element, if any

Examples

To list all elements in Main.cs, their QL class and location:

from Element e
where e.getFile().getShortName() = "Main"
select e, e.getAQlClass(), e.getLocation()

Note that getAQlClass() is available on all QL classes and is a useful way to figure out the QL class of something. Often the same element will have several QL classes which are all returned by getAQlClass().

Locations

Location represents a section of text in the source code, or an assembly. All elements have a Location obtained by their getLocation() predicate. A SourceLocation represents a span of text in source code, whereas an Assembly location represents a referenced assembly.

Sometimes elements have several locations, for example if they occur in both source code and an assembly. In this case, only the SourceLocation is returned.

Class hierarchy

  • Location
    • SourceLocation
    • Assembly

Predicates

Some predicates of Location include:

  • getFile() - gets the File
  • getStartLine() - gets the first line of the text
  • getEndLine() - gets the last line of the text
  • getStartColumn() - gets the column of the start of the text
  • getEndColumn() - gets the column of the end of the text

Examples

Find all elements that are one character wide:

from Element e, Location l
where l = e.getLocation()
  and l.getStartLine() = l.getEndLine()
  and l.getStartColumn() = l.getEndColumn()
select e, "This element is a single character."

Declarations

Declaration is the common class of all entities defined in the program, such as types, methods, variables etc. The database contains all declarations from the source code and all referenced assemblies.

Class hierarchy

  • Element
    • Declaration
      • Callable
      • UnboundGeneric
      • ConstructedGeneric
      • Modifiable - a declaration which can have a modifier (for example public)
        • Member - a declaration that is member of a type
      • Assignable - an element that can be assigned to
        • Variable
        • Property
        • Indexer
        • Event

Predicates

Useful member predicates on Declaration include:

  • getDeclaringType() - gets the type containing the declaration, if any
  • getName()/hasName(string) - gets the name of the declared entity
  • isSourceDeclaration() - whether the declaration is source code and is not a constructed type/method
  • getSourceDeclaration() - gets the original (unconstructed) declaration

Examples

Find declarations containing a username:

from Declaration decl
where decl.getName().regexpMatch("[uU]ser([Nn]ame)?")
select decl, "A username."

Variables

The QL class Variable represents C# variables, such as fields, parameters and local variables. The database contains all variables from the source code, as well as all fields and parameters from assemblies referenced by the program.

Class hierarchy

  • Element
    • Declaration
      • Variable - any type of variable
        • Field - a field in a class/struct
          • MemberConstant - a const field
            • EnumConstant - a field in an enum
        • LocalScopeVariable - a variable whose scope is limited to a single Callable
          • LocalVariable - a local variable in a Callable
            • LocalConstant - a locally defined constant in a Callable
          • Parameter - a parameter to a Callable

Predicates

Some common predicates on Variable are:

  • getType() - gets the Type of this variable
  • getAnAccess() - gets an expression that accesses (reads or writes) this variable, if any
  • getAnAssignedValue() - gets an expression that is assigned to this variable, if any
  • getInitializer() - gets the expression used to initialize the variable, if any

Examples

Find all unused local variables:

from LocalVariable v
where not exists(v.getAnAccess())
select v, "This local variable is unused."

Types

Types are represented by the QL class Type and consist of builtin types, interfaces, classes, structs, enums and type parameters. The database contains types from the program and all referenced assemblies including mscorlib and the .NET framework.

The builtin types (object, int, double etc.) have corresponding types (System.Object, System.Int32 etc.) in mscorlib.

Class ValueOrRefType represents defined types, such as a class, struct, interface or enum.

Class hierarchy

  • Element
    • Declaration
      • Modifiable - a declaration which can have a modifier (for example public)
        • Member - a declaration that is member of a type
          • Type - all types
            • ValueOrRefType - a defined type
              • ValueType - a value type (see below for further hierarchy)
              • RefType - a reference type (see below for further hierarchy)
              • NestedType - a type defined in another type
            • VoidType - void
            • PointerType - a pointer type

The ValueType class extends further as follows:

  • ValueType - a value type
    • SimpleType - a simple built-in type
      • BoolType - bool
      • CharType - char
      • IntegralType
        • UnsignedIntegralType
          • ByteType - byte
          • UShortType - unsigned short/System.UInt16
          • UIntType - unsigned int/System.UInt32
          • ULongType - unsigned long/System.UInt64
        • SignedIntegralType
          • SByteType - signed byte
          • ShortType - short/System.Int16
          • IntType - int/System.Int32
          • LongType - long/System.Int64
        • FloatingPointType
          • FloatType - float/System.Single
          • DoubleType - double/System.Double
        • DecimalType - decimal/System.Decimal
      • Enum - an enum
      • Struct - a struct
      • NullableType
      • ArrayType

The RefType class extends further as follows:

  • RefType
    • Class - a class
      • AnonymousClass
      • ObjectType - object/System.Object
      • StringType - string/System.String
    • Interface - an interface
    • DelegateType
    • NullType - the type of null
    • DynamicType - dynamic
  • NestedType - a type defined in another type

These class hierarchies omit generic types for simplicity.

Predicates

Useful members of ValueOrRefType include:

  • getQualifiedName()/hasQualifiedName(string) - gets the qualified name of the type (for example, "System.String")
  • getABaseInterface() - gets an immediate interface of this type, if any
  • getABaseType() - gets an immediate base class or interface of this type, if any
  • getBaseClass() - gets the immmediate base class of this type, if any
  • getASubType() - gets an immediate subtype, a type which directly inherits from this type, if any
  • getAMember() - gets any member (field/method/property etc), if any
  • getAMethod() - gets a method, if any
  • getAProperty() - gets a property, if any
  • getAnIndexer() - gets an indexer, if any
  • getAnEvent() - gets an event, if any
  • getAnOperator() - gets an operator, if any
  • getANestedType() - gets a nested type
  • getNamespace() - gets the enclosing namespace

Examples

Find all members of System.Object:

from ObjectType object
select object.getAMember()

Find all types which directly implement System.Collections.IEnumerable:

from Interface ienumerable
where ienumerable.hasQualifiedName("System.Collections.IEnumerable")
select ienumerable.getASubType()

List all simple types in the System namespace:

select any(SimpleType t | t.getNamespace().hasName("System"))

Find all variables of type PointerType:

from Variable v
where v.fromSource()
  and v.getType() instanceof PointerType
select v

List all classes in source files:

from Class c
where c.fromSource()
select c

Exercises

Exercise 3: Write a query to list the methods in string. (Answer)

Exercise 4: Adapt the example to find all types which indirectly implement IEnumerable. (Answer)

Exercise 5: Write a query to find all classes starting with the letter A. (Answer)

Callables

Callables are represented by the QL class Callable and are anything that can be called independently, such as methods, constructors, destructors, operators, anonymous functions, indexers and property accessors.

The database contains all of the callables in your program and in all referenced assemblies.

Class hierarchy

  • Element
    • Declaration
      • Callable
        • Method
          • ExtensionMethod
        • Constructor
          • StaticConstructor
          • InstanceConstructor
        • Destructor
        • Operator
          • UnaryOperator
            • PlusOperator, MinusOperator, NotOperator, ComplementOperator, IncrementOperator, DecrementOperator, FalseOperator, TrueOperator
          • BinaryOperator
            • AddOperator, SubOperator, MulOperator, DivOperator, RemOperator, AndOperator, OrOperator, XorOperator, LShiftOperator, RShiftOperator, EQOperator, NEOperator, LTOperator, GTOperator, LEOperator, GEOperator
          • ConversionOperator
            • ImplicitConversionOperator
            • ExplicitConversionOperator
        • AnonymousFunctionExpr
          • LambdaExpr
          • AnonymousMethodExpr
        • Accessor
          • Getter
          • Setter
          • EventAccessor
            • AddEventAccessor, RemoveEventAccessor

Predicates

Here are a few useful predicates on the Callable class:

  • getParameter(int)/getAParameter() - gets a parameter
  • calls(Callable) - whether there’s a direct call from one callable to another
  • getReturnType() - gets the return type
  • getBody()/getExpressionBody() - gets the body of the callable

Since Callable extends Declaration, it also has predicates from Declaration, such as

  • getName()/hasName(string)
  • getSourceDeclaration()
  • getName()
  • getDeclaringType()

Methods have additional predicates, including:

  • getAnOverridee() - gets a method that is immediately overridden by this method
  • getAnOverrider() - gets a method that immediately overrides this method
  • getAnImplementee() - gets an interface method that is immediately implemented by this method
  • getAnImplementor() - gets a method that immediately implements this interface method

Examples

List all types which override ToString:

from Method m
where m.hasName("ToString")
select m

Find methods that look like ToString methods but don’t override Object.ToString:

from Method toString, Method falseToString
where toString.hasQualifiedName("System.Object.ToString")
 and falseToString.getName().toLowerCase() = "tostring"
 and not falseToString.overrides*(toString)
 and falseToString.getNumberOfParameters() = 0
select falseToString, "This method looks like it overrides Object.ToString but it doesn't."

Find all methods which take a pointer type:

from Method m
where m.getAParameter().getType() instanceof PointerType
select m, "This method uses pointers."

Find all classes which have a destructor but aren’t disposable:

from Class c
where c.getAMember() instanceof Destructor
  and not c.getABaseType*().hasQualifiedName("System.IDisposable")
select c, "This class has a destructor but is not IDisposable."

Find Main methods which are not private:

from Method m
where m.hasName("Main")
  and not m.isPrivate()
select m, "Main method should be private."

Statements

Statements are represented by the QL class Stmt and make up the body of methods (and other callables). The database contains all statements in the source code, but does not contain any statements from referenced assemblies where the source code is not available.

Class hierarchy

  • Element
    • ControlFlowElement
      • Stmt
        • BlockStmt - { ... }
        • ExprStmt
        • SelectionStmt
          • IfStmt - if
          • SwitchStmt - switch
        • LabeledStmt
          • ConstCase
          • DefaultCase - default
          • LabelStmt
        • LoopStmt
          • WhileStmt - while(...) { ... }
          • DoStmt - do { ... } while(...)
          • ForStmt - for
          • ForEachStmt - foreach
        • JumpStmt
          • BreakStmt - break
          • ContinueStmt - continue
          • GotoStmt - goto
            • GotoLabelStmt
            • GotoCaseStmt
            • GotoDefaultStmt
          • ThrowStmt - throw
          • ReturnStmt - return
          • YieldStmt
            • YieldBreakStmt - yield break
            • YieldReturnStmt - yield return
        • TryStmt - try
        • CatchClause - catch
          • SpecificCatchClause
          • GeneralCatchClause
        • CheckedStmt - checked
        • UncheckedStmt - unchecked
        • LockStmt - lock
        • UsingStmt - using
        • LocalVariableDeclStmt
          • LocalConstantDeclStmt
        • EmptyStmt - ;
        • UnsafeStmt - unsafe
        • FixedStmt - fixed

Examples

Find long methods:

from Method m
where m.getBody().(BlockStmt).getNumberOfStmts() >= 100
select m, "This is a long method!"

Find for(;;):

from ForStmt for
where not exists(for.getAnInitializer())
  and not exists(for.getUpdate(_))
  and not exists(for.getCondition())
select for, "Infinite loop."

Find catch(NullDefererenceException):

from SpecificCatchClause catch
where catch.getCaughtExceptionType().hasQualifiedName("System.NullReferenceException")
select catch, "Catch NullReferenceException."

Find an if statement with a constant condition:

from IfStmt ifStmt
where ifStmt.getCondition().hasValue()
select ifStmt, "This 'if' statement is constant."

Find an if statement with an empty “then” clause:

from IfStmt ifStmt
where ifStmt.getThen().(BlockStmt).isEmpty()
select ifStmt, "If statement with empty 'then' block."

The (BlockStmt) is an inline cast, which restricts the query to cases where the result of getThen() has the QL class BlockStmt, and allows predicates on BlockStmt to be used, such as isEmpty().

Exercises

Exercise 6: Write a query to list all empty methods. (Answer)

Exercise 7: Modify the last example to also detect empty statements (;) in the then block. (Answer)

Exercise 8: Modify the last example to exclude chains of if statements, where the else part is another if statement. (Answer)

Expressions

The Expr class represents all C# expressions in the program. An expression is something producing a value such as a+b or new List<int>(). The database contains all expressions from the source code, but no expressions from referenced assemblies where the source code is not available.

The Access class represents any use or cross-reference of another Declaration such a variable, property, method or field. The getTarget() predicate gets the declaration being accessed.

The Call class represents a call to a Callable, for example to a Method or an Accessor, and the getTarget() method gets the Callable being called. The Operation class consists of arithmetic, bitwise operations and logical operations.

Some expressions use a “qualifier”, which is the object on which the expression operates. A typical example is a MethodCall. In this case, the getQualifier() predicate is used to get the expression on the left of the ., and getArgument(int) is used to get the arguments of the call.

Class hierarchy

  • Element
    • ControlFlowElement
      • Expr
        • LocalVariableDeclExpr
          • LocalConstantDeclExpr
        • Operation
          • UnaryOperation
            • SizeofExpr, PointerIndirectionExpr, AddressOfExpr
          • BinaryOperation
            • ComparisonOperation
              • EqualityOperation
                • EQExpr, NEExpr
                • RelationalOperation
                  • GTExpr, LTExpr, GEExpr, LEExpr
          • Assignment
            • AssignOperation
              • AddOrRemoveEventExpr
                • AddEventExpr
                • RemoveEventExpr
              • AssignArithmeticOperation
                • AssignAddExpr, AssignSubExpr, AssignMulExpr, AssignDivExpr, AssignRemExpr
              • AssignBitwiseOperation
                • AssignAndExpr, AssignOrExpr, AssignXorExpr, AssignLShiftExpr, AssignRShiftExpr
            • AssignExpr
              • MemberInitializer
          • ArithmeticOperation
            • UnaryArithmeticOperation
              • UnaryMinusExpr, UnaryPlusExpr
              • MutatorOperation
                • IncrementOperation
                  • PreIncrExpr, PostIncrExpr
                • DecrementOperation
                  • PreDecrExpr, PostDecrExpr
            • BinaryArithmeticOperation
              • AddExpr, SubExpr, MulExpr, DivExpr, RemExpr
          • BitwiseOperation
            • UnaryBitwiseOperation
              • ComplementOperation
            • BinaryBitwiseOperation
              • LShiftExpr, RShiftExpr, BitwiseAndExpr, BitwiseOrExpr, BitwiseXorExpr
          • LogicalOperation
            • UnaryLogicalOperation
              • LogicalNotOperation
            • BinaryLogicalOperation
              • LogicalAndExpr, LogicalOrExpr, NullCoalescingExpr
            • ConditionalExpr
        • ParenthesisedExpr, CheckedExpr, UncheckedExpr, IsExpr, AsExpr, CastExpr, TypeofExpr, DefaultValueExpr, AwaitExpr, NameofExpr, InterpolatedStringExpr
        • Access
          • ThisAccess
          • BaseAccess
          • MemberAccess
            • MethodAccess
              • VirtualMethodAccess
            • FieldAccess, PropertyAccess, IndexerAccess, EventAccess, MethodAccess
          • AssignableAccess
            • VariableAccess
              • ParameterAccess
              • LocalVariableAccess
              • LocalScopeVariableAccess
              • FieldAccess
                • MemberConstantAccess
            • PropertyAccess
              • TrivialPropertyAccess
              • VirtualPropertyAccess
            • IndexerAccess
              • VirtualIndexerAccess
            • EventAccess
              • VirtualEventAccess
          • TypeAccess
          • ArrayAccess
        • Call
          • PropertyCall
          • IndexerCall
          • EventCall
          • MethodCall
            • VirtualMethodCall
            • ElementInitializer
          • ConstructorInitializer
          • OperatorCall
            • MutatorOperatorCall
          • DelegateCall
          • ObjectCreation
            • DefaultValueTypeObjectCreation
            • TypeParameterObjectCreation
            • AnonymousObjectCreation
        • ObjectOrCollectionInitializer
          • ObjectInitializer
          • CollectionInitializer
        • DelegateCreation
          • ExplicitDelegateCreation, ImplicitDelegateCreation
        • ArrayInitializer
        • ArrayCreation
        • AnonymousFunctionExpr
          • LambdaExpr
          • AnonymousMethodExpr
        • Literal
          • BoolLiteral, CharLiteral, IntegerLiteral, IntLiteral, LongLiteral, UIntLiteral, ULongLiteral, RealLiteral, FloatLiteral, DoubleLiteral, DecimalLiteral, StringLiteral, NullLiteral

Predicates

Useful predicates on Expr include:

  • getType() - gets the Type of the expression
  • getValue() - gets the compile-time constant, if any
  • hasValue() - whether the expression has a compile-time constant
  • getEnclosingStmt() - gets the statement containing the expression, if any
  • getEnclosingCallable() - gets the callable containing the expression, if any
  • stripCasts() - remove all explicit or implicit casts
  • isImplicit() - whether the expression was implicit, such as an implicit this qualifier (ThisAccess)

Examples

Find calls to String.Format with just one argument:

from MethodCall c
where c.getTarget().hasQualifiedName("System.String.Format")
  and c.getNumberOfArguments() = 1
select c, "Missing arguments to 'String.Format'."

Find all comparisons of floating point values:

from ComparisonOperation cmp
where (cmp instanceof EQExpr or cmp instanceof NEExpr)
  and cmp.getAnOperand().getType() instanceof FloatingPointType
select cmp, "Comparison of floating point values."

Find hard-coded passwords:

from Variable v, string value
where v.getName().regexpMatch("[pP]ass(word|wd|)")
  and value = v.getAnAssignedValue().getValue()
select v, "Hard-coded password '" + value + "'."

Exercises

Exercise 9: Limit the previous query to string types. Exclude empty passwords or null passwords. (Answer)

Attributes

C# attributes are represented by the QL class Attribute. They can be present on many C# elements, such as classes, methods, fields and parameters. The database contains attributes from the source code and all assembly references.

The attribute of any Element can be obtained via getAnAttribute(), whereas if you have an attribute, you can find its element via getTarget(). The following two query fragments are identical:

attribute = element.getAnAttribute()
element = attribute.getTarget()

Class hierarchy

  • Element
    • Attribute

Predicates

  • getTarget() - gets the Element to which this attribute applies
  • getArgument(int) - gets the given argument of the attribute
  • getType() - gets the type of this attribute. Note that the class name must end in “Attribute”.

Examples

Find all obsolete elements:

from Element e, Attribute attribute
where e = attribute.getTarget()
  and attribute.getType().hasName("ObsoleteAttribute")
select e, "This is obsolete because " + attribute.getArgument(0).getValue()

Model NUnit test fixtures:

class TestFixture extends Class
{
  TestFixture() {
    this.getAnAttribute().getType().hasName("TestFixtureAttribute")
  }

  TestMethod getATest() {
    result = this.getAMethod()
  }
}

class TestMethod extends Method
{
  TestMethod() {
    this.getAnAttribute().getType().hasName("TestAttribute")
  }
}

from TestFixture f
select f, f.getATest()

Exercises

Exercise 10: Write a query to find just obsolete methods. (Answer)

Exercise 11: Write a query to find all places where the Obsolete attribute is used without a reason string (that is, [Obsolete]). (Answer)

Exercise 12: In the first example, what happens if the Obsolete attribute doesn’t have a reason string? How could the query be fixed to accommodate this? (Answer)


Answers

Exercise 1

from AddExpr op
select op

or

select any(AddExpr op)

Exercise 2

from File f
where f.getNumberOfLines() = max(any(File g).getNumberOfLines())
select f

Exercise 3

from StringType s
select s.getAMethod()

Exercise 4

from Interface ienumerable
where ienumerable.hasQualifiedName("System.Collections.IEnumerable")
select ienumerable.getASubType*()

Exercise 5

from Class a
where a.getName().toLowerCase().matches("a%")
select a

Exercise 6

select any(Method m | m.getBody().(BlockStmt).isEmpty())

Exercise 7

from IfStmt ifStmt
where ifStmt.getThen().(BlockStmt).isEmpty() or ifStmt.getThen() instanceof EmptyStmt
select ifStmt, "If statement with empty 'then' block."

Exercise 8

from IfStmt ifStmt
where (ifStmt.getThen().(BlockStmt).isEmpty() or ifStmt.getThen() instanceof EmptyStmt)
  and not ifStmt.getElse() instanceof IfStmt
select ifStmt, "If statement with empty 'then' block."

Exercise 9

from Variable v, StringLiteral value
where v.getName().regexpMatch("[pP]ass(word|wd|)")
  and value = v.getAnAssignedValue()
  and value.getValue() != ""
select v, "Hard-coded password '" + value.getValue() + "'."

Exercise 10

from Method method, Attribute attribute
where method = attribute.getTarget()
  and attribute.getType().hasName("ObsoleteAttribute")
select method, "This is obsolete because " + attribute.getArgument(0).getValue()

Exercise 11

from Attribute attribute
where attribute.getType().hasName("ObsoleteAttribute")
  and not exists(attribute.getArgument(0))
select attribute, "Missing reason in 'Obsolete' attribute."

Exercise 12

The query does not return results where the argument is missing.

Here is the fixed version:

from Element e, Attribute attribute, string reason
where e = attribute.getTarget()
  and attribute.getType().hasName("ObsoleteAttribute")
  and if exists(attribute.getArgument(0))
    then reason = attribute.getArgument(0).getValue()
    else reason = "(not given)"
select e, "This is obsolete because " + reason

What next?