JS: Use in TypeAnnotation.getClass and hasUnderlyingType predicates

This commit is contained in:
Asger F
2025-04-11 13:08:23 +02:00
parent b923eac9be
commit cca48c09b9
12 changed files with 53 additions and 137 deletions

View File

@@ -415,7 +415,7 @@ class JSDocNamedTypeExpr extends JSDocTypeExpr {
* - `foo.bar.Baz` has prefix `foo` and suffix `.bar.Baz`.
* - `Baz` has prefix `Baz` and an empty suffix.
*/
predicate hasNameParts(string prefix, string suffix) {
deprecated predicate hasNameParts(string prefix, string suffix) {
not this = any(JSDocQualifiedTypeAccess a).getBase() and // restrict size of predicate
exists(string regex, string name | regex = "([^.]+)(.*)" |
name = this.getRawName() and
@@ -423,46 +423,6 @@ class JSDocNamedTypeExpr extends JSDocTypeExpr {
suffix = name.regexpCapture(regex, 2)
)
}
pragma[noinline]
pragma[nomagic]
private predicate hasNamePartsAndEnv(string prefix, string suffix, JSDoc::Environment env) {
// Force join ordering
this.hasNameParts(prefix, suffix) and
env.isContainerInScope(this.getContainer())
}
/**
* Gets the qualified name of this name by resolving its prefix, if any.
*/
cached
private string resolvedName() {
exists(string prefix, string suffix, JSDoc::Environment env |
this.hasNamePartsAndEnv(prefix, suffix, env) and
result = env.resolveAlias(prefix) + suffix
)
}
override predicate hasQualifiedName(string globalName) {
globalName = this.resolvedName()
or
not exists(this.resolvedName()) and
globalName = this.getRawName()
}
override DataFlow::ClassNode getClass() {
exists(string name |
this.hasQualifiedName(name) and
result.hasQualifiedName(name)
)
or
// Handle case where a local variable has a reference to the class,
// but the class doesn't have a globally qualified name.
exists(string alias, JSDoc::Environment env |
this.hasNamePartsAndEnv(alias, "", env) and
result.getAClassReference().flowsTo(env.getNodeFromAlias(alias))
)
}
}
/**
@@ -491,12 +451,6 @@ class JSDocAppliedTypeExpr extends @jsdoc_applied_type_expr, JSDocTypeExpr {
* For example, in `Array<string>`, `string` is the only argument type.
*/
JSDocTypeExpr getAnArgument() { result = this.getArgument(_) }
override predicate hasQualifiedName(string globalName) {
this.getHead().hasQualifiedName(globalName)
}
override DataFlow::ClassNode getClass() { result = this.getHead().getClass() }
}
/**
@@ -516,8 +470,6 @@ class JSDocNullableTypeExpr extends @jsdoc_nullable_type_expr, JSDocTypeExpr {
predicate isPrefix() { jsdoc_prefix_qualifier(this) }
override JSDocTypeExpr getAnUnderlyingType() { result = this.getTypeExpr().getAnUnderlyingType() }
override DataFlow::ClassNode getClass() { result = this.getTypeExpr().getClass() }
}
/**
@@ -537,8 +489,6 @@ class JSDocNonNullableTypeExpr extends @jsdoc_non_nullable_type_expr, JSDocTypeE
predicate isPrefix() { jsdoc_prefix_qualifier(this) }
override JSDocTypeExpr getAnUnderlyingType() { result = this.getTypeExpr().getAnUnderlyingType() }
override DataFlow::ClassNode getClass() { result = this.getTypeExpr().getClass() }
}
/**
@@ -643,8 +593,6 @@ class JSDocOptionalParameterTypeExpr extends @jsdoc_optional_type_expr, JSDocTyp
override JSDocTypeExpr getAnUnderlyingType() {
result = this.getUnderlyingType().getAnUnderlyingType()
}
override DataFlow::ClassNode getClass() { result = this.getUnderlyingType().getClass() }
}
/**
@@ -679,7 +627,7 @@ module JSDoc {
/**
* A statement container which may declare JSDoc name aliases.
*/
class Environment extends StmtContainer {
deprecated class Environment extends StmtContainer {
/**
* Gets the fully qualified name aliased by the given unqualified name
* within this container.
@@ -729,7 +677,7 @@ module JSDoc {
}
pragma[noinline]
private predicate isTypenamePrefix(string name) {
deprecated private predicate isTypenamePrefix(string name) {
any(JSDocNamedTypeExpr expr).hasNameParts(name, _)
}
}

View File

@@ -4,6 +4,8 @@
import javascript
private import internal.StmtContainers
private import internal.NameResolution
private import internal.UnderlyingTypes
/**
* A type annotation, either in the form of a TypeScript type or a JSDoc comment.
@@ -75,14 +77,38 @@ class TypeAnnotation extends @type_annotation, NodeInStmtContainer {
TypeAnnotation getAnUnderlyingType() { result = this }
/**
* DEPRECATED. Use `hasUnderlyingType` instead.
*
* Holds if this is a reference to the type with qualified name `globalName` relative to the global scope.
*/
predicate hasQualifiedName(string globalName) { none() }
deprecated predicate hasQualifiedName(string globalName) {
UnderlyingTypes::nodeHasUnderlyingType(this, globalName)
}
/**
* DEPRECATED. Use `hasUnderlyingType` instead.
*
* Holds if this is a reference to the type exported from `moduleName` under the name `exportedName`.
*/
predicate hasQualifiedName(string moduleName, string exportedName) { none() }
deprecated predicate hasQualifiedName(string moduleName, string exportedName) {
UnderlyingTypes::nodeHasUnderlyingType(this, moduleName, exportedName)
}
/**
* Holds if this is a reference to the type with qualified name `globalName` relative to the global scope,
* or is declared as a subtype thereof, or is a union or intersection containing such a type.
*/
final predicate hasUnderlyingType(string globalName) {
UnderlyingTypes::nodeHasUnderlyingType(this, globalName)
}
/**
* Holds if this is a reference to the type exported from `moduleName` under the name `exportedName`,
* or is declared as a subtype thereof, or is a union or intersection containing such a type.
*/
final predicate hasUnderlyingType(string moduleName, string exportedName) {
UnderlyingTypes::nodeHasUnderlyingType(this, moduleName, exportedName)
}
/** Gets the statement in which this type appears. */
Stmt getEnclosingStmt() { none() }
@@ -107,5 +133,5 @@ class TypeAnnotation extends @type_annotation, NodeInStmtContainer {
*
* This unfolds nullability modifiers and generic type applications.
*/
DataFlow::ClassNode getClass() { none() }
final DataFlow::ClassNode getClass() { UnderlyingTypes::nodeHasUnderlyingClassType(this, result) }
}

View File

@@ -1,4 +1,5 @@
import javascript
private import semmle.javascript.internal.UnderlyingTypes
/**
* A statement that defines a namespace, that is, a namespace declaration or enum declaration.
@@ -575,10 +576,6 @@ class TypeExpr extends ExprOrType, @typeexpr, TypeAnnotation {
override Function getEnclosingFunction() { result = ExprOrType.super.getEnclosingFunction() }
override TopLevel getTopLevel() { result = ExprOrType.super.getTopLevel() }
override DataFlow::ClassNode getClass() {
result.getAstNode() = this.getType().(ClassType).getClass()
}
}
/**
@@ -698,58 +695,9 @@ class TypeAccess extends @typeaccess, TypeExpr, TypeRef {
*/
TypeName getTypeName() { ast_node_symbol(this, result) }
override predicate hasQualifiedName(string globalName) {
this.getTypeName().hasQualifiedName(globalName)
or
exists(LocalTypeAccess local | local = this |
not exists(local.getLocalTypeName()) and // Without a local type name, the type is looked up in the global scope.
globalName = local.getName()
)
}
override predicate hasQualifiedName(string moduleName, string exportedName) {
this.getTypeName().hasQualifiedName(moduleName, exportedName)
or
exists(ImportDeclaration imprt, ImportSpecifier spec |
moduleName = getImportName(imprt) and
spec = imprt.getASpecifier()
|
spec.getImportedName() = exportedName and
this = spec.getLocal().(TypeDecl).getLocalTypeName().getAnAccess()
or
(spec instanceof ImportNamespaceSpecifier or spec instanceof ImportDefaultSpecifier) and
this =
spec.getLocal().(LocalNamespaceDecl).getLocalNamespaceName().getAMemberAccess(exportedName)
)
or
exists(ImportEqualsDeclaration imprt |
moduleName = getImportName(imprt.getImportedEntity()) and
this =
imprt
.getIdentifier()
.(LocalNamespaceDecl)
.getLocalNamespaceName()
.getAMemberAccess(exportedName)
)
}
override string getAPrimaryQlClass() { result = "TypeAccess" }
}
/**
* Gets a suitable name for the library imported by `imprt`.
*
* For relative imports, this is the snapshot-relative path to the imported module.
* For non-relative imports, it is the import path itself.
*/
private string getImportName(Import imprt) {
exists(string path | path = imprt.getImportedPathString() |
if path.regexpMatch("[./].*")
then result = imprt.getImportedModule().getFile().getRelativePath()
else result = path
)
}
/** An identifier that is used as part of a type, such as `Date`. */
class LocalTypeAccess extends @local_type_access, TypeAccess, Identifier, LexicalAccess {
override predicate isStringy() { this.getName() = "String" }
@@ -822,14 +770,6 @@ class GenericTypeExpr extends @generic_typeexpr, TypeExpr {
/** Gets the number of type arguments. This is always at least one. */
int getNumTypeArgument() { result = count(this.getATypeArgument()) }
override predicate hasQualifiedName(string globalName) {
this.getTypeAccess().hasQualifiedName(globalName)
}
override predicate hasQualifiedName(string moduleName, string exportedName) {
this.getTypeAccess().hasQualifiedName(moduleName, exportedName)
}
override string getAPrimaryQlClass() { result = "GenericTypeExpr" }
}

View File

@@ -237,7 +237,7 @@ module NestJS {
CustomPipeClass() {
exists(ClassDefinition cls |
this = cls.flow() and
cls.getASuperInterface().hasQualifiedName("@nestjs/common", "PipeTransform")
cls.getASuperInterface().hasUnderlyingType("@nestjs/common", "PipeTransform")
)
}

View File

@@ -1,10 +1,10 @@
| bar.js:5:14:5:14 | x | x |
| bar.js:5:14:5:14 | x | ns.very.long.namespace |
| bar.js:5:14:5:18 | x.Foo | ns.very.long.namespace.Foo |
| bar.js:12:14:12:17 | iife | iife |
| bar.js:12:14:12:17 | iife | IIFE |
| bar.js:12:14:12:21 | iife.Foo | IIFE.Foo |
| closure.js:8:12:8:15 | goog | goog |
| closure.js:8:12:8:19 | goog.net | goog.net |
| closure.js:8:12:8:28 | goog.net.SomeType | goog.net.SomeType |
| closure.js:9:12:9:14 | net | net |
| closure.js:9:12:9:14 | net | goog.net |
| closure.js:9:12:9:23 | net.SomeType | goog.net.SomeType |
| closure.js:10:12:10:19 | SomeType | goog.net.SomeType |

View File

@@ -1,3 +1,3 @@
import javascript
query string test_hasQualifiedName(JSDocNamedTypeExpr expr) { expr.hasQualifiedName(result) }
query string test_hasUnderlyingType(JSDocNamedTypeExpr expr) { expr.hasUnderlyingType(result) }

View File

@@ -2,13 +2,14 @@ test_isString
| tst.js:2:12:2:17 | string |
test_isNumber
| tst.js:3:12:3:17 | number |
test_QualifiedName
test_hasUnderlyingType
| VarType | tst.js:9:13:9:19 | VarType |
| boolean | tst.js:5:14:5:20 | boolean |
| foo | tst.js:4:12:4:14 | foo |
| foo.bar | tst.js:4:12:4:18 | foo.bar |
| foo.bar.baz | tst.js:4:12:4:22 | foo.bar.baz |
| number | tst.js:3:12:3:17 | number |
| number | tst.js:3:12:3:18 | number? |
| string | tst.js:2:12:2:17 | string |
test_ParameterType
| tst.js:7:12:7:12 | x | tst.js:2:12:2:17 | string |

View File

@@ -4,7 +4,7 @@ query TypeAnnotation test_isString() { result.isString() }
query TypeAnnotation test_isNumber() { result.isNumber() }
query TypeAnnotation test_QualifiedName(string name) { result.hasQualifiedName(name) }
query TypeAnnotation test_hasUnderlyingType(string name) { result.hasUnderlyingType(name) }
query TypeAnnotation test_ParameterType(Parameter p) { result = p.getTypeAnnotation() }

View File

@@ -1,5 +1,5 @@
import javascript
from TypeAnnotation type, string mod, string name
where type.hasQualifiedName(mod, name)
where type.hasUnderlyingType(mod, name)
select type, mod, name

View File

@@ -1,13 +1,15 @@
hasQualifiedNameModule
| default-import | default | tst.ts:11:9:11:21 | DefaultImport |
hasUnderlyingTypeModule
| default-import | | tst.ts:11:9:11:21 | DefaultImport |
| global | UnresolvedName | tst.ts:12:9:12:22 | UnresolvedName |
| import-assign | | tst.ts:10:9:10:11 | asn |
| import-assign | Foo | tst.ts:10:9:10:15 | asn.Foo |
| named-import | Name1 | tst.ts:7:9:7:13 | Name1 |
| named-import | Name1 | tst.ts:13:9:13:13 | Name1 |
| named-import | Name1 | tst.ts:13:9:13:21 | Name1<number> |
| named-import | Name2 | tst.ts:8:9:8:13 | Name2 |
| namespace-import | | tst.ts:9:9:9:17 | namespace |
| namespace-import | Foo | tst.ts:9:9:9:21 | namespace.Foo |
| tst.ts | ExportedClass | relative.ts:4:8:4:20 | ExportedClass |
hasQualifiedNameGlobal
hasUnderlyingTypeGlobal
| UnresolvedName | tst.ts:12:9:12:22 | UnresolvedName |
paramExample
| tst.ts:7:5:7:6 | x1 |

View File

@@ -1,13 +1,13 @@
import javascript
query TypeAnnotation hasQualifiedNameModule(string moduleName, string member) {
result.hasQualifiedName(moduleName, member)
query TypeAnnotation hasUnderlyingTypeModule(string moduleName, string member) {
result.hasUnderlyingType(moduleName, member)
}
query TypeAnnotation hasQualifiedNameGlobal(string globalName) {
result.hasQualifiedName(globalName)
query TypeAnnotation hasUnderlyingTypeGlobal(string globalName) {
result.hasUnderlyingType(globalName)
}
query Parameter paramExample() {
result.getTypeAnnotation().hasQualifiedName("named-import", "Name1")
result.getTypeAnnotation().hasUnderlyingType("named-import", "Name1")
}

View File

@@ -6,5 +6,4 @@
underlyingTypeNode
| foo | | file://:0:0:0:0 | use moduleImport("foo").getMember("exports") |
| foo | | foo.ts:1:8:1:10 | use moduleImport("foo").getMember("exports").getMember("default") |
| foo | Bar | foo.ts:3:1:5:1 | use moduleImport("foo").getMember("exports").getMember("Bar").getInstance() |
| foo | Bar | foo.ts:3:12:3:12 | use moduleImport("foo").getMember("exports").getMember("Bar").getInstance() |