]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
perf(runtime-vapor): use `setAttr` or `setDOMProp` instead of `setDynamicProp` when...
authoredison <daiwei521@126.com>
Wed, 27 Nov 2024 08:55:45 +0000 (16:55 +0800)
committerGitHub <noreply@github.com>
Wed, 27 Nov 2024 08:55:45 +0000 (16:55 +0800)
Co-authored-by: Doctor Wu <doctorwu@moego.pet>
packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/vBind.spec.ts
packages/compiler-vapor/src/generators/prop.ts
packages/compiler-vapor/src/ir/index.ts
packages/compiler-vapor/src/transforms/transformElement.ts
packages/runtime-vapor/src/dom/prop.ts
packages/shared/src/domAttrConfig.ts

index efb33e64d754c296f9a4ef2f2d599d8a4d4f022a..bc827dd732fb2ba890f53aa2e0a8d8881285ddba 100644 (file)
@@ -151,7 +151,7 @@ export function render(_ctx, $props, $emit, $attrs, $slots) {
 `;
 
 exports[`compile > directives > v-pre > should not affect siblings after it 1`] = `
-"import { resolveComponent as _resolveComponent, createComponent as _createComponent, createTextNode as _createTextNode, insert as _insert, renderEffect as _renderEffect, setDynamicProp as _setDynamicProp, template as _template } from 'vue/vapor';
+"import { resolveComponent as _resolveComponent, createComponent as _createComponent, createTextNode as _createTextNode, insert as _insert, renderEffect as _renderEffect, setDOMProp as _setDOMProp, template as _template } from 'vue/vapor';
 const t0 = _template("<div :id=\\"foo\\"><Comp></Comp>{{ bar }}</div>")
 const t1 = _template("<div></div>")
 
@@ -162,7 +162,7 @@ export function render(_ctx, $props, $emit, $attrs, $slots) {
   const n1 = _createComponent(_component_Comp)
   const n2 = _createTextNode(() => [_ctx.bar])
   _insert([n1, n2], n3)
-  _renderEffect(() => _setDynamicProp(n3, "id", _ctx.foo))
+  _renderEffect(() => _setDOMProp(n3, "id", _ctx.foo))
   return [n0, n3]
 }"
 `;
@@ -177,7 +177,7 @@ export function render(_ctx) {
 `;
 
 exports[`compile > dynamic root nodes and interpolation 1`] = `
-"import { delegate as _delegate, setInheritAttrs as _setInheritAttrs, renderEffect as _renderEffect, setText as _setText, setDynamicProp as _setDynamicProp, delegateEvents as _delegateEvents, template as _template } from 'vue/vapor';
+"import { delegate as _delegate, setInheritAttrs as _setInheritAttrs, renderEffect as _renderEffect, setText as _setText, setDOMProp as _setDOMProp, delegateEvents as _delegateEvents, template as _template } from 'vue/vapor';
 const t0 = _template("<button></button>")
 _delegateEvents("click")
 
@@ -186,7 +186,7 @@ export function render(_ctx) {
   _delegate(n0, "click", () => _ctx.handleClick)
   _setInheritAttrs(["id"])
   _renderEffect(() => _setText(n0, _ctx.count, "foo", _ctx.count, "foo", _ctx.count))
-  _renderEffect(() => _setDynamicProp(n0, "id", _ctx.count, true))
+  _renderEffect(() => _setDOMProp(n0, "id", _ctx.count, true))
   return n0
 }"
 `;
index 16e60bd846e092d6247cbea27f41953139b3a879..822a6af66c24874bab838172de6a7543db6c0eac 100644 (file)
@@ -121,14 +121,100 @@ export function render(_ctx) {
 }"
 `;
 
+exports[`compiler v-bind > HTML global attributes should set as dom prop 1`] = `
+"import { setInheritAttrs as _setInheritAttrs, renderEffect as _renderEffect, setDOMProp as _setDOMProp, template as _template } from 'vue/vapor';
+const t0 = _template("<div></div>")
+
+export function render(_ctx) {
+  const n0 = t0()
+  _setInheritAttrs(["id", "title", "lang", "dir", "tabindex"])
+  _renderEffect(() => _setDOMProp(n0, "id", _ctx.id, true))
+  _renderEffect(() => _setDOMProp(n0, "title", _ctx.title, true))
+  _renderEffect(() => _setDOMProp(n0, "lang", _ctx.lang, true))
+  _renderEffect(() => _setDOMProp(n0, "dir", _ctx.dir, true))
+  _renderEffect(() => _setDOMProp(n0, "tabindex", _ctx.tabindex, true))
+  return n0
+}"
+`;
+
+exports[`compiler v-bind > MathML global attributes should set as dom prop 1`] = `
+"import { setInheritAttrs as _setInheritAttrs, renderEffect as _renderEffect, setDOMProp as _setDOMProp, template as _template } from 'vue/vapor';
+const t0 = _template("<math></math>")
+
+export function render(_ctx) {
+  const n0 = t0()
+  _setInheritAttrs(["autofucus", "dir", "displaystyle", "mathcolor", "tabindex"])
+  _renderEffect(() => _setDOMProp(n0, "autofucus", _ctx.autofucus, true))
+  _renderEffect(() => _setDOMProp(n0, "dir", _ctx.dir, true))
+  _renderEffect(() => _setDOMProp(n0, "displaystyle", _ctx.displaystyle, true))
+  _renderEffect(() => _setDOMProp(n0, "mathcolor", _ctx.mathcolor, true))
+  _renderEffect(() => _setDOMProp(n0, "tabindex", _ctx.tabindex, true))
+  return n0
+}"
+`;
+
+exports[`compiler v-bind > SVG global attributes should set as dom prop 1`] = `
+"import { setInheritAttrs as _setInheritAttrs, renderEffect as _renderEffect, setDOMProp as _setDOMProp, template as _template } from 'vue/vapor';
+const t0 = _template("<svg></svg>")
+
+export function render(_ctx) {
+  const n0 = t0()
+  _setInheritAttrs(["id", "lang", "tabindex"])
+  _renderEffect(() => _setDOMProp(n0, "id", _ctx.id, true))
+  _renderEffect(() => _setDOMProp(n0, "lang", _ctx.lang, true))
+  _renderEffect(() => _setDOMProp(n0, "tabindex", _ctx.tabindex, true))
+  return n0
+}"
+`;
+
+exports[`compiler v-bind > attributes must be set as attribute 1`] = `
+"import { renderEffect as _renderEffect, setAttr as _setAttr, template as _template } from 'vue/vapor';
+const t0 = _template("<div></div>")
+const t1 = _template("<input>")
+const t2 = _template("<textarea></textarea>")
+const t3 = _template("<img>")
+const t4 = _template("<video></video>")
+const t5 = _template("<canvas></canvas>")
+const t6 = _template("<source>")
+
+export function render(_ctx) {
+  const n0 = t0()
+  const n1 = t1()
+  const n2 = t2()
+  const n3 = t3()
+  const n4 = t4()
+  const n5 = t5()
+  const n6 = t6()
+  _renderEffect(() => _setAttr(n0, "spellcheck", _ctx.spellcheck))
+  _renderEffect(() => _setAttr(n0, "draggable", _ctx.draggable))
+  _renderEffect(() => _setAttr(n0, "translate", _ctx.translate))
+  _renderEffect(() => _setAttr(n0, "form", _ctx.form))
+  _renderEffect(() => _setAttr(n1, "list", _ctx.list))
+  _renderEffect(() => _setAttr(n2, "type", _ctx.type))
+  _renderEffect(() => {
+    _setAttr(n3, "width", _ctx.width)
+    _setAttr(n4, "width", _ctx.width)
+    _setAttr(n5, "width", _ctx.width)
+    _setAttr(n6, "width", _ctx.width)
+  })
+  _renderEffect(() => {
+    _setAttr(n3, "height", _ctx.height)
+    _setAttr(n4, "height", _ctx.height)
+    _setAttr(n5, "height", _ctx.height)
+    _setAttr(n6, "height", _ctx.height)
+  })
+  return [n0, n1, n2, n3, n4, n5, n6]
+}"
+`;
+
 exports[`compiler v-bind > basic 1`] = `
-"import { setInheritAttrs as _setInheritAttrs, renderEffect as _renderEffect, setDynamicProp as _setDynamicProp, template as _template } from 'vue/vapor';
+"import { setInheritAttrs as _setInheritAttrs, renderEffect as _renderEffect, setDOMProp as _setDOMProp, template as _template } from 'vue/vapor';
 const t0 = _template("<div></div>")
 
 export function render(_ctx) {
   const n0 = t0()
   _setInheritAttrs(["id"])
-  _renderEffect(() => _setDynamicProp(n0, "id", _ctx.id, true))
+  _renderEffect(() => _setDOMProp(n0, "id", _ctx.id, true))
   return n0
 }"
 `;
@@ -170,13 +256,13 @@ export function render(_ctx) {
 `;
 
 exports[`compiler v-bind > no expression 1`] = `
-"import { setInheritAttrs as _setInheritAttrs, renderEffect as _renderEffect, setDynamicProp as _setDynamicProp, template as _template } from 'vue/vapor';
+"import { setInheritAttrs as _setInheritAttrs, renderEffect as _renderEffect, setDOMProp as _setDOMProp, template as _template } from 'vue/vapor';
 const t0 = _template("<div></div>")
 
 export function render(_ctx) {
   const n0 = t0()
   _setInheritAttrs(["id"])
-  _renderEffect(() => _setDynamicProp(n0, "id", _ctx.id, true))
+  _renderEffect(() => _setDOMProp(n0, "id", _ctx.id, true))
   return n0
 }"
 `;
index 213879feabd33f249a3310cf31cb5f42f831e558..2665b615750603b56a3c5a3185d13c4eabd31f5e 100644 (file)
@@ -1,12 +1,12 @@
 // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
 
 exports[`compiler: v-once > as root node 1`] = `
-"import { setDynamicProp as _setDynamicProp, setInheritAttrs as _setInheritAttrs, template as _template } from 'vue/vapor';
+"import { setDOMProp as _setDOMProp, setInheritAttrs as _setInheritAttrs, template as _template } from 'vue/vapor';
 const t0 = _template("<div></div>")
 
 export function render(_ctx) {
   const n0 = t0()
-  _setDynamicProp(n0, "id", _ctx.foo, true)
+  _setDOMProp(n0, "id", _ctx.foo, true)
   _setInheritAttrs(["id"])
   return n0
 }"
@@ -52,13 +52,13 @@ export function render(_ctx) {
 `;
 
 exports[`compiler: v-once > on nested plain element 1`] = `
-"import { setDynamicProp as _setDynamicProp, template as _template } from 'vue/vapor';
+"import { setDOMProp as _setDOMProp, template as _template } from 'vue/vapor';
 const t0 = _template("<div><div></div></div>")
 
 export function render(_ctx) {
   const n1 = t0()
   const n0 = n1.firstChild
-  _setDynamicProp(n0, "id", _ctx.foo)
+  _setDOMProp(n0, "id", _ctx.foo)
   return n1
 }"
 `;
index a419f55ddef59d812dcf116f4aca32f02d149311..7f14bb25d02590c92b4e089a67674e1be24fff7f 100644 (file)
@@ -74,7 +74,7 @@ describe('compiler v-bind', () => {
     })
 
     expect(code).matchSnapshot()
-    expect(code).contains('_setDynamicProp(n0, "id", _ctx.id, true)')
+    expect(code).contains('_setDOMProp(n0, "id", _ctx.id, true)')
   })
 
   test('no expression', () => {
@@ -104,7 +104,7 @@ describe('compiler v-bind', () => {
         ],
       },
     })
-    expect(code).contains('_setDynamicProp(n0, "id", _ctx.id, true)')
+    expect(code).contains('_setDOMProp(n0, "id", _ctx.id, true)')
   })
 
   test('no expression (shorthand)', () => {
@@ -527,6 +527,73 @@ describe('compiler v-bind', () => {
     expect(code).contains('_setAttr(n0, "foo-bar", _ctx.fooBar, true)')
   })
 
+  test('attributes must be set as attribute', () => {
+    const { code } = compileWithVBind(`
+      <div :spellcheck :draggable :translate :form />
+      <input :list="list" />
+      <textarea :type="type" />
+      <img :width="width" :height="height"/>
+      <video :width="width" :height="height"/>
+      <canvas :width="width" :height="height"/>
+      <source :width="width" :height="height"/>
+    `)
+
+    expect(code).matchSnapshot()
+    expect(code).contains('_setAttr(n0, "spellcheck", _ctx.spellcheck)')
+    expect(code).contains('_setAttr(n0, "draggable", _ctx.draggable)')
+    expect(code).contains('_setAttr(n0, "translate", _ctx.translate)')
+    expect(code).contains('_setAttr(n0, "form", _ctx.form)')
+    expect(code).contains('_setAttr(n1, "list", _ctx.list)')
+    expect(code).contains('_setAttr(n2, "type", _ctx.type)')
+    expect(code).contains('_setAttr(n3, "width", _ctx.width)')
+    expect(code).contains('_setAttr(n3, "height", _ctx.height)')
+    expect(code).contains('_setAttr(n4, "width", _ctx.width)')
+    expect(code).contains('_setAttr(n4, "height", _ctx.height)')
+    expect(code).contains('_setAttr(n5, "width", _ctx.width)')
+    expect(code).contains('_setAttr(n5, "height", _ctx.height)')
+    expect(code).contains('_setAttr(n6, "width", _ctx.width)')
+    expect(code).contains('_setAttr(n6, "height", _ctx.height)')
+  })
+
+  test('HTML global attributes should set as dom prop', () => {
+    const { code } = compileWithVBind(`
+      <div :id="id" :title="title" :lang="lang" :dir="dir" :tabindex="tabindex" />
+    `)
+
+    expect(code).matchSnapshot()
+    expect(code).contains('_setDOMProp(n0, "id", _ctx.id, true)')
+    expect(code).contains('_setDOMProp(n0, "title", _ctx.title, true)')
+    expect(code).contains('_setDOMProp(n0, "lang", _ctx.lang, true)')
+    expect(code).contains('_setDOMProp(n0, "dir", _ctx.dir, true)')
+    expect(code).contains('_setDOMProp(n0, "tabindex", _ctx.tabindex, true)')
+  })
+
+  test('SVG global attributes should set as dom prop', () => {
+    const { code } = compileWithVBind(`
+      <svg :id="id" :lang="lang" :tabindex="tabindex" />
+    `)
+
+    expect(code).matchSnapshot()
+    expect(code).contains('_setDOMProp(n0, "id", _ctx.id, true)')
+    expect(code).contains('_setDOMProp(n0, "lang", _ctx.lang, true)')
+    expect(code).contains('_setDOMProp(n0, "tabindex", _ctx.tabindex, true)')
+  })
+
+  test('MathML global attributes should set as dom prop', () => {
+    const { code } = compileWithVBind(`
+      <math :autofucus :dir :displaystyle :mathcolor :tabindex/>
+    `)
+
+    expect(code).matchSnapshot()
+    expect(code).contains('_setDOMProp(n0, "autofucus", _ctx.autofucus, true)')
+    expect(code).contains('_setDOMProp(n0, "dir", _ctx.dir, true)')
+    expect(code).contains(
+      '_setDOMProp(n0, "displaystyle", _ctx.displaystyle, true)',
+    )
+    expect(code).contains('_setDOMProp(n0, "mathcolor", _ctx.mathcolor, true)')
+    expect(code).contains('_setDOMProp(n0, "tabindex", _ctx.tabindex, true)')
+  })
+
   test('number value', () => {
     const { code } = compileWithVBind(`<Comp :depth="0" />`)
     expect(code).matchSnapshot()
index 120aa3733d988264f7f7a79028f29872dd048d18..295b177d6ee4cca99ebcffc4f7a74473ff20bd42 100644 (file)
@@ -21,7 +21,17 @@ import {
   genCall,
   genMulti,
 } from './utils'
-import { toHandlerKey } from '@vue/shared'
+import {
+  attributeCache,
+  isHTMLGlobalAttr,
+  isHTMLTag,
+  isMathMLGlobalAttr,
+  isMathMLTag,
+  isSVGTag,
+  isSvgGlobalAttr,
+  shouldSetAsAttr,
+  toHandlerKey,
+} from '@vue/shared'
 
 // only the static key prop will reach here
 export function genSetProp(
@@ -31,9 +41,12 @@ export function genSetProp(
   const { vaporHelper } = context
   const {
     prop: { key, values, modifier },
+    tag,
   } = oper
 
   const keyName = key.content
+  const tagName = tag.toUpperCase()
+  const attrCacheKey = `${tagName}_${keyName}`
 
   let helperName: VaporHelper
   let omitKey = false
@@ -45,6 +58,21 @@ export function genSetProp(
     omitKey = true
   } else if (modifier) {
     helperName = modifier === '.' ? 'setDOMProp' : 'setAttr'
+  } else if (
+    attributeCache[attrCacheKey] === undefined
+      ? (attributeCache[attrCacheKey] = shouldSetAsAttr(
+          tag.toUpperCase(),
+          keyName,
+        ))
+      : attributeCache[attrCacheKey]
+  ) {
+    helperName = 'setAttr'
+  } else if (
+    (isHTMLTag(tag) && isHTMLGlobalAttr(keyName)) ||
+    (isSVGTag(tag) && isSvgGlobalAttr(keyName)) ||
+    (isMathMLTag(tag) && isMathMLGlobalAttr(keyName))
+  ) {
+    helperName = 'setDOMProp'
   } else {
     helperName = 'setDynamicProp'
   }
index 9cca9bf3ca1385ebbb481a4ee11b79a6b54eca65..7d1ddac894514a2ae7ad75ebff0bc4cb5f33de19 100644 (file)
@@ -94,6 +94,7 @@ export interface SetPropIRNode extends BaseIRNode {
   element: number
   prop: IRProp
   root: boolean
+  tag: string
 }
 
 export interface SetDynamicPropsIRNode extends BaseIRNode {
index 3356a1d772ae06a961da1557a50edbf4faeec5d0..51f0e427ee8d91d000f967ff43fca74d96d52747 100644 (file)
@@ -210,6 +210,7 @@ function transformNativeElement(
           element: context.reference(),
           prop,
           root: singleRoot,
+          tag,
         })
       }
     }
index 48d95173ec61816c7a87328da15d0eda5fd24972..8dfe24875872b2c3e8edddafeea1cc9cec85c4bd 100644 (file)
@@ -1,4 +1,5 @@
 import {
+  attributeCache,
   includeBooleanAttr,
   isArray,
   isFunction,
@@ -7,6 +8,7 @@ import {
   isString,
   normalizeClass,
   normalizeStyle,
+  shouldSetAsAttr,
   toDisplayString,
 } from '@vue/shared'
 import { warn } from '../warning'
@@ -242,45 +244,15 @@ function shouldSetAsProp(
     return false
   }
 
-  // these are enumerated attrs, however their corresponding DOM properties
-  // are actually booleans - this leads to setting it with a string "false"
-  // value leading it to be coerced to `true`, so we need to always treat
-  // them as attributes.
-  // Note that `contentEditable` doesn't have this problem: its DOM
-  // property is also enumerated string values.
-  if (key === 'spellcheck' || key === 'draggable' || key === 'translate') {
-    return false
-  }
-
-  // #1787, #2840 form property on form elements is readonly and must be set as
-  // attribute.
-  if (key === 'form') {
-    return false
-  }
-
-  // #1526 <input list> must be set as attribute
-  if (key === 'list' && el.tagName === 'INPUT') {
-    return false
-  }
-
-  // #2766 <textarea type> must be set as attribute
-  if (key === 'type' && el.tagName === 'TEXTAREA') {
+  const attrCacheKey = `${el.tagName}_${key}`
+  if (
+    attributeCache[attrCacheKey] === undefined
+      ? (attributeCache[attrCacheKey] = shouldSetAsAttr(el.tagName, key))
+      : attributeCache[attrCacheKey]
+  ) {
     return false
   }
 
-  // #8780 the width or height of embedded tags must be set as attribute
-  if (key === 'width' || key === 'height') {
-    const tag = el.tagName
-    if (
-      tag === 'IMG' ||
-      tag === 'VIDEO' ||
-      tag === 'CANVAS' ||
-      tag === 'SOURCE'
-    ) {
-      return false
-    }
-  }
-
   // native onclick with string value, must be set as attribute
   if (isNativeOn(key) && isString(value)) {
     return false
index b5f0166327fb3133e765fd47ff40a475ab3824de..b13df275769fcdf0ed5cf29298d86380fb56f5ce 100644 (file)
@@ -78,6 +78,16 @@ export const isKnownHtmlAttr: (key: string) => boolean = /*@__PURE__*/ makeMap(
     `value,width,wrap`,
 )
 
+/**
+ * Generated from https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes
+ */
+export const isHTMLGlobalAttr: (key: string) => boolean = /*@__PURE__*/ makeMap(
+  `accesskey,anchor,autocapitalize,autocorrect,autofocus,class,contenteditable,` +
+    `dir,draggable,enterkeyhint,exportparts,hidden,id,inert,inputmode,is,` +
+    `itemid,itemprop,itemref,itemscope,itemtype,lang,nonce,part,popover,role,slot,` +
+    `spellcheck,style,tabindex,title,translate,virtualkeyboardpolicy,writingsuggestions`,
+)
+
 /**
  * Generated from https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute
  */
@@ -123,6 +133,14 @@ export const isKnownSvgAttr: (key: string) => boolean = /*@__PURE__*/ makeMap(
     `xml:space,y,y1,y2,yChannelSelector,z,zoomAndPan`,
 )
 
+/**
+ * Generated from https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute#generic_attributes
+ */
+export const isSvgGlobalAttr: (key: string) => boolean = /*@__PURE__*/ makeMap(
+  `id,class,style,lang,tabindex,xml:base,xml:lang,xml:space,requiredExtensions,` +
+    `requiredFeatures,systemLanguage`,
+)
+
 /**
  * Generated from https://developer.mozilla.org/en-US/docs/Web/MathML/Attribute
  */
@@ -142,6 +160,15 @@ export const isKnownMathMLAttr: (key: string) => boolean =
       `voffset,width,widths,xlink:href,xlink:show,xlink:type,xmlns`,
   )
 
+/**
+ * Generated from https://developer.mozilla.org/en-US/docs/Web/MathML/Global_Attributes
+ */
+export const isMathMLGlobalAttr: (key: string) => boolean =
+  /*@__PURE__*/ makeMap(
+    `autofucus,class,dir,displaystyle,id,mathbackground,mathcolor,mathsize,nonce,scriptlevel,` +
+      `style,tabindex`,
+  )
+
 /**
  * Shared between server-renderer and runtime-core hydration logic
  */
@@ -152,3 +179,54 @@ export function isRenderableAttrValue(value: unknown): boolean {
   const type = typeof value
   return type === 'string' || type === 'number' || type === 'boolean'
 }
+
+/**
+ * cache seen attributes which must be set as attribute
+ */
+export const attributeCache: Record<string, boolean> = Object.create(null)
+
+/*
+ * The following attributes must be set as attribute
+ */
+export function shouldSetAsAttr(tagName: string, key: string): boolean {
+  // these are enumerated attrs, however their corresponding DOM properties
+  // are actually booleans - this leads to setting it with a string "false"
+  // value leading it to be coerced to `true`, so we need to always treat
+  // them as attributes.
+  // Note that `contentEditable` doesn't have this problem: its DOM
+  // property is also enumerated string values.
+  if (key === 'spellcheck' || key === 'draggable' || key === 'translate') {
+    return true
+  }
+
+  // #1787, #2840 form property on form elements is readonly and must be set as
+  // attribute.
+  if (key === 'form') {
+    return true
+  }
+
+  // #1526 <input list> must be set as attribute
+  if (key === 'list' && tagName === 'INPUT') {
+    return true
+  }
+
+  // #2766 <textarea type> must be set as attribute
+  if (key === 'type' && tagName === 'TEXTAREA') {
+    return true
+  }
+
+  // #8780 the width or height of embedded tags must be set as attribute
+  if (key === 'width' || key === 'height') {
+    const tag = tagName
+    if (
+      tag === 'IMG' ||
+      tag === 'VIDEO' ||
+      tag === 'CANVAS' ||
+      tag === 'SOURCE'
+    ) {
+      return true
+    }
+  }
+
+  return false
+}