-
Notifications
You must be signed in to change notification settings - Fork 2k
JS: QL-side type/name resolution for TypeScript and JSDoc #19078
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
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 9fc0b8c
JS: Add ImportSpecifier.getImportDeclaration()
asgerf 50e4ac8
JS: Do not ignore variables from ambient declarations
asgerf b5a4fc0
JS: Make Closure concepts based on AST instead
asgerf 4cd6f45
JS: Avoid accidental recursion with API graphs
asgerf 9566265
JS: Add helper for getting local type names
asgerf 4bfb048
JS: Resolve JSDocLocalTypeAccess to a variable in scope
asgerf 1051136
JS: Add test
asgerf 1533e13
JS: Add NameResolution.qll
asgerf d61f576
JS: Add UnderlyingTypes.qll
asgerf fc580a5
JS: Add TypeResolution.qll
asgerf b923eac
JS: Use underlying types in DataFlow::Node
asgerf cca48c0
JS: Use in TypeAnnotation.getClass and hasUnderlyingType predicates
asgerf 9fd85c9
JS: Update jQuery model
asgerf 2d21074
JS: Use sanitizing primitive types in ViewComponentInput
asgerf 6fdd7fe
JS: Use sanitizing primitive type in Nest model
asgerf 4e44fda
JS: Use hasUnderlyingStringOrAnyType in Nest model
asgerf 6ac35f1
JS: Use in MissingAwait
asgerf 989402d
JS: Remove some dependencies on type extraction
asgerf 57811ed
JS: Some test updates
asgerf 307715a
JS: Use type resolution for CG augmentation
asgerf f06b9a9
JS: Add call graph test with types
asgerf 500291d
JS: Hide shadowed inherited members
asgerf 167f752
JS: Also propagate through promise types
asgerf 6e82b6e
JS: Add failing test for assigning a non-SourceNode to a type annotat…
asgerf e07a036
JS: Mark type-annotated nodes as SourceNode
asgerf fbafd6f
JS: Update to avoid deprecations after import resolution change
asgerf b8dc1b3
JS: Remove redundant casts
asgerf bba872a
JS: Make jump-to-def behave nicer
asgerf de7d851
JS: Update output of old HasUnderlyingType test
asgerf 22a4114
JS: Accept regression in overload resolution
asgerf b610e10
JS: Accept change in handling of variable resolution in face of ambie…
asgerf 27979c6
JS: Add regression tests for declared globals
asgerf 9bcc620
JS: Fix regression from global declare vars
asgerf 11607e5
JS: Update TRAP after extractor change
asgerf b698b4e
JS: Add test for missing type flow through generics
asgerf d644f80
JS: Remove obsolete meta query
asgerf 853ba49
Update javascript/ql/lib/semmle/javascript/internal/TypeResolution.qll
asgerf 79101fd
JS: Add test with type casts
asgerf 57fad7e
JS: Add SatisfiesExpr
asgerf 691fdb1
JS: Nicer jump-to-def for function declarations
asgerf 42f762a
JS: Update test output now that 'satisfies' is a SourceNode
asgerf a6488cb
Update javascript/ql/lib/semmle/javascript/internal/NameResolution.qll
asgerf 18f9133
JS: Rename and clarify comment for trackFunctionType
asgerf 72cc439
JS: Normalize a few more extensions
asgerf 2aa5fa1
JS: Add comment and examples in FlowImpl doc
asgerf e848aa7
JS: Clarifying comment on commonStep
asgerf File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
384 changes: 384 additions & 0 deletions
384
javascript/ql/lib/semmle/javascript/internal/TypeResolution.qll
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) | ||
| ) | ||
| } | ||
|
|
||
| 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)) | ||
|
|
||
| or | ||
| valueHasType(call.(InvokeExpr).getCallee(), trackFunctionType(target)) and | ||
|
|
||
| ( | ||
| 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 | ||
|
|
||
| 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() | ||
| ) | ||
|
|
||
| | | ||
| 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 | ||
|
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) | ||
| ) | ||
| } | ||
| } | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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 fromUnderlyingTypes::underlyingTypeStep.Why can't those steps be part of
TypeFlow::TrackNode? Or why can't functions useTypeFlow::TrackNode?There was a problem hiding this comment.
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