Files
codeql/javascript/ql/lib/semmle/javascript/JSDoc.qll

687 lines
18 KiB
Plaintext

/** Provides classes for working with JSDoc comments. */
overlay[local?]
module;
import javascript
private import semmle.javascript.internal.CachedStages
/**
* A JSDoc comment.
*
* Example:
*
* <pre>
* /**
* * An example JSDoc comment documenting a constructor function.
* *
* * @constructor
* * @param {Object=} options An object literal with options.
* *&#47;
* </pre>
*/
class JSDoc extends @jsdoc, Locatable {
/** Gets the description text of this JSDoc comment. */
string getDescription() { jsdoc(this, result, _) }
/** Gets the raw comment this JSDoc comment is an interpretation of. */
Comment getComment() { jsdoc(this, _, result) }
/** Gets a JSDoc tag within this JSDoc comment. */
JSDocTag getATag() { result.getParent() = this }
/** Gets a JSDoc tag within this JSDoc comment with the given title. */
JSDocTag getATagByTitle(string title) {
result = this.getATag() and
result.getTitle() = title
}
/** Gets the element to which this JSDoc comment is attached */
Documentable getDocumentedElement() { result.getDocumentation() = this }
override string toString() { result = this.getComment().toString() }
}
/**
* A program element that can have a JSDoc comment.
*
* Example:
*
* <pre>
* /**
* * An example JSDoc comment documenting a constructor function.
* *
* * @constructor
* * @param {Object=} options An object literal with options.
* *&#47;
* function MyConstructor(options) { // documentable
* this.options = options || {};
* }
* </pre>
*/
abstract class Documentable extends AstNode {
/** Gets the JSDoc comment for this element, if any. */
cached
JSDoc getDocumentation() {
Stages::Ast::ref() and result.getComment().getNextToken() = this.getFirstToken()
}
}
/**
* A syntactic element that a JSDoc type expression may be nested in, that is,
* either a JSDoc tag or another JSDoc type expression.
*
* Examples:
*
* ```
* // the `@param` tag and the `...=` type expressions are JSDoc type expression parents
* @param {Object=} options An object literal with options.
* ```
*/
class JSDocTypeExprParent extends @jsdoc_type_expr_parent, Locatable {
/** Gets the JSDoc comment to which this element belongs. */
JSDoc getJSDocComment() { none() }
}
/**
* A JSDoc tag.
*
* Examples:
*
* ```
* @param {Object=} options An object literal with options.
* @return {!Server}
* ```
*/
class JSDocTag extends @jsdoc_tag, JSDocTypeExprParent {
/** Gets the tag title; for instance, the title of a `@param` tag is `"param"`. */
string getTitle() { jsdoc_tags(this, result, _, _, _) }
/** Gets the JSDoc comment this tag belongs to. */
JSDoc getParent() { jsdoc_tags(this, _, result, _, _) }
/** Gets the index of this tag within its parent comment. */
int getIndex() { jsdoc_tags(this, _, _, result, _) }
override string toString() { jsdoc_tags(this, _, _, _, result) }
/** Gets the description associated with the tag. */
string getDescription() { jsdoc_tag_descriptions(this, result) }
/**
* Gets the (optional) name associated with the tag, such as the name of the documented parameter
* for a `@param` tag.
*/
string getName() { jsdoc_tag_names(this, result) }
/**
* Gets the (optional) type associated with the tag, such as the type of the documented parameter
* for a `@param` tag.
*/
JSDocTypeExpr getType() { result.getParent() = this }
/** Holds if this tag documents a simple name (as opposed to a name path). */
predicate documentsSimpleName() {
exists(string name | name = this.getName() | not name.matches("%.%"))
}
/** Gets the toplevel in which this tag appears. */
TopLevel getTopLevel() { result = this.getParent().getComment().getTopLevel() }
override JSDoc getJSDocComment() { result.getATag() = this }
}
/**
* A `@param` tag.
*
* Example:
*
* ```
* @param {Object=} options An object literal with options.
* ```
*/
class JSDocParamTag extends JSDocTag {
JSDocParamTag() { this.getTitle().regexpMatch("param|arg(ument)?") }
/** Gets the parameter this tag refers to, if it can be determined. */
Variable getDocumentedParameter() {
exists(Parameterized parm | parm.getDocumentation() = this.getParent() |
result = pragma[only_bind_out](parm).getParameterVariable(this.getName())
)
}
}
/**
* A JSDoc type expression.
*
* Examples:
*
* ```
* *
* Array<string>
* !Object
* ```
*/
class JSDocTypeExpr extends @jsdoc_type_expr, JSDocTypeExprParent, TypeAnnotation {
/**
* Gets the syntactic element in which this type expression is nested, which may either
* be another type expression or a JSDoc tag.
*/
JSDocTypeExprParent getParent() { jsdoc_type_exprs(this, _, result, _, _) }
/**
* Gets the index of this type expression within its parent.
*/
int getIndex() { jsdoc_type_exprs(this, _, _, result, _) }
/**
* Gets the `i`th child type expression of this type expression.
*
* _Note_: the indices of child type expressions in their parent elements are an implementation
* detail that may change between versions of the extractor.
*/
JSDocTypeExpr getChild(int i) { jsdoc_type_exprs(result, _, this, i, _) }
override string toString() { jsdoc_type_exprs(this, _, _, _, result) }
override JSDoc getJSDocComment() { result = this.getParent().getJSDocComment() }
override Stmt getEnclosingStmt() {
exists(Documentable astNode | astNode.getDocumentation() = this.getJSDocComment() |
result = astNode
or
result = astNode.(ExprOrType).getEnclosingStmt()
or
result = astNode.(Property).getObjectExpr().getEnclosingStmt()
)
}
override Function getEnclosingFunction() { result = this.getContainer() }
override TopLevel getTopLevel() { result = this.getEnclosingStmt().getTopLevel() }
}
/**
* An `any` type expression.
*
* Example:
*
* ```
* *
* ```
*/
class JSDocAnyTypeExpr extends @jsdoc_any_type_expr, JSDocTypeExpr {
override predicate isAny() { any() }
}
/**
* A null type expression.
*
* Example:
*
* ```
* null
* ```
*/
class JSDocNullTypeExpr extends @jsdoc_null_type_expr, JSDocTypeExpr {
override predicate isNull() { any() }
}
/**
* A type expression representing the type of `undefined`.
*
* Example:
*
* ```
* undefined
* ```
*/
class JSDocUndefinedTypeExpr extends @jsdoc_undefined_type_expr, JSDocTypeExpr {
override predicate isUndefined() { any() }
}
/**
* A type expression representing an unknown type.
*
* Example:
*
* ```
* ?
* ```
*/
class JSDocUnknownTypeExpr extends @jsdoc_unknown_type_expr, JSDocTypeExpr {
override predicate isUnknownKeyword() { any() }
}
/**
* A type expression representing the void type.
*
* Example:
*
* ```
* void
* ```
*/
class JSDocVoidTypeExpr extends @jsdoc_void_type_expr, JSDocTypeExpr {
override predicate isVoid() { any() }
}
/**
* An identifier in a JSDoc type expression, such as `Object` or `string`.
*
* Note that qualified names consist of multiple identifier nodes.
*/
class JSDocIdentifierTypeExpr extends @jsdoc_identifier_type_expr, JSDocTypeExpr {
/**
* Gets the name of the identifier.
*/
string getName() { result = this.toString() }
override predicate isString() { this.getName() = "string" }
override predicate isStringy() {
exists(string name | name = this.getName() |
name = "string" or
name = "String"
)
}
override predicate isNumber() { this.getName() = "number" }
override predicate isNumbery() {
exists(string name | name = this.getName() |
name = ["number", "Number", "double", "Double", "int", "integer", "Integer"]
)
}
override predicate isBoolean() { this.getName() = "boolean" }
override predicate isBooleany() {
this.getName() = "boolean" or
this.getName() = "Boolean" or
this.getName() = "bool"
}
override predicate isRawFunction() { this.getName() = "Function" }
}
private AstNode getAncestorInScope(Documentable doc) {
any(JSDocLocalTypeAccess t).getJSDocComment() = doc.getDocumentation() and // restrict to cases where we need this
result = doc.getParent()
or
exists(AstNode mid |
mid = getAncestorInScope(doc) and
not mid = any(Scope s).getScopeElement() and
result = mid.getParent()
)
}
private Scope getScope(Documentable doc) { result.getScopeElement() = getAncestorInScope(doc) }
pragma[nomagic]
private predicate shouldResolveName(TopLevel top, string name) {
exists(JSDocLocalTypeAccess access |
access.getName() = name and
access.getTopLevel() = top
)
}
private LexicalName getOwnLocal(Scope scope, string name, DeclarationSpace space) {
scope = result.getScope() and
name = result.getName() and
space = result.getDeclarationSpace() and
shouldResolveName(scope.getScopeElement().getTopLevel(), name) // restrict size of predicate
}
private LexicalName resolveLocal(Scope scope, string name, DeclarationSpace space) {
result = getOwnLocal(scope, name, space)
or
result = resolveLocal(scope.getOuterScope(), name, space) and
not exists(getOwnLocal(scope, name, space))
}
/**
* An unqualified identifier in a JSDoc type expression.
*
* Example:
*
* ```
* string
* Object
* ```
*/
class JSDocLocalTypeAccess extends JSDocIdentifierTypeExpr {
JSDocLocalTypeAccess() { not this = any(JSDocQualifiedTypeAccess a).getNameNode() }
/** Gets a variable, type-name, or namespace that this expression may resolve to. */
LexicalName getALexicalName() {
result =
resolveLocal(getScope(this.getJSDocComment().getDocumentedElement()), this.getName(), _)
}
}
/**
* A qualified type name in a JSDoc type expression, such as `X.Y`.
*/
class JSDocQualifiedTypeAccess extends @jsdoc_qualified_type_expr, JSDocTypeExpr {
/**
* Gets the base of this access, such as the `X` in `X.Y`.
*/
JSDocTypeExpr getBase() { result = this.getChild(0) }
/**
* Gets the node naming the member being accessed, such as the `Y` node in `X.Y`.
*/
JSDocIdentifierTypeExpr getNameNode() { result = this.getChild(1) }
/**
* Gets the name being accessed, such as `Y` in `X.Y`.
*/
string getName() { result = this.getNameNode().getName() }
}
/**
* A type expression referring to a named type.
*
* Example:
*
* ```
* string
* Object
* Namespace.Type
* ```
*/
class JSDocNamedTypeExpr extends JSDocTypeExpr {
JSDocNamedTypeExpr() {
this instanceof JSDocLocalTypeAccess
or
this instanceof JSDocQualifiedTypeAccess
}
/**
* Gets the name directly as it appears in this type, including any qualifiers.
*
* For example, for `X.Y` this gets the string `"X.Y"`.
*/
string getRawName() { result = this.toString() }
/**
* DEPRECATED. Use `getRawName()` instead.
*/
deprecated string getName() { result = this.toString() }
/**
* Holds if this name consists of the unqualified name `prefix`
* followed by a (possibly empty) `suffix`.
*
* For example:
* - `foo.bar.Baz` has prefix `foo` and suffix `.bar.Baz`.
* - `Baz` has prefix `Baz` and an empty 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
prefix = name.regexpCapture(regex, 1) and
suffix = name.regexpCapture(regex, 2)
)
}
}
/**
* An applied type expression.
*
* Example:
*
* ```
* Array<string>
* ```
*/
class JSDocAppliedTypeExpr extends @jsdoc_applied_type_expr, JSDocTypeExpr {
/** Gets the head type expression, such as `Array` in `Array<string>`. */
JSDocTypeExpr getHead() { result = this.getChild(-1) }
/**
* Gets the `i`th argument type of the applied type expression.
*
* For example, in `Array<string>`, `string` is the 0th argument type.
*/
JSDocTypeExpr getArgument(int i) { i >= 0 and result = this.getChild(i) }
/**
* Gets an argument type of the applied type expression.
*
* For example, in `Array<string>`, `string` is the only argument type.
*/
JSDocTypeExpr getAnArgument() { result = this.getArgument(_) }
}
/**
* A nullable type expression.
*
* Example:
*
* ```
* ?Array
* ```
*/
class JSDocNullableTypeExpr extends @jsdoc_nullable_type_expr, JSDocTypeExpr {
/** Gets the argument type expression. */
JSDocTypeExpr getTypeExpr() { result = this.getChild(0) }
/** Holds if the `?` operator of this type expression is written in prefix notation. */
predicate isPrefix() { jsdoc_prefix_qualifier(this) }
override JSDocTypeExpr getAnUnderlyingType() { result = this.getTypeExpr().getAnUnderlyingType() }
}
/**
* A non-nullable type expression.
*
* Example:
*
* ```
* !Array
* ```
*/
class JSDocNonNullableTypeExpr extends @jsdoc_non_nullable_type_expr, JSDocTypeExpr {
/** Gets the argument type expression. */
JSDocTypeExpr getTypeExpr() { result = this.getChild(0) }
/** Holds if the `!` operator of this type expression is written in prefix notation. */
predicate isPrefix() { jsdoc_prefix_qualifier(this) }
override JSDocTypeExpr getAnUnderlyingType() { result = this.getTypeExpr().getAnUnderlyingType() }
}
/**
* A record type expression.
*
* Example:
*
* ```
* { x: number, y: string }
* ```
*/
class JSDocRecordTypeExpr extends @jsdoc_record_type_expr, JSDocTypeExpr {
/** Gets the name of the `i`th field of the record type. */
string getFieldName(int i) { jsdoc_record_field_name(this, i, result) }
/** Gets the name of some field of the record type. */
string getAFieldName() { result = this.getFieldName(_) }
/** Gets the type of the `i`th field of the record type. */
JSDocTypeExpr getFieldType(int i) { result = this.getChild(i) }
/** Gets the type of the field with the given name. */
JSDocTypeExpr getFieldTypeByName(string fieldname) {
exists(int idx | fieldname = this.getFieldName(idx) and result = this.getFieldType(idx))
}
}
/**
* An array type expression.
*
* Example:
*
* ```
* [string]
* ```
*/
class JSDocArrayTypeExpr extends @jsdoc_array_type_expr, JSDocTypeExpr {
/** Gets the type of the `i`th element of this array type. */
JSDocTypeExpr getElementType(int i) { result = this.getChild(i) }
/** Gets an element type of this array type. */
JSDocTypeExpr getAnElementType() { result = this.getElementType(_) }
}
/**
* A union type expression.
*
* Example:
*
* ```
* number|string
* ```
*/
class JSDocUnionTypeExpr extends @jsdoc_union_type_expr, JSDocTypeExpr {
/** Gets one of the type alternatives of this union type. */
JSDocTypeExpr getAnAlternative() { result = this.getChild(_) }
override JSDocTypeExpr getAnUnderlyingType() {
result = this.getAnAlternative().getAnUnderlyingType()
}
}
/**
* A function type expression.
*
* Example:
*
* ```
* function(string): number
* ```
*/
class JSDocFunctionTypeExpr extends @jsdoc_function_type_expr, JSDocTypeExpr {
/** Gets the result type of this function type. */
JSDocTypeExpr getResultType() { result = this.getChild(-1) }
/** Gets the receiver type of this function type. */
JSDocTypeExpr getReceiverType() { result = this.getChild(-2) }
/** Gets the `i`th parameter type of this function type. */
JSDocTypeExpr getParameterType(int i) { i >= 0 and result = this.getChild(i) }
/** Gets a parameter type of this function type. */
JSDocTypeExpr getAParameterType() { result = this.getParameterType(_) }
/** Holds if this function type is a constructor type. */
predicate isConstructorType() { jsdoc_has_new_parameter(this) }
}
/**
* An optional parameter type.
*
* Example:
*
* ```
* number=
* ```
*/
class JSDocOptionalParameterTypeExpr extends @jsdoc_optional_type_expr, JSDocTypeExpr {
/** Gets the underlying type of this optional type. */
JSDocTypeExpr getUnderlyingType() { result = this.getChild(0) }
override JSDocTypeExpr getAnUnderlyingType() {
result = this.getUnderlyingType().getAnUnderlyingType()
}
}
/**
* A rest parameter type.
*
* Example:
*
* ```
* string...
* ```
*/
class JSDocRestParameterTypeExpr extends @jsdoc_rest_type_expr, JSDocTypeExpr {
/** Gets the underlying type of this rest parameter type. */
JSDocTypeExpr getUnderlyingType() { result = this.getChild(0) }
}
/**
* An error encountered while parsing a JSDoc comment.
*/
class JSDocError extends @jsdoc_error {
/** Gets the tag that triggered the error. */
JSDocTag getTag() { jsdoc_errors(this, result, _, _) }
/** Gets the message associated with the error. */
string getMessage() { jsdoc_errors(this, _, result, _) }
/** Gets a textual representation of this element. */
string toString() { jsdoc_errors(this, _, _, result) }
}
module JSDoc {
/**
* A statement container which may declare JSDoc name aliases.
*/
overlay[global]
deprecated class Environment extends StmtContainer {
/**
* Gets the fully qualified name aliased by the given unqualified name
* within this container.
*/
string resolveAlias(string alias) {
this.getNodeFromAlias(alias) = AccessPath::getAReferenceOrAssignmentTo(result)
}
/**
* Gets a data flow node from which the type name `alias` should
* be resolved.
*/
DataFlow::Node getNodeFromAlias(string alias) {
exists(VarDef def, VarRef ref |
def.getContainer() = this and
ref = def.getTarget().(BindingPattern).getABindingVarRef() and
ref.getName() = alias and
isTypenamePrefix(ref.getName()) // restrict size of predicate
|
exists(PropertyPattern p |
ref = p.getValuePattern() and
result.getAstNode() = p
)
or
result = DataFlow::valueNode(def.(Stmt))
or
ref = def.getTarget() and
result = def.getSource().flow()
or
result = DataFlow::parameterNode(def)
)
}
/**
* Holds if the aliases declared in this environment should be in scope
* within the given container.
*
* Specifically, this holds if this environment declares at least one
* alias and is an ancestor of `container`.
*/
final predicate isContainerInScope(StmtContainer container) {
exists(this.resolveAlias(_)) and // restrict size of predicate
container = this
or
this.isContainerInScope(container.getEnclosingContainer())
}
}
pragma[noinline]
deprecated private predicate isTypenamePrefix(string name) {
any(JSDocNamedTypeExpr expr).hasNameParts(name, _)
}
}