CVE-2024-32468
Description
Deno is a runtime for JavaScript and TypeScript written in rust. Several cross-site scripting vulnerabilities existed in the deno_doc crate which lead to Self-XSS with deno doc --html. 1.) XSS in generated search_index.js, deno_doc outputs a JavaScript file for searching. However, the generated file used innerHTML on unsanitzed HTML input. 2.) XSS via property, method and enum names, deno_doc did not sanitize property names, method names and enum names. The first XSS most likely didn't have an impact since deno doc --html is expected to be used locally with own packages.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Multiple cross-site scripting vulnerabilities in deno_doc crate allow self-XSS when generating HTML documentation via `deno doc --html`.
Vulnerability
Overview
CVE-2024-32468 describes two cross-site scripting (XSS) vulnerabilities in the deno_doc crate, which is used by the Deno runtime to generate HTML documentation from JavaScript and TypeScript source code. The first issue occurs in the generated search_index.js file, where the code uses innerHTML to insert unsanitized HTML input, allowing injection of arbitrary scripts. The second issue stems from a lack of sanitization of property names, method names, and enum names when rendering documentation pages. Both vulnerabilities stem from insufficient output encoding [1][2].
Exploitation
Prerequisites
Exploitation requires a user to run deno doc --html on a package that contains maliciously crafted identifiers or documentation content. Since deno doc --html is typically used locally with own packages, the attack vector is limited to self-XSS scenarios where an attacker tricks a user into generating documentation for a malicious package. The search_index.js file directly embeds user-controlled data without escaping, as seen in the reference to the template file [3].
Impact
An attacker who successfully exploits these vulnerabilities can execute arbitrary JavaScript in the context of the generated HTML documentation page. This could lead to theft of local data, session hijacking, or other actions performed with the user's privileges. However, the practical impact is reduced because the attack requires the user to generate documentation for a malicious package, making it a self-XSS vulnerability [2].
Mitigation
The vulnerabilities have been addressed in a commit to the deno_doc repository by applying proper HTML escaping via html_escape::encode_safe and removing unsafe innerHTML assignments [1]. Users should update to the latest version of deno_doc that includes this fix. No workarounds are documented, and the issue is not listed in CISA's Known Exploited Vulnerabilities catalog.
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
deno_doccrates.io | < 0.119.0 | 0.119.0 |
Affected products
4Patches
10f1ef3efbf16feat: fix various minor issues & improve inferring (#533)
35 files changed · +1307 −100
src/html/mod.rs+1 −1 modified@@ -221,7 +221,7 @@ impl core::ops::Deref for DocNodeWithContext { pub fn setup_hbs<'t>() -> Result<Handlebars<'t>, anyhow::Error> { let mut reg = Handlebars::new(); - reg.register_escape_fn(|str| html_escape::encode_safe(str).to_string()); + reg.register_escape_fn(|str| html_escape::encode_safe(str).into_owned()); reg.set_strict_mode(true); #[cfg(debug_assertions)]
src/html/parameters.rs+1 −4 modified@@ -19,7 +19,6 @@ pub(crate) fn render_params( format!("<span>{items}</span>") } else { - // TODO(bartlomieju): refactor to use template let mut items = Vec::with_capacity(params.len()); for (i, def) in params.iter().enumerate() { @@ -63,13 +62,11 @@ pub(crate) fn param_name(param: &ParamDef, i: usize) -> (String, String) { ), ParamPatternDef::Assign { left, .. } => param_name(left, i), ParamPatternDef::Identifier { name, .. } => { - (html_escape::encode_safe(name).to_string(), name.clone()) + (html_escape::encode_safe(name).into_owned(), name.clone()) } ParamPatternDef::Rest { arg } => ( format!("<span>...{}</span>", param_name(arg, i).0), format!("(...{})", param_name(arg, i).1), ), } } - -// TODO: classes: italic
src/html/search.rs+21 −10 modified@@ -36,9 +36,13 @@ fn doc_node_into_search_index_nodes( .ns_qualifiers .is_empty() { - name.to_string() + html_escape::encode_safe(name).into_owned() } else { - format!("{}.{}", doc_nodes[0].ns_qualifiers.join("."), name) + format!( + "{}.{}", + doc_nodes[0].ns_qualifiers.join("."), + html_escape::encode_safe(name) + ) }; if doc_nodes[0].kind != DocNodeKind::Namespace { @@ -50,15 +54,16 @@ fn doc_node_into_search_index_nodes( .map(|main_entrypoint| main_entrypoint != &location_url) .unwrap_or(true) { - ctx.url_to_short_path(&location_url).to_name() + html_escape::encode_safe(&ctx.url_to_short_path(&location_url).to_name()) + .into_owned() } else { String::new() }; return vec![SearchIndexNode { kind: kinds, name, - file: doc_nodes[0].origin.as_str().to_string(), + file: html_escape::encode_safe(doc_nodes[0].origin.as_str()).into_owned(), location, declaration_kind: doc_nodes[0].declaration_kind, deprecated, @@ -77,15 +82,16 @@ fn doc_node_into_search_index_nodes( .map(|main_entrypoint| main_entrypoint != &location_url) .unwrap_or(true) { - ctx.url_to_short_path(&location_url).to_name() + html_escape::encode_safe(&ctx.url_to_short_path(&location_url).to_name()) + .into_owned() } else { String::new() }; nodes.push(SearchIndexNode { kind: kinds, name, - file: doc_nodes[0].origin.as_str().to_string(), + file: html_escape::encode_safe(doc_nodes[0].origin.as_str()).into_owned(), location, declaration_kind: doc_nodes[0].declaration_kind, deprecated, @@ -117,23 +123,28 @@ fn doc_node_into_search_index_nodes( .map(|main_entrypoint| main_entrypoint != &location_url) .unwrap_or(true) { - ctx.url_to_short_path(&location_url).to_name() + html_escape::encode_safe(&ctx.url_to_short_path(&location_url).to_name()) + .into_owned() } else { String::new() }; let name = if ns_qualifiers_.is_empty() { - el_name.to_string() + html_escape::encode_safe(el_name).into_owned() } else { - format!("{}.{}", ns_qualifiers_.join("."), el_name) + format!( + "{}.{}", + ns_qualifiers_.join("."), + html_escape::encode_safe(el_name) + ) }; let kinds = el_nodes.iter().map(|node| node.kind).collect(); nodes.push(SearchIndexNode { kind: kinds, name, - file: doc_nodes[0].origin.as_str().to_string(), + file: html_escape::encode_safe(doc_nodes[0].origin.as_str()).into_owned(), location, declaration_kind: el_nodes[0].declaration_kind, deprecated,
src/html/symbols/class.rs+5 −4 modified@@ -42,6 +42,7 @@ pub(crate) fn render_class( if let Some(type_params) = crate::html::types::render_type_params( ctx, + &doc_node.js_doc, &class_def.type_params, &doc_node.location, ) { @@ -127,7 +128,7 @@ fn render_constructors( DocEntryCtx::new( ctx, &id, - name, + &html_escape::encode_safe(&name), None, &format!("({params})"), HashSet::from([Tag::New]), @@ -379,7 +380,7 @@ fn render_class_accessor( DocEntryCtx::new( ctx, &id, - name, + &html_escape::encode_safe(&name), ctx.lookup_symbol_href(&qualify_drilldown_name( class_name, name, @@ -418,7 +419,7 @@ fn render_class_method( Some(DocEntryCtx::new( ctx, &id, - &method.name, + &html_escape::encode_safe(&method.name), ctx.lookup_symbol_href(&qualify_drilldown_name( class_name, &method.name, @@ -461,7 +462,7 @@ fn render_class_property( DocEntryCtx::new( ctx, &id, - &property.name, + &html_escape::encode_safe(&property.name), ctx.lookup_symbol_href(&qualify_drilldown_name( class_name, &property.name,
src/html/symbols/enum.rs+1 −1 modified@@ -24,7 +24,7 @@ pub(crate) fn render_enum( DocEntryCtx::new( render_ctx, &id, - &member.name, + &html_escape::encode_safe(&member.name), None, &member .init
src/html/symbols/function.rs+6 −6 modified@@ -4,7 +4,6 @@ use crate::html::parameters::render_params; use crate::html::render_context::RenderContext; use crate::html::types::render_type_def; use crate::html::types::render_type_def_colon; -use crate::html::types::render_type_params; use crate::html::types::type_params_summary; use crate::html::util::*; use crate::html::DocNodeWithContext; @@ -154,8 +153,6 @@ fn render_single_function( .collect::<HashSet<&str>>(); let ctx = &ctx.with_current_type_params(current_type_params); - // TODO: tags - let param_docs = doc_node .js_doc @@ -251,9 +248,12 @@ fn render_single_function( sections.push(examples); } - if let Some(type_params) = - render_type_params(ctx, &function_def.type_params, &doc_node.location) - { + if let Some(type_params) = crate::html::types::render_type_params( + ctx, + &doc_node.js_doc, + &function_def.type_params, + &doc_node.location, + ) { sections.push(type_params); }
src/html/symbols/interface.rs+10 −8 modified@@ -2,7 +2,6 @@ use crate::html::parameters::render_params; use crate::html::render_context::RenderContext; use crate::html::symbols::class::IndexSignatureCtx; use crate::html::types::render_type_def_colon; -use crate::html::types::render_type_params; use crate::html::types::type_params_summary; use crate::html::util::*; use crate::html::DocNodeWithContext; @@ -22,9 +21,12 @@ pub(crate) fn render_interface( let mut sections = vec![]; - if let Some(type_params) = - render_type_params(ctx, &interface_def.type_params, &doc_node.location) - { + if let Some(type_params) = crate::html::types::render_type_params( + ctx, + &doc_node.js_doc, + &interface_def.type_params, + &doc_node.location, + ) { sections.push(type_params); } @@ -185,9 +187,9 @@ fn render_properties( ctx, &id, &if property.computed { - format!("[{}]", property.name) + format!("[{}]", html_escape::encode_safe(&property.name)) } else { - property.name.clone() + html_escape::encode_safe(&property.name).into_owned() }, ctx.lookup_symbol_href(&qualify_drilldown_name( interface_name, @@ -226,9 +228,9 @@ fn render_methods( let name = if method.name == "new" { "<span>new</span>".to_string() } else if method.computed { - format!("[{}]", method.name) + format!("[{}]", html_escape::encode_safe(&method.name)) } else { - method.name.clone() + html_escape::encode_safe(&method.name).into_owned() }; let return_type = method
src/html/symbols/mod.rs+1 −1 modified@@ -200,7 +200,7 @@ impl DocBlockSubtitleCtx { if let Some(extends) = class_def.extends.as_ref() { class_extends = Some(DocBlockClassSubtitleExtendsCtx { href: ctx.lookup_symbol_href(extends), - symbol: extends.to_owned(), + symbol: html_escape::encode_safe(extends).into_owned(), type_args: super::types::type_arguments( ctx, &class_def.super_type_params,
src/html/symbols/type_alias.rs+2 −1 modified@@ -19,12 +19,13 @@ pub(crate) fn render_type_alias( let id = name_to_id("typeAlias", doc_node.get_name()); - // TODO: tags, TypeParamsDoc + // TODO: tags let mut sections = vec![]; if let Some(type_params) = crate::html::types::render_type_params( ctx, + &doc_node.js_doc, &type_alias_def.type_params, &doc_node.location, ) {
src/html/templates/deprecated.hbs+1 −1 modified@@ -8,7 +8,7 @@ {{~#if (ne this "")~}} <div class="ml-1 pl-2 border-l-4 border-red-300"> - {{{this}}} + {{{this}}} {{! markdown rendering }} </div> {{~/if~}} </div>
src/html/templates/doc_block_subtitle_class.hbs+4 −4 modified@@ -2,7 +2,7 @@ <div> <span class="text-stone-400 italic"> implements </span> {{~#each implements~}} - {{{~this~}}} + {{{~this~}}} {{! typedef rendering }} {{~#unless @last~}} <span>, </span> {{~/unless~}} @@ -15,12 +15,12 @@ <span class="text-stone-400 italic"> extends </span> {{~#if href ~}} <a class="link" href="{{href}}"> - {{{~symbol~}}} + {{~symbol~}} </a> {{~else~}} - <span>{{{symbol}}}</span> + <span>{{symbol}}</span> {{~/if~}} - <span>{{{type_args}}}</span> + <span>{{{type_args}}} {{! typedef rendering }}</span> </div> {{~else~}} {{~/with~}}
src/html/templates/doc_block_subtitle_interface.hbs+1 −1 modified@@ -1,7 +1,7 @@ <div> <span class="text-stone-400 italic"> extends </span> {{~#each extends~}} - {{{~this~}}} + {{{~this~}}} {{! typedef rendering }} {{~#unless @last~}} <span>, </span> {{~/unless~}}
src/html/templates/doc_entry.hbs+2 −2 modified@@ -13,7 +13,7 @@ <code> {{~#if name_href~}} - <a class="font-bold link" href="{{{name_href}}}">{{{name}}}</a> + <a class="font-bold link" href="{{name_href}}">{{{name}}}</a> {{~else~}} <span class="font-bold">{{{name}}}</span> {{~/if~}} @@ -28,7 +28,7 @@ {{~#if js_doc~}} <div class="markdown_border"> - {{{~js_doc~}}} + {{{~js_doc~}}} {{! markdown rendering }} </div> {{~/if~}} </div>
src/html/templates/example.hbs+2 −2 modified@@ -4,10 +4,10 @@ <details id="{{id}}" class="group" open> <summary class="list-none flex items-center gap-2 py-2 rounded-lg w-full leading-6 cursor-pointer"> <div class="text-stone-600 group-open:rotate-90 select-none">▶</div> - {{{~markdown_title~}}} + {{{~markdown_title~}}} {{! markdown rendering }} </summary> <div class="!ml-2 markdown_border"> - {{{~markdown_body~}}} + {{{~markdown_body~}}} {{! markdown rendering }} </div> </details> </div>
src/html/templates/function.hbs+3 −3 modified@@ -1,6 +1,6 @@ {{~#each overloads_ctx~}} <style scoped> - {{{~this.additional_css~}}} + {{{~this.additional_css~}}} {{! css }} </style> <input type="radio" name="{{this.function_id}}" id="{{this.overload_id}}" {{this.html_attrs}} /> {{~/each~}} @@ -10,11 +10,11 @@ <label for="{{this.overload_id}}" class="space-y-1 block px-4 py-2.5 rounded-lg border border-stone-300 cursor-pointer hover:bg-stone-100"> {{~> deprecated this.deprecated ~}} <code class="text-sm"> - <span class="font-bold">{{this.name}}</span><span class="font-medium">{{{this.summary}}}</span> + <span class="font-bold">{{this.name}}</span><span class="font-medium">{{{this.summary}}} {{! typedef rendering }}</span> </code> {{~#if this.summary_doc~}} <div class="markdown_border"> - {{{~this.summary_doc~}}} + {{{~this.summary_doc~}}} {{! markdown rendering }} </div> {{~/if~}} </label>
src/html/templates/index_signature.hbs+2 −2 modified@@ -3,6 +3,6 @@ {{~#if readonly~}} <span>readonly </span> {{~/if~}} - [{{{params}}}] - {{{~ts_type~}}} + [{{{params}}}] {{! param rendering }} + {{{~ts_type~}}} {{! typedef rendering }} </div>
src/html/templates/module_doc.hbs+1 −1 modified@@ -16,7 +16,7 @@ {{~#if toc~}} <nav class="flex-none max-w-52 text-sm max-lg:hidden sticky top-0 py-4 max-h-screen box-border"> - {{{~toc~}}} + {{{~toc~}}} {{! markdown rendering ToC }} </nav> {{~/if~}} </section>
src/html/templates/namespace_section.hbs+1 −1 modified@@ -24,7 +24,7 @@ </div> <div class="block py-1 text-sm leading-5 text-stone-600 !ml-2 markdown_border"> {{~#if this.docs~}} - {{{~this.docs~}}} + {{{~this.docs~}}} {{! markdown rendering }} {{~else~}} <span class="italic">No documentation available</span> {{~/if~}}
src/html/templates/symbol_content.hbs+1 −1 modified@@ -1,6 +1,6 @@ <div class="space-y-7" id="{{id}}"> {{~#if docs~}} - {{{~docs~}}} + {{{~docs~}}} {{! markdown rendering }} {{~/if~}} {{~#each sections~}}
src/html/templates/usages.hbs+2 −2 modified@@ -1,7 +1,7 @@ <div class="p-4 mb-4 border-2 rounded-lg bg-stone-50 border-stone-200 usage"> {{~#each usages~}} <style scoped> - {{{~additional_css~}}} + {{{~additional_css~}}} {{! css }} </style> <input type="radio" name="usage" id="{{name}}" class="hidden" {{~#if @first~}}checked{{~/if~}} /> @@ -18,7 +18,7 @@ <div> {{~#each usages~}} <div id="{{name}}_content"> - {{{~content~}}} + {{{~content~}}} {{! markdown rendering }} </div> {{~/each~}} </div>
src/html/types.rs+30 −3 modified@@ -3,6 +3,8 @@ use super::render_context::RenderContext; use super::util::*; use std::collections::HashSet; +use crate::js_doc::JsDoc; +use crate::js_doc::JsDocTag; use crate::ts_type::LiteralDefKind; use crate::ts_type::TsTypeDefKind; use crate::ts_type_param::TsTypeParamDef; @@ -124,7 +126,19 @@ pub(crate) fn render_type_def( TsTypeDefKind::Optional => { render_type_def(ctx, def.optional.as_ref().unwrap()) } - TsTypeDefKind::TypeQuery => def.type_query.clone().unwrap(), + TsTypeDefKind::TypeQuery => { + let query = def.type_query.as_ref().unwrap(); + + if let Some(href) = ctx.lookup_symbol_href(query) { + format!( + r#"<a href="{}" class="link">{}</a>"#, + html_escape::encode_safe(&href), + html_escape::encode_safe(query), + ) + } else { + format!("<span>{}</span>", html_escape::encode_safe(query)) + } + } TsTypeDefKind::This => "<span>this</span>".to_string(), TsTypeDefKind::FnOrConstructor => { let fn_or_constructor = def.fn_or_constructor.as_ref().unwrap(); @@ -496,6 +510,7 @@ pub(crate) fn type_arguments( pub(crate) fn render_type_params( ctx: &RenderContext, + js_doc: &JsDoc, type_params: &[TsTypeParamDef], location: &crate::Location, ) -> Option<SectionCtx> { @@ -505,6 +520,18 @@ pub(crate) fn render_type_params( let mut items = Vec::with_capacity(type_params.len()); + let type_param_docs = js_doc + .tags + .iter() + .filter_map(|tag| { + if let JsDocTag::Template { name, doc } = tag { + doc.as_ref().map(|doc| (name.as_str(), doc.as_str())) + } else { + None + } + }) + .collect::<std::collections::HashMap<&str, &str>>(); + for type_param in type_params.iter() { let id = name_to_id("type_param", &type_param.name); @@ -533,11 +560,11 @@ pub(crate) fn render_type_params( let content = DocEntryCtx::new( ctx, &id, - &type_param.name, + &html_escape::encode_safe(&type_param.name), None, &format!("{constraint}{default}"), HashSet::new(), - None, + type_param_docs.get(type_param.name.as_str()).cloned(), location, );
src/html/usage.rs+9 −6 modified@@ -62,20 +62,23 @@ pub fn usage_to_md( let mut usage_statement = if is_default { format!( - r#"import {}{import_symbol} from "{url}";"#, - if is_type { "type " } else { "" } + r#"import {}{} from "{url}";"#, + if is_type { "type " } else { "" }, + html_escape::encode_safe(&import_symbol), ) } else { format!( - r#"import {{ {}{import_symbol} }} from "{url}";"#, - if is_type { "type " } else { "" } + r#"import {{ {}{} }} from "{url}";"#, + if is_type { "type " } else { "" }, + html_escape::encode_safe(&import_symbol), ) }; if let Some((usage_symbol, local_var)) = usage_symbol { usage_statement.push_str(&format!( - "\n{} {{ {usage_symbol} }} = {local_var};", - if is_type { "type" } else { "const" } + "\n{} {{ {} }} = {local_var};", + if is_type { "type" } else { "const" }, + html_escape::encode_safe(usage_symbol), )); }
src/html/util.rs+4 −1 modified@@ -18,7 +18,10 @@ lazy_static! { } pub(crate) fn name_to_id(kind: &str, name: &str) -> String { - format!("{kind}_{}", TARGET_RE.replace_all(name, "_")) + format!( + "{kind}_{}", + html_escape::encode_safe(&TARGET_RE.replace_all(name, "_")) + ) } /// A container to hold a list of symbols with their namespaces:
src/js_doc.rs+4 −2 modified@@ -7,7 +7,7 @@ use serde::Serialize; lazy_static! { static ref JS_DOC_TAG_MAYBE_DOC_RE: Regex = Regex::new(r"(?s)^\s*@(deprecated)(?:\s+(.+))?").unwrap(); static ref JS_DOC_TAG_DOC_RE: Regex = Regex::new(r"(?s)^\s*@(category|see|example|tags)(?:\s+(.+))").unwrap(); - static ref JS_DOC_TAG_NAMED_RE: Regex = Regex::new(r"(?s)^\s*@(callback|template)\s+([a-zA-Z_$]\S*)(?:\s+(.+))?").unwrap(); + static ref JS_DOC_TAG_NAMED_RE: Regex = Regex::new(r"(?s)^\s*@(callback|template|typeparam|typeParam)\s+([a-zA-Z_$]\S*)(?:\s+(.+))?").unwrap(); static ref JS_DOC_TAG_NAMED_TYPED_RE: Regex = Regex::new(r"(?s)^\s*@(prop(?:erty)?|typedef)\s+\{([^}]+)\}\s+([a-zA-Z_$]\S*)(?:\s+(.+))?").unwrap(); static ref JS_DOC_TAG_ONLY_RE: Regex = Regex::new(r"^\s*@(constructor|class|ignore|module|public|private|protected|readonly)").unwrap(); static ref JS_DOC_TAG_PARAM_RE: Regex = Regex::new( @@ -168,6 +168,8 @@ pub enum JsDocTag { tags: Vec<String>, }, /// `@template T comment` + /// `@typeparam T comment` + /// `@typeParam T comment` Template { name: String, #[serde(skip_serializing_if = "Option::is_none", default)] @@ -225,7 +227,7 @@ impl From<String> for JsDocTag { let doc = caps.get(3).map(|m| m.as_str().to_string()); match kind { "callback" => Self::Callback { name, doc }, - "template" => Self::Template { name, doc }, + "template" | "typeparam" | "typeParam" => Self::Template { name, doc }, _ => unreachable!("kind unexpected: {}", kind), } } else if let Some(caps) = JS_DOC_TAG_TYPED_RE.captures(&value) {
src/ts_type.rs+122 −2 modified@@ -1380,7 +1380,126 @@ pub fn infer_ts_type_from_expr( // e.g.) const value = {foo: "bar"}; infer_ts_type_from_obj(parsed_source, obj) } - _ => None, + Expr::TsSatisfies(satisfies) => { + // e.g.) const value = {foo: "bar"} satifies Record<string, string>; + infer_ts_type_from_expr(parsed_source, &satisfies.expr, is_const) + } + Expr::Update(_) => { + // e.g.) let foo = 0; + // const bar = foo++; + Some(TsTypeDef::number_with_repr("number")) + } + Expr::TsTypeAssertion(assertion) => { + // e.g.) export const foo = <string> 1; + Some(TsTypeDef::new(parsed_source, &assertion.type_ann)) + } + Expr::TsAs(as_expr) => { + // e.g.) export const foo = 1 as string; + Some(TsTypeDef::new(parsed_source, &as_expr.type_ann)) + } + Expr::Paren(paren) => { + // e.g.) export const foo = (1); + infer_ts_type_from_expr(parsed_source, &paren.expr, is_const) + } + Expr::Await(await_expr) => { + // e.g.) export const foo = await 1; + infer_ts_type_from_expr(parsed_source, &await_expr.arg, is_const) + } + Expr::Cond(cond) => { + // e.g.) export const foo = true ? "a" : 1; + let left = infer_ts_type_from_expr(parsed_source, &cond.cons, is_const)?; + let right = infer_ts_type_from_expr(parsed_source, &cond.alt, is_const)?; + + Some(TsTypeDef { + union: Some(vec![left, right]), + kind: Some(TsTypeDefKind::Union), + ..Default::default() + }) + } + Expr::TsNonNull(non_null) => { + // e.g.) export const foo = (true ? "a" : null)!; + // e.g.) export const foo = null!; + let with_null = + infer_ts_type_from_expr(parsed_source, &non_null.expr, is_const)?; + + if let Some(union) = with_null.union { + let mut non_null_union = union + .into_iter() + .filter(|item| { + if let Some(keyword) = &item.keyword { + return keyword != "null"; + } + + true + }) + .collect::<Vec<_>>(); + + Some(match non_null_union.len() { + 0 => TsTypeDef::keyword("never"), + 1 => non_null_union.remove(0), + _ => TsTypeDef { + union: Some(non_null_union), + kind: Some(TsTypeDefKind::Union), + ..Default::default() + }, + }) + } else if with_null.keyword.is_some_and(|keyword| keyword == "null") { + Some(TsTypeDef::keyword("never")) + } else { + None + } + } + Expr::Bin(bin) => { + // e.g.) export const foo = 1 == "bar"; + // e.g.) export const foo = 1 >> 1; + match bin.op { + BinaryOp::EqEq + | BinaryOp::NotEq + | BinaryOp::EqEqEq + | BinaryOp::NotEqEq + | BinaryOp::Lt + | BinaryOp::LtEq + | BinaryOp::Gt + | BinaryOp::GtEq + | BinaryOp::In + | BinaryOp::InstanceOf => Some(TsTypeDef::bool_with_repr("boolean")), + BinaryOp::LShift + | BinaryOp::RShift + | BinaryOp::ZeroFillRShift + | BinaryOp::Sub + | BinaryOp::Mul + | BinaryOp::Div + | BinaryOp::Mod + | BinaryOp::BitOr + | BinaryOp::BitXor + | BinaryOp::BitAnd + | BinaryOp::Exp => Some(TsTypeDef::number_with_repr("number")), + BinaryOp::LogicalOr + | BinaryOp::LogicalAnd + | BinaryOp::NullishCoalescing + | BinaryOp::Add => None, + } + } + Expr::This(_) + | Expr::Unary(_) + | Expr::Assign(_) + | Expr::Member(_) + | Expr::SuperProp(_) + | Expr::Seq(_) + | Expr::Ident(_) + | Expr::TaggedTpl(_) + | Expr::Class(_) + | Expr::Yield(_) + | Expr::MetaProp(_) + | Expr::JSXMember(_) + | Expr::JSXNamespacedName(_) + | Expr::JSXEmpty(_) + | Expr::JSXElement(_) + | Expr::JSXFragment(_) + | Expr::TsInstantiation(_) + | Expr::PrivateName(_) + | Expr::OptChain(_) + | Expr::Invalid(_) => None, } } @@ -1524,7 +1643,8 @@ fn infer_ts_type_from_lit(lit: &Lit, is_const: bool) -> Option<TsTypeDef> { } } Lit::Regex(regex) => Some(TsTypeDef::regexp(regex.exp.to_string())), - _ => None, + Lit::Null(_null) => Some(TsTypeDef::keyword("null")), + Lit::JSXText(_) => None, } }
src/variable.rs+3 −5 modified@@ -82,23 +82,21 @@ pub fn get_docs_for_var_declarator( }); match &var_declarator.name { - deno_ast::swc::ast::Pat::Ident(ident) => { + Pat::Ident(ident) => { let var_name = ident.id.sym.to_string(); let variable_def = VariableDef { ts_type: maybe_ts_type, kind: var_decl.kind, }; items.push((var_name, variable_def, Some(var_declarator.range()))); } - deno_ast::swc::ast::Pat::Object(pat) => { + Pat::Object(pat) => { for prop in &pat.props { let (name, reassign_name, maybe_range) = match prop { deno_ast::swc::ast::ObjectPatProp::KeyValue(kv) => ( crate::params::prop_name_to_string(module_info.source(), &kv.key), match &*kv.value { - deno_ast::swc::ast::Pat::Ident(ident) => { - Some(ident.sym.to_string()) - } + Pat::Ident(ident) => Some(ident.sym.to_string()), _ => None, // TODO: properly implement }, None,
tests/html_test.rs+1 −0 modified@@ -213,6 +213,7 @@ async fn html_doc_files_rewrite() { "./~/Bar.prototype.html", "./~/Foo.bar.html", "./~/Foo.html", + "./~/Foo.prototype.\"><img src=x onerror=alert(1)>.html", "./~/Foo.prototype.foo.html", "./~/Foo.prototype.html", "./~/Foobar.html",
tests/specs/InferObjectLiteralSatifies.txt+189 −0 added@@ -0,0 +1,189 @@ +# mod.ts +export type Colors = "red" | "green" | "blue"; + +// Ensure that we have exactly the keys from 'Colors'. +export const favoriteColors = { + "red": "yes", + "green": false, + "blue": "kinda", + "platypus": false +// ~~~~~~~~~~ error - "platypus" was never listed in 'Colors'. +} satisfies Record<Colors, unknown>; + +export const g: boolean = favoriteColors.green; + +# diagnostics +error[missing-jsdoc]: exported symbol is missing JSDoc documentation + --> /mod.ts:1:1 + | +1 | export type Colors = "red" | "green" | "blue"; + | ^ + + +error[missing-jsdoc]: exported symbol is missing JSDoc documentation + --> /mod.ts:4:14 + | +4 | export const favoriteColors = { + | ^ + + +error[missing-jsdoc]: exported symbol is missing JSDoc documentation + --> /mod.ts:12:14 + | +12 | export const g: boolean = favoriteColors.green; + | ^ + + +# output.txt +Defined in file:///mod.ts:4:14 + +const favoriteColors: { red: string; green: boolean; blue: string; platypus: boolean; } + +Defined in file:///mod.ts:12:14 + +const g: boolean + +Defined in file:///mod.ts:1:1 + +type Colors = "red" | "green" | "blue" + + +# output.json +[ + { + "kind": "typeAlias", + "name": "Colors", + "location": { + "filename": "file:///mod.ts", + "line": 1, + "col": 0, + "byteIndex": 0 + }, + "declarationKind": "export", + "typeAliasDef": { + "tsType": { + "repr": "", + "kind": "union", + "union": [ + { + "repr": "red", + "kind": "literal", + "literal": { + "kind": "string", + "string": "red" + } + }, + { + "repr": "green", + "kind": "literal", + "literal": { + "kind": "string", + "string": "green" + } + }, + { + "repr": "blue", + "kind": "literal", + "literal": { + "kind": "string", + "string": "blue" + } + } + ] + }, + "typeParams": [] + } + }, + { + "kind": "variable", + "name": "favoriteColors", + "location": { + "filename": "file:///mod.ts", + "line": 4, + "col": 13, + "byteIndex": 116 + }, + "declarationKind": "export", + "variableDef": { + "tsType": { + "repr": "", + "kind": "typeLiteral", + "typeLiteral": { + "methods": [], + "properties": [ + { + "name": "red", + "params": [], + "computed": false, + "optional": false, + "tsType": { + "repr": "string", + "kind": "keyword", + "keyword": "string" + }, + "typeParams": [] + }, + { + "name": "green", + "params": [], + "computed": false, + "optional": false, + "tsType": { + "repr": "boolean", + "kind": "keyword", + "keyword": "boolean" + }, + "typeParams": [] + }, + { + "name": "blue", + "params": [], + "computed": false, + "optional": false, + "tsType": { + "repr": "string", + "kind": "keyword", + "keyword": "string" + }, + "typeParams": [] + }, + { + "name": "platypus", + "params": [], + "computed": false, + "optional": false, + "tsType": { + "repr": "boolean", + "kind": "keyword", + "keyword": "boolean" + }, + "typeParams": [] + } + ], + "callSignatures": [], + "indexSignatures": [] + } + }, + "kind": "const" + } + }, + { + "kind": "variable", + "name": "g", + "location": { + "filename": "file:///mod.ts", + "line": 12, + "col": 13, + "byteIndex": 331 + }, + "declarationKind": "export", + "variableDef": { + "tsType": { + "repr": "boolean", + "kind": "keyword", + "keyword": "boolean" + }, + "kind": "const" + } + } +]
tests/specs/InferObjectLiteral.txt+21 −21 modified@@ -1,33 +1,33 @@ # mod.ts const s: symbol = Symbol.for("s"); - const t: symbol = Symbol.for("t"); +const t: symbol = Symbol.for("t"); - export const a = { - a: "a", - b: new Map<string, number>(), - c: { d: "d" }, - d(e: string): void {}, - f: (g: string): void => {}, - get h(): string { - return "h"; - }, - set h(value: string) { +export const a = { + a: "a", + b: new Map<string, number>(), + c: { d: "d" }, + d(e: string): void {}, + f: (g: string): void => {}, + get h(): string { + return "h"; + }, + set h(value: string) { - }, - [s]: [1, 2, 3, "a"], - [t](u: string): void {}, - }; + }, + [s]: [1, 2, 3, "a"], + [t](u: string): void {}, +}; # diagnostics error[missing-jsdoc]: exported symbol is missing JSDoc documentation - --> /mod.ts:4:18 + --> /mod.ts:4:14 | -4 | export const a = { - | ^ +4 | export const a = { + | ^ # output.txt -Defined in file:///mod.ts:4:18 +Defined in file:///mod.ts:4:14 const a: { d(e: string): void; h(): string; h(value: string); [[t]](u: string): void; a: string; b: Map<string, number>; c: { d: string; }; f: (g: string) => void; [s]: (number | string)[]; } @@ -40,8 +40,8 @@ const a: { d(e: string): void; h(): string; h(value: string); [[t]](u: string): "location": { "filename": "file:///mod.ts", "line": 4, - "col": 17, - "byteIndex": 92 + "col": 13, + "byteIndex": 84 }, "declarationKind": "export", "variableDef": {
tests/specs/InferVariable.txt+341 −0 added@@ -0,0 +1,341 @@ +# mod.ts +export let a = (0); +export const b = a++; +export const c = <string> 1; +export const d = 1 as string; +export const e = await 1; +export const f = true ? "a" : 1; +export const g = (true ? "a" : null)!; +export const h = null!; +export const i = 1 == "bar"; +export const j = 1 >> 1; + +# diagnostics +error[missing-jsdoc]: exported symbol is missing JSDoc documentation + --> /mod.ts:1:12 + | +1 | export let a = (0); + | ^ + + +error[missing-jsdoc]: exported symbol is missing JSDoc documentation + --> /mod.ts:2:14 + | +2 | export const b = a++; + | ^ + + +error[missing-jsdoc]: exported symbol is missing JSDoc documentation + --> /mod.ts:3:14 + | +3 | export const c = <string> 1; + | ^ + + +error[missing-jsdoc]: exported symbol is missing JSDoc documentation + --> /mod.ts:4:14 + | +4 | export const d = 1 as string; + | ^ + + +error[missing-jsdoc]: exported symbol is missing JSDoc documentation + --> /mod.ts:5:14 + | +5 | export const e = await 1; + | ^ + + +error[missing-jsdoc]: exported symbol is missing JSDoc documentation + --> /mod.ts:6:14 + | +6 | export const f = true ? "a" : 1; + | ^ + + +error[missing-jsdoc]: exported symbol is missing JSDoc documentation + --> /mod.ts:7:14 + | +7 | export const g = (true ? "a" : null)!; + | ^ + + +error[missing-jsdoc]: exported symbol is missing JSDoc documentation + --> /mod.ts:8:14 + | +8 | export const h = null!; + | ^ + + +error[missing-jsdoc]: exported symbol is missing JSDoc documentation + --> /mod.ts:9:14 + | +9 | export const i = 1 == "bar"; + | ^ + + +error[missing-jsdoc]: exported symbol is missing JSDoc documentation + --> /mod.ts:10:14 + | +10 | export const j = 1 >> 1; + | ^ + + +# output.txt +Defined in file:///mod.ts:1:12 + +let a: number + +Defined in file:///mod.ts:2:14 + +const b: number + +Defined in file:///mod.ts:3:14 + +const c: string + +Defined in file:///mod.ts:4:14 + +const d: string + +Defined in file:///mod.ts:5:14 + +const e: 1 + +Defined in file:///mod.ts:6:14 + +const f: "a" | 1 + +Defined in file:///mod.ts:7:14 + +const g: "a" + +Defined in file:///mod.ts:8:14 + +const h: never + +Defined in file:///mod.ts:9:14 + +const i: boolean + +Defined in file:///mod.ts:10:14 + +const j: number + + +# output.json +[ + { + "kind": "variable", + "name": "a", + "location": { + "filename": "file:///mod.ts", + "line": 1, + "col": 11, + "byteIndex": 11 + }, + "declarationKind": "export", + "variableDef": { + "tsType": { + "repr": "number", + "kind": "keyword", + "keyword": "number" + }, + "kind": "let" + } + }, + { + "kind": "variable", + "name": "b", + "location": { + "filename": "file:///mod.ts", + "line": 2, + "col": 13, + "byteIndex": 33 + }, + "declarationKind": "export", + "variableDef": { + "tsType": { + "repr": "number", + "kind": "keyword", + "keyword": "number" + }, + "kind": "const" + } + }, + { + "kind": "variable", + "name": "c", + "location": { + "filename": "file:///mod.ts", + "line": 3, + "col": 13, + "byteIndex": 55 + }, + "declarationKind": "export", + "variableDef": { + "tsType": { + "repr": "string", + "kind": "keyword", + "keyword": "string" + }, + "kind": "const" + } + }, + { + "kind": "variable", + "name": "d", + "location": { + "filename": "file:///mod.ts", + "line": 4, + "col": 13, + "byteIndex": 84 + }, + "declarationKind": "export", + "variableDef": { + "tsType": { + "repr": "string", + "kind": "keyword", + "keyword": "string" + }, + "kind": "const" + } + }, + { + "kind": "variable", + "name": "e", + "location": { + "filename": "file:///mod.ts", + "line": 5, + "col": 13, + "byteIndex": 114 + }, + "declarationKind": "export", + "variableDef": { + "tsType": { + "repr": "1", + "kind": "literal", + "literal": { + "kind": "number", + "number": 1.0 + } + }, + "kind": "const" + } + }, + { + "kind": "variable", + "name": "f", + "location": { + "filename": "file:///mod.ts", + "line": 6, + "col": 13, + "byteIndex": 140 + }, + "declarationKind": "export", + "variableDef": { + "tsType": { + "repr": "", + "kind": "union", + "union": [ + { + "repr": "a", + "kind": "literal", + "literal": { + "kind": "string", + "string": "a" + } + }, + { + "repr": "1", + "kind": "literal", + "literal": { + "kind": "number", + "number": 1.0 + } + } + ] + }, + "kind": "const" + } + }, + { + "kind": "variable", + "name": "g", + "location": { + "filename": "file:///mod.ts", + "line": 7, + "col": 13, + "byteIndex": 173 + }, + "declarationKind": "export", + "variableDef": { + "tsType": { + "repr": "a", + "kind": "literal", + "literal": { + "kind": "string", + "string": "a" + } + }, + "kind": "const" + } + }, + { + "kind": "variable", + "name": "h", + "location": { + "filename": "file:///mod.ts", + "line": 8, + "col": 13, + "byteIndex": 212 + }, + "declarationKind": "export", + "variableDef": { + "tsType": { + "repr": "never", + "kind": "keyword", + "keyword": "never" + }, + "kind": "const" + } + }, + { + "kind": "variable", + "name": "i", + "location": { + "filename": "file:///mod.ts", + "line": 9, + "col": 13, + "byteIndex": 236 + }, + "declarationKind": "export", + "variableDef": { + "tsType": { + "repr": "boolean", + "kind": "keyword", + "keyword": "boolean" + }, + "kind": "const" + } + }, + { + "kind": "variable", + "name": "j", + "location": { + "filename": "file:///mod.ts", + "line": 10, + "col": 13, + "byteIndex": 265 + }, + "declarationKind": "export", + "variableDef": { + "tsType": { + "repr": "number", + "kind": "keyword", + "keyword": "number" + }, + "kind": "const" + } + } +]
tests/testdata/multiple/a.ts+1 −0 modified@@ -25,6 +25,7 @@ export class Foo { static bar: "string"; foo: "string"; + '"><img src=x onerror=alert(1)>' = 0; } export class Bar extends Foo {
tests/testdata/symbol_group.json+170 −0 modified@@ -230,6 +230,18 @@ "content": { "kind": "doc_entry", "content": [ + { + "id": "property_"><img src=x onerror=alert(1)>", + "name": ""><img src=x onerror=alert(1)>", + "name_href": "../././~/Foo.prototype.\"><img src=x onerror=alert(1)>.html", + "content": "<span>: <span>number</span></span>", + "anchor": { + "id": "property_"><img src=x onerror=alert(1)>" + }, + "tags": [], + "js_doc": null, + "source_href": null + }, { "id": "property_foo", "name": "foo", @@ -585,6 +597,164 @@ ] } }, + { + "html_head_ctx": { + "title": "Foo.prototype.\"><img src=x onerror=alert(1)> - documentation", + "current_file": ".", + "stylesheet_url": "../styles.css", + "page_stylesheet_url": "../page.css", + "url_search_index": "../search_index.js", + "script_js": "../script.js", + "fuse_js": "../fuse.js", + "url_search": "../search.js" + }, + "sidepanel_ctx": { + "package_name": null, + "partitions": [ + { + "name": "Class", + "symbols": [ + { + "kind": [ + { + "kind": "Class", + "char": "c", + "title": "Class", + "title_lowercase": "class", + "title_plural": "Classes" + } + ], + "name": "Bar", + "href": "../././~/Bar.html", + "active": false, + "deprecated": false + }, + { + "kind": [ + { + "kind": "Class", + "char": "c", + "title": "Class", + "title_lowercase": "class", + "title_plural": "Classes" + } + ], + "name": "Foo", + "href": "../././~/Foo.html", + "active": false, + "deprecated": false + }, + { + "kind": [ + { + "kind": "Class", + "char": "c", + "title": "Class", + "title_lowercase": "class", + "title_plural": "Classes" + } + ], + "name": "Foobar", + "href": "../././~/Foobar.html", + "active": false, + "deprecated": false + } + ] + } + ] + }, + "symbol_group_ctx": { + "name": "Foo.prototype.\"><img src=x onerror=alert(1)>", + "symbols": [ + { + "kind": { + "kind": "Property", + "char": " ", + "title": "Property", + "title_lowercase": "property", + "title_plural": "Properties" + }, + "tags": [], + "subtitle": null, + "content": [ + { + "kind": "other", + "value": { + "id": "", + "docs": null, + "sections": [ + { + "title": "Type", + "content": { + "kind": "doc_entry", + "content": [ + { + "id": "variable_Foo_prototype_"><img src=x onerror=alert(1)>", + "name": "", + "name_href": null, + "content": "<span>number</span>", + "anchor": { + "id": "variable_Foo_prototype_"><img src=x onerror=alert(1)>" + }, + "tags": [], + "js_doc": null, + "source_href": null + } + ] + } + } + ] + } + } + ], + "deprecated": null, + "source_href": null + } + ], + "usages": { + "show_tabs": false, + "usages": [ + { + "name": "", + "content": "<div class=\"markdown flex-1\"><pre class=\"highlight\"><code>import { Foo } from \".\";\nconst { \"><img src=x onerror=alert(1)> } = Foo.prototype;\n</code><button class=\"context_button\" data-copy=\"import { Foo } from ".";\nconst { &quot;&gt;&lt;img src=x onerror=alert(1)&gt; } = Foo.prototype;\n\"><svg width=\"15\" height=\"15\" viewBox=\"0 0 15 15\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <path d=\"M1.55566 2.7C1.55566 2.03726 2.09292 1.5 2.75566 1.5H8.75566C9.41841 1.5 9.95566 2.03726 9.95566 2.7V5.1H12.3557C13.0184 5.1 13.5557 5.63726 13.5557 6.3V12.3C13.5557 12.9627 13.0184 13.5 12.3557 13.5H6.35566C5.69292 13.5 5.15566 12.9627 5.15566 12.3V9.9H2.75566C2.09292 9.9 1.55566 9.36274 1.55566 8.7V2.7ZM6.35566 9.9V12.3H12.3557V6.3H9.95566V8.7C9.95566 9.36274 9.41841 9.9 8.75566 9.9H6.35566ZM8.75566 8.7V2.7L2.75566 2.7V8.7H8.75566Z\" fill=\"#232323\"></path>\n</svg>\n</button><code></code></pre>\n</div>", + "additional_css": "" + } + ] + } + }, + "breadcrumbs_ctx": { + "parts": [ + { + "name": "index", + "href": "../", + "is_symbol": false, + "is_first_symbol": false, + "is_all_symbols_part": false + }, + { + "name": "Foo", + "href": "../././~/Foo.html", + "is_symbol": true, + "is_first_symbol": true, + "is_all_symbols_part": false + }, + { + "name": "prototype", + "href": "../././~/Foo.prototype.html", + "is_symbol": true, + "is_first_symbol": false, + "is_all_symbols_part": false + }, + { + "name": "\"><img src=x onerror=alert(1)>", + "href": "../././~/Foo.prototype.\"><img src=x onerror=alert(1)>.html", + "is_symbol": true, + "is_first_symbol": false, + "is_all_symbols_part": false + } + ] + } + }, { "html_head_ctx": { "title": "Foo.prototype.foo - documentation",
tests/testdata/symbol_group-syntect.json+170 −0 modified@@ -230,6 +230,18 @@ "content": { "kind": "doc_entry", "content": [ + { + "id": "property_"><img src=x onerror=alert(1)>", + "name": ""><img src=x onerror=alert(1)>", + "name_href": "../././~/Foo.prototype.\"><img src=x onerror=alert(1)>.html", + "content": "<span>: <span>number</span></span>", + "anchor": { + "id": "property_"><img src=x onerror=alert(1)>" + }, + "tags": [], + "js_doc": null, + "source_href": null + }, { "id": "property_foo", "name": "foo", @@ -585,6 +597,164 @@ ] } }, + { + "html_head_ctx": { + "title": "Foo.prototype.\"><img src=x onerror=alert(1)> - documentation", + "current_file": ".", + "stylesheet_url": "../styles.css", + "page_stylesheet_url": "../page.css", + "url_search_index": "../search_index.js", + "script_js": "../script.js", + "fuse_js": "../fuse.js", + "url_search": "../search.js" + }, + "sidepanel_ctx": { + "package_name": null, + "partitions": [ + { + "name": "Class", + "symbols": [ + { + "kind": [ + { + "kind": "Class", + "char": "c", + "title": "Class", + "title_lowercase": "class", + "title_plural": "Classes" + } + ], + "name": "Bar", + "href": "../././~/Bar.html", + "active": false, + "deprecated": false + }, + { + "kind": [ + { + "kind": "Class", + "char": "c", + "title": "Class", + "title_lowercase": "class", + "title_plural": "Classes" + } + ], + "name": "Foo", + "href": "../././~/Foo.html", + "active": false, + "deprecated": false + }, + { + "kind": [ + { + "kind": "Class", + "char": "c", + "title": "Class", + "title_lowercase": "class", + "title_plural": "Classes" + } + ], + "name": "Foobar", + "href": "../././~/Foobar.html", + "active": false, + "deprecated": false + } + ] + } + ] + }, + "symbol_group_ctx": { + "name": "Foo.prototype.\"><img src=x onerror=alert(1)>", + "symbols": [ + { + "kind": { + "kind": "Property", + "char": " ", + "title": "Property", + "title_lowercase": "property", + "title_plural": "Properties" + }, + "tags": [], + "subtitle": null, + "content": [ + { + "kind": "other", + "value": { + "id": "", + "docs": null, + "sections": [ + { + "title": "Type", + "content": { + "kind": "doc_entry", + "content": [ + { + "id": "variable_Foo_prototype_"><img src=x onerror=alert(1)>", + "name": "", + "name_href": null, + "content": "<span>number</span>", + "anchor": { + "id": "variable_Foo_prototype_"><img src=x onerror=alert(1)>" + }, + "tags": [], + "js_doc": null, + "source_href": null + } + ] + } + } + ] + } + } + ], + "deprecated": null, + "source_href": null + } + ], + "usages": { + "show_tabs": false, + "usages": [ + { + "name": "", + "content": "<div class=\"markdown flex-1\"><pre class=\"highlight\"><code><span style=\"font-weight:bold;color:#a71d5d;\">import </span><span style=\"color:#323232;\">{ Foo } </span><span style=\"font-weight:bold;color:#a71d5d;\">from </span><span style=\"color:#183691;\">\".\"</span><span style=\"color:#323232;\">;\n</span><span style=\"font-weight:bold;color:#a71d5d;\">const </span><span style=\"color:#323232;\">{ &</span><span style=\"color:#0086b3;\">quot</span><span style=\"color:#323232;\">;&</span><span style=\"color:#0086b3;\">gt</span><span style=\"color:#323232;\">;&</span><span style=\"color:#0086b3;\">lt</span><span style=\"color:#323232;\">;</span><span style=\"color:#0086b3;\">img src</span><span style=\"font-weight:bold;color:#a71d5d;\">=</span><span style=\"color:#323232;\">x onerror</span><span style=\"font-weight:bold;color:#a71d5d;\">=</span><span style=\"font-weight:bold;color:#795da3;\">alert</span><span style=\"color:#323232;\">(</span><span style=\"color:#0086b3;\">1</span><span style=\"color:#323232;\">)</span><span style=\"font-weight:bold;color:#a71d5d;\">&</span><span style=\"color:#323232;\">gt; } </span><span style=\"font-weight:bold;color:#a71d5d;\">= </span><span style=\"color:#0086b3;\">Foo</span><span style=\"color:#323232;\">.prototype;\n</span></code><button class=\"context_button\" data-copy=\"import { Foo } from ".";\nconst { &quot;&gt;&lt;img src=x onerror=alert(1)&gt; } = Foo.prototype;\n\"><svg width=\"15\" height=\"15\" viewBox=\"0 0 15 15\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <path d=\"M1.55566 2.7C1.55566 2.03726 2.09292 1.5 2.75566 1.5H8.75566C9.41841 1.5 9.95566 2.03726 9.95566 2.7V5.1H12.3557C13.0184 5.1 13.5557 5.63726 13.5557 6.3V12.3C13.5557 12.9627 13.0184 13.5 12.3557 13.5H6.35566C5.69292 13.5 5.15566 12.9627 5.15566 12.3V9.9H2.75566C2.09292 9.9 1.55566 9.36274 1.55566 8.7V2.7ZM6.35566 9.9V12.3H12.3557V6.3H9.95566V8.7C9.95566 9.36274 9.41841 9.9 8.75566 9.9H6.35566ZM8.75566 8.7V2.7L2.75566 2.7V8.7H8.75566Z\" fill=\"#232323\"></path>\n</svg>\n</button><code></code></pre>\n</div>", + "additional_css": "" + } + ] + } + }, + "breadcrumbs_ctx": { + "parts": [ + { + "name": "index", + "href": "../", + "is_symbol": false, + "is_first_symbol": false, + "is_all_symbols_part": false + }, + { + "name": "Foo", + "href": "../././~/Foo.html", + "is_symbol": true, + "is_first_symbol": true, + "is_all_symbols_part": false + }, + { + "name": "prototype", + "href": "../././~/Foo.prototype.html", + "is_symbol": true, + "is_first_symbol": false, + "is_all_symbols_part": false + }, + { + "name": "\"><img src=x onerror=alert(1)>", + "href": "../././~/Foo.prototype.\"><img src=x onerror=alert(1)>.html", + "is_symbol": true, + "is_first_symbol": false, + "is_all_symbols_part": false + } + ] + } + }, { "html_head_ctx": { "title": "Foo.prototype.foo - documentation",
tests/testdata/symbol_group-tree-sitter.json+170 −0 modified@@ -230,6 +230,18 @@ "content": { "kind": "doc_entry", "content": [ + { + "id": "property_"><img src=x onerror=alert(1)>", + "name": ""><img src=x onerror=alert(1)>", + "name_href": "../././~/Foo.prototype.\"><img src=x onerror=alert(1)>.html", + "content": "<span>: <span>number</span></span>", + "anchor": { + "id": "property_"><img src=x onerror=alert(1)>" + }, + "tags": [], + "js_doc": null, + "source_href": null + }, { "id": "property_foo", "name": "foo", @@ -585,6 +597,164 @@ ] } }, + { + "html_head_ctx": { + "title": "Foo.prototype.\"><img src=x onerror=alert(1)> - documentation", + "current_file": ".", + "stylesheet_url": "../styles.css", + "page_stylesheet_url": "../page.css", + "url_search_index": "../search_index.js", + "script_js": "../script.js", + "fuse_js": "../fuse.js", + "url_search": "../search.js" + }, + "sidepanel_ctx": { + "package_name": null, + "partitions": [ + { + "name": "Class", + "symbols": [ + { + "kind": [ + { + "kind": "Class", + "char": "c", + "title": "Class", + "title_lowercase": "class", + "title_plural": "Classes" + } + ], + "name": "Bar", + "href": "../././~/Bar.html", + "active": false, + "deprecated": false + }, + { + "kind": [ + { + "kind": "Class", + "char": "c", + "title": "Class", + "title_lowercase": "class", + "title_plural": "Classes" + } + ], + "name": "Foo", + "href": "../././~/Foo.html", + "active": false, + "deprecated": false + }, + { + "kind": [ + { + "kind": "Class", + "char": "c", + "title": "Class", + "title_lowercase": "class", + "title_plural": "Classes" + } + ], + "name": "Foobar", + "href": "../././~/Foobar.html", + "active": false, + "deprecated": false + } + ] + } + ] + }, + "symbol_group_ctx": { + "name": "Foo.prototype.\"><img src=x onerror=alert(1)>", + "symbols": [ + { + "kind": { + "kind": "Property", + "char": " ", + "title": "Property", + "title_lowercase": "property", + "title_plural": "Properties" + }, + "tags": [], + "subtitle": null, + "content": [ + { + "kind": "other", + "value": { + "id": "", + "docs": null, + "sections": [ + { + "title": "Type", + "content": { + "kind": "doc_entry", + "content": [ + { + "id": "variable_Foo_prototype_"><img src=x onerror=alert(1)>", + "name": "", + "name_href": null, + "content": "<span>number</span>", + "anchor": { + "id": "variable_Foo_prototype_"><img src=x onerror=alert(1)>" + }, + "tags": [], + "js_doc": null, + "source_href": null + } + ] + } + } + ] + } + } + ], + "deprecated": null, + "source_href": null + } + ], + "usages": { + "show_tabs": false, + "usages": [ + { + "name": "", + "content": "<div class=\"markdown flex-1\"><pre class=\"highlight\"><code><span class=\"pl-k\">import</span> { Foo } <span class=\"pl-k\">from</span> <span class=\"pl-s\">\".\"</span>;\n<span class=\"pl-k\">const</span> { <span class=\"pl-c1\">&</span>quot;<span class=\"pl-c1\">&</span>gt;<span class=\"pl-c1\">&</span>lt;img src<span class=\"pl-c1\">=</span>x onerror<span class=\"pl-c1\">=</span>alert(<span class=\"pl-c1\">1</span>)<span class=\"pl-c1\">&</span>gt; } <span class=\"pl-c1\">=</span> Foo.<span class=\"pl-c1\">prototype</span>;\n</code><button class=\"context_button\" data-copy=\"import { Foo } from ".";\nconst { &quot;&gt;&lt;img src=x onerror=alert(1)&gt; } = Foo.prototype;\n\"><svg width=\"15\" height=\"15\" viewBox=\"0 0 15 15\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <path d=\"M1.55566 2.7C1.55566 2.03726 2.09292 1.5 2.75566 1.5H8.75566C9.41841 1.5 9.95566 2.03726 9.95566 2.7V5.1H12.3557C13.0184 5.1 13.5557 5.63726 13.5557 6.3V12.3C13.5557 12.9627 13.0184 13.5 12.3557 13.5H6.35566C5.69292 13.5 5.15566 12.9627 5.15566 12.3V9.9H2.75566C2.09292 9.9 1.55566 9.36274 1.55566 8.7V2.7ZM6.35566 9.9V12.3H12.3557V6.3H9.95566V8.7C9.95566 9.36274 9.41841 9.9 8.75566 9.9H6.35566ZM8.75566 8.7V2.7L2.75566 2.7V8.7H8.75566Z\" fill=\"#232323\"></path>\n</svg>\n</button><code></code></pre>\n</div>", + "additional_css": "" + } + ] + } + }, + "breadcrumbs_ctx": { + "parts": [ + { + "name": "index", + "href": "../", + "is_symbol": false, + "is_first_symbol": false, + "is_all_symbols_part": false + }, + { + "name": "Foo", + "href": "../././~/Foo.html", + "is_symbol": true, + "is_first_symbol": true, + "is_all_symbols_part": false + }, + { + "name": "prototype", + "href": "../././~/Foo.prototype.html", + "is_symbol": true, + "is_first_symbol": false, + "is_all_symbols_part": false + }, + { + "name": "\"><img src=x onerror=alert(1)>", + "href": "../././~/Foo.prototype.\"><img src=x onerror=alert(1)>.html", + "is_symbol": true, + "is_first_symbol": false, + "is_all_symbols_part": false + } + ] + } + }, { "html_head_ctx": { "title": "Foo.prototype.foo - documentation",
tests/testdata/symbol_search.json+4 −4 modified@@ -23,9 +23,9 @@ "file": ".", "location": { "filename": "", - "line": 30, + "line": 31, "col": 0, - "byteIndex": 551 + "byteIndex": 591 }, "declarationKind": "export", "deprecated": false @@ -38,9 +38,9 @@ "file": ".", "location": { "filename": "", - "line": 33, + "line": 34, "col": 0, - "byteIndex": 585 + "byteIndex": 625 }, "declarationKind": "export", "deprecated": false
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-qqwr-j9mm-fhw6ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-32468ghsaADVISORY
- github.com/denoland/deno/security/advisories/GHSA-qqwr-j9mm-fhw6nvdWEB
- github.com/denoland/deno_doc/blob/dc556c848831d7ae48f3eff2ababc6e75eb6b73e/src/html/templates/pages/search.jsnvdWEB
- github.com/denoland/deno_doc/commit/0f1ef3efbf16194730a29d93dcb9c02f6c490942ghsaWEB
News mentions
0No linked articles in our index yet.