Skip to content
Merged
Changes from 1 commit
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
5064cd5
JS: Exclude externs from CallGraph meta-query
asgerf Apr 11, 2025
9fc0b8c
JS: Add ImportSpecifier.getImportDeclaration()
asgerf Mar 11, 2025
50e4ac8
JS: Do not ignore variables from ambient declarations
asgerf Apr 11, 2025
b5a4fc0
JS: Make Closure concepts based on AST instead
asgerf Apr 11, 2025
4cd6f45
JS: Avoid accidental recursion with API graphs
asgerf Apr 11, 2025
9566265
JS: Add helper for getting local type names
asgerf Apr 11, 2025
4bfb048
JS: Resolve JSDocLocalTypeAccess to a variable in scope
asgerf Apr 11, 2025
1051136
JS: Add test
asgerf Apr 11, 2025
1533e13
JS: Add NameResolution.qll
asgerf Apr 11, 2025
d61f576
JS: Add UnderlyingTypes.qll
asgerf Apr 11, 2025
fc580a5
JS: Add TypeResolution.qll
asgerf Apr 11, 2025
b923eac
JS: Use underlying types in DataFlow::Node
asgerf Apr 11, 2025
cca48c0
JS: Use in TypeAnnotation.getClass and hasUnderlyingType predicates
asgerf Apr 11, 2025
9fd85c9
JS: Update jQuery model
asgerf Apr 11, 2025
2d21074
JS: Use sanitizing primitive types in ViewComponentInput
asgerf Apr 11, 2025
6fdd7fe
JS: Use sanitizing primitive type in Nest model
asgerf Apr 11, 2025
4e44fda
JS: Use hasUnderlyingStringOrAnyType in Nest model
asgerf Apr 11, 2025
6ac35f1
JS: Use in MissingAwait
asgerf Apr 11, 2025
989402d
JS: Remove some dependencies on type extraction
asgerf Apr 11, 2025
57811ed
JS: Some test updates
asgerf Apr 11, 2025
307715a
JS: Use type resolution for CG augmentation
asgerf Apr 22, 2025
f06b9a9
JS: Add call graph test with types
asgerf Apr 30, 2025
500291d
JS: Hide shadowed inherited members
asgerf Apr 30, 2025
167f752
JS: Also propagate through promise types
asgerf Apr 30, 2025
6e82b6e
JS: Add failing test for assigning a non-SourceNode to a type annotat…
asgerf Apr 30, 2025
e07a036
JS: Mark type-annotated nodes as SourceNode
asgerf Apr 14, 2025
fbafd6f
JS: Update to avoid deprecations after import resolution change
asgerf May 2, 2025
b8dc1b3
JS: Remove redundant casts
asgerf May 2, 2025
bba872a
JS: Make jump-to-def behave nicer
asgerf May 12, 2025
de7d851
JS: Update output of old HasUnderlyingType test
asgerf May 12, 2025
22a4114
JS: Accept regression in overload resolution
asgerf May 12, 2025
b610e10
JS: Accept change in handling of variable resolution in face of ambie…
asgerf May 12, 2025
27979c6
JS: Add regression tests for declared globals
asgerf May 13, 2025
9bcc620
JS: Fix regression from global declare vars
asgerf May 13, 2025
11607e5
JS: Update TRAP after extractor change
asgerf May 19, 2025
b698b4e
JS: Add test for missing type flow through generics
asgerf May 20, 2025
d644f80
JS: Remove obsolete meta query
asgerf May 20, 2025
853ba49
Update javascript/ql/lib/semmle/javascript/internal/TypeResolution.qll
asgerf Jun 4, 2025
79101fd
JS: Add test with type casts
asgerf Jun 4, 2025
57fad7e
JS: Add SatisfiesExpr
asgerf Jun 4, 2025
691fdb1
JS: Nicer jump-to-def for function declarations
asgerf Jun 4, 2025
42f762a
JS: Update test output now that 'satisfies' is a SourceNode
asgerf Jun 9, 2025
a6488cb
Update javascript/ql/lib/semmle/javascript/internal/NameResolution.qll
asgerf Jun 10, 2025
18f9133
JS: Rename and clarify comment for trackFunctionType
asgerf Jun 10, 2025
72cc439
JS: Normalize a few more extensions
asgerf Jun 10, 2025
2aa5fa1
JS: Add comment and examples in FlowImpl doc
asgerf Jun 11, 2025
e848aa7
JS: Clarifying comment on commonStep
asgerf Jun 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
384 changes: 384 additions & 0 deletions javascript/ql/lib/semmle/javascript/internal/TypeResolution.qll
Original file line number Diff line number Diff line change
@@ -0,0 +1,384 @@
private import javascript
private import semmle.javascript.internal.NameResolution::NameResolution
private import semmle.javascript.internal.UnderlyingTypes
private import semmle.javascript.dataflow.internal.sharedlib.SummaryTypeTracker as SummaryTypeTracker

module TypeResolution {
predicate trackClassValue = ValueFlow::TrackNode<ClassDefinition>::track/1;

predicate trackType = TypeFlow::TrackNode<TypeDefinition>::track/1;

Node trackFunctionType(Function fun) {
result = fun
or
exists(Node mid | mid = trackFunctionType(fun) |
TypeFlow::step(mid, result)
or
UnderlyingTypes::underlyingTypeStep(mid, result)
)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems you're using a specialized predicate (instead of TypeFlow::TrackNode) because you additionally need the steps from UnderlyingTypes::underlyingTypeStep.

Why can't those steps be part of TypeFlow::TrackNode? Or why can't functions use TypeFlow::TrackNode?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. I've renamed the predicate and added a clarifying qldoc comment in 18f9133


predicate trackFunctionValue = ValueFlow::TrackNode<Function>::track/1;

/**
* Gets the representative for the type containing the given member.
*
* For non-static members this is simply the enclosing type declaration.
*
* For static members we use the class's `Variable` as representative for the type of the class object.
*/
private Node getMemberBase(MemberDeclaration member) {
if member.isStatic()
then result = member.getDeclaringClass().getVariable()
else result = member.getDeclaringType()
}

/**
* Holds if `host` is a type with a `content` of type `memberType`.
*/
private predicate typeMember(Node host, DataFlow::Content content, Node memberType) {
exists(MemberDeclaration decl | host = getMemberBase(decl) |
exists(FieldDeclaration field |
decl = field and
content.asPropertyName() = field.getName() and
memberType = field.getTypeAnnotation()
)
or
exists(MethodDeclaration method |
decl = method and
content.asPropertyName() = method.getName() and
memberType = method.getBody() // use the Function as representative for the function type
)
or
decl instanceof IndexSignature and
memberType = decl.(IndexSignature).getBody().getReturnTypeAnnotation() and
content.isUnknownArrayElement()
)
or
// Ad-hoc support for array types. We don't support generics in general currently, we just special-case arrays.
content.isUnknownArrayElement() and
(
memberType = host.(ArrayTypeExpr).getElementType()
or
exists(GenericTypeExpr type |
host = type and
type.getTypeAccess().(LocalTypeAccess).getName() = ["Array", "ReadonlyArray"] and
memberType = type.getTypeArgument(0)
)
or
exists(JSDocAppliedTypeExpr type |
host = type and
type.getHead().(JSDocLocalTypeAccess).getName() = "Array" and
memberType = type.getArgument(0)
)
)
or
// Inherit members from base types
exists(ClassOrInterface baseType | typeMember(baseType, content, memberType) |
host.(ClassDefinition).getSuperClass() = trackClassValue(baseType)
or
host.(ClassOrInterface).getASuperInterface() = trackType(baseType)
)
}

/**
* Holds `use` refers to `host`, and `host` has type members.
*
* Currently steps through unions and intersections, which acts as a basic
* approximation to the unions/intersection of objects.
*/
private predicate typeMemberHostReaches(Node host, Node use) {
typeMember(host, _, _) and
use = host
or
exists(Node mid | typeMemberHostReaches(host, mid) |
TypeFlow::step(mid, use)
or
UnderlyingTypes::underlyingTypeStep(mid, use)
)
}

/**
* Holds if there is a read from from `object` to `member` that reads `contents`.
*/
private predicate valueReadStep(Node object, DataFlow::ContentSet contents, Node member) {
member.(PropAccess).accesses(object, contents.asPropertyName())
or
object.(ObjectPattern).getPropertyPatternByName(contents.asPropertyName()).getValuePattern() =
member
or
SummaryTypeTracker::basicLoadStep(object.(AST::ValueNode).flow(),
member.(AST::ValueNode).flow(), contents)
}

private predicate callTarget(InvokeExpr call, Function target) {
exists(ClassDefinition cls |
valueHasType(call.(NewExpr).getCallee(), trackClassValue(cls)) and
target = cls.getConstructor().getBody()
)
or
valueHasType(call.(InvokeExpr).getCallee(), trackFunctionValue(target))
Comment thread Fixed
or
valueHasType(call.(InvokeExpr).getCallee(), trackFunctionType(target)) and
Comment thread Fixed
(
call instanceof NewExpr and
target = any(ConstructorTypeExpr t).getFunction()
or
call instanceof CallExpr and
target = any(PlainFunctionTypeExpr t).getFunction()
)
or
exists(InterfaceDefinition interface, CallSignature sig |
valueHasType(call.(InvokeExpr).getCallee(), trackType(interface)) and
Comment thread Fixed
sig = interface.getACallSignature() and
target = sig.getBody()
|
call instanceof NewExpr and
sig instanceof ConstructorCallSignature
or
call instanceof CallExpr and
sig instanceof FunctionCallSignature
)
}

private predicate functionReturnType(Function func, Node returnType) {
returnType = func.getReturnTypeAnnotation()
or
not exists(func.getReturnTypeAnnotation()) and
exists(Function functionType |
contextualType(func, trackFunctionType(functionType)) and
returnType = functionType.getReturnTypeAnnotation()
)
}

bindingset[name]
private predicate isPromiseTypeName(string name) {
name.regexpMatch(".?(Promise|Thenable)(Like)?")
}

private Node unwrapPromiseType(Node promiseType) {
exists(GenericTypeExpr type |
promiseType = type and
isPromiseTypeName(type.getTypeAccess().(LocalTypeAccess).getName()) and
result = type.getTypeArgument(0)
)
or
exists(JSDocAppliedTypeExpr type |
promiseType = type and
isPromiseTypeName(type.getHead().(JSDocLocalTypeAccess).getName()) and
result = type.getArgument(0)
)
}

private predicate contextualType(Node value, Node type) {
exists(InvokeExpr call, Function target, int i |
callTarget(call, target) and
value = call.getArgument(i) and
type = target.getParameter(i).getTypeAnnotation()
)
or
exists(Function lambda, Node returnType |
value = lambda.getAReturnedExpr() and
functionReturnType(lambda, returnType)
|
not lambda.isAsyncOrGenerator() and
type = returnType
or
lambda.isAsync() and
type = unwrapPromiseType(returnType)
)
or
exists(ObjectExpr object, Node objectType, Node host, string name |
contextualType(object, objectType) and
typeMemberHostReaches(host, objectType) and
typeMember(host, any(DataFlow::Content c | c.asPropertyName() = name), type) and
value = object.getPropertyByName(name).getInit()
)
or
exists(ArrayExpr array, Node arrayType, Node host |
contextualType(array, arrayType) and
typeMemberHostReaches(host, arrayType) and
typeMember(host, any(DataFlow::Content c | c.isUnknownArrayElement()), type) and
value = array.getAnElement()
)
}

/**
* Holds if `value` has the given `type`.
*/
predicate valueHasType(Node value, Node type) {
value.(BindingPattern).getTypeAnnotation() = type
or
exists(VarDecl decl |
// ValueFlow::step is restricted to variables with at most one assignment. Allow the type annotation
// of a variable to propagate to its uses, even if the variable has multiple assignments.
type = decl.getTypeAnnotation() and
value = decl.getVariable().(LocalVariable).getAnAccess()
)
or
exists(MemberDeclaration member |
value.(ThisExpr).getBindingContainer() = member.getInit() and
type = getMemberBase(member)
)
or
exists(ClassDefinition cls |
value = cls and
type = cls.getVariable()
)
or
exists(FunctionDeclStmt fun |
value = fun and
type = fun.getVariable()
)
or
exists(Function target | callTarget(value, target) |
type = target.getReturnTypeAnnotation()
or
exists(ClassDefinition cls |
target = cls.getConstructor().getBody() and
type = cls
)
)
or
// Contextual typing for parameters
exists(Function lambda, Function functionType, int i |
contextualType(lambda, trackFunctionType(functionType))
or
exists(InterfaceDefinition interface |
contextualType(lambda, trackType(interface)) and
functionType = interface.getACallSignature().getBody()
)
Comment thread Fixed
|
value = lambda.getParameter(i) and
not exists(value.(Parameter).getTypeAnnotation()) and
type = functionType.getParameter(i).getTypeAnnotation()
)
or
exists(Node mid | valueHasType(mid, type) | ValueFlow::step(mid, value))
or
exists(Node mid, Node midType, DataFlow::ContentSet contents, Node host |
valueReadStep(mid, contents, value) and
valueHasType(mid, midType) and
typeMemberHostReaches(host, midType) and
typeMember(host, contents.getAReadContent(), type)
)
}

signature predicate nodeSig(Node node);

/**
* Tracks types that have a certain property, in the sense that:
* - an intersection type has the property if any member has the property
* - a union type has the property if all its members have the property
*/
module TrackMustProp<nodeSig/1 directlyHasProperty> {
predicate hasProperty(Node node) {
directlyHasProperty(node)
or
exists(Node mid |
hasProperty(mid) and
TypeFlow::step(mid, node)
)
or
unionHasProp(node)
or
hasProperty(node.(IntersectionTypeExpr).getAnElementType())
or
exists(ConditionalTypeExpr cond |
node = cond and
hasProperty(cond.getTrueType()) and
hasProperty(cond.getFalseType())
)
}

private predicate unionHasProp(UnionTypeExpr node, int n) {
hasProperty(node.getElementType(0)) and n = 1
or
unionHasProp(node, n - 1) and
hasProperty(node.getElementType(n - 1))
}

private predicate unionHasProp(UnionTypeExpr node) {
unionHasProp(node, node.getNumElementType())
}
}

module ValueHasProperty<nodeSig/1 typeHasProperty> {
predicate valueHasProperty(Node value) {
exists(Node type |
valueHasType(value, type) and
typeHasProperty(type)
)
}
}

private predicate isSanitizingPrimitiveTypeBase(Node node) {
node.(TypeExpr).isNumbery()
or
node.(TypeExpr).isBooleany()
or
node.(TypeExpr).isNull()
or
node.(TypeExpr).isUndefined()
or
node.(TypeExpr).isVoid()
or
node.(TypeExpr).isNever()
or
Comment thread
asgerf marked this conversation as resolved.
node instanceof LiteralTypeExpr
or
node = any(EnumMember m).getIdentifier() // enum members are constant
or
node instanceof EnumDeclaration // enums are unions of constants
}

/**
* Holds if `node` refers to a type that is considered untaintable (if actually enforced at runtime).
*
* Specifically, the types `number`, `boolean`, `null`, `undefined`, `void`, `never`, as well as literal types (`"foo"`)
* and enums and enum members have this property.
*/
predicate isSanitizingPrimitiveType =
TrackMustProp<isSanitizingPrimitiveTypeBase/1>::hasProperty/1;

/**
* Holds if `value` has a type that is considered untaintable (if actually enforced at runtime).
*
* See `isSanitizingPrimitiveType`.
*/
predicate valueHasSanitizingPrimitiveType =
ValueHasProperty<isSanitizingPrimitiveType/1>::valueHasProperty/1;

private predicate isPromiseBase(Node node) { exists(unwrapPromiseType(node)) }

/**
* Holds if the given type is a Promise object. Does not hold for unions unless all parts of the union are promises.
*/
predicate isPromiseType = TrackMustProp<isPromiseBase/1>::hasProperty/1;

/**
* Holds if the given value has a type that implied it is a Promise object. Does not hold for unions unless all parts of the union are promises.
*/
predicate valueHasPromiseType = ValueHasProperty<isPromiseType/1>::valueHasProperty/1;

/**
* Holds if `type` contains `string` or `any`, possibly wrapped in a promise.
*/
predicate hasUnderlyingStringOrAnyType(Node type) {
type.(TypeAnnotation).isStringy()
or
type.(TypeAnnotation).isAny()
or
type instanceof StringLiteralTypeExpr
or
type instanceof TemplateLiteralTypeExpr
or
exists(Node mid | hasUnderlyingStringOrAnyType(mid) |
TypeFlow::step(mid, type)
or
UnderlyingTypes::underlyingTypeStep(mid, type)
or
type = unwrapPromiseType(mid)
)
}
}