├── .npmrc ├── .gitattributes ├── CONTRIBUTING.md ├── .github └── workflows │ ├── build.yml │ └── deploy.yml ├── .gitignore ├── LICENSE ├── package.json ├── test └── sanity_check_test262_test.js ├── test262 └── test │ └── built-ins │ └── Reflect │ └── is-template-object.js ├── spec.emu └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | index.html -diff merge=ours 2 | spec.js -diff merge=ours 3 | spec.css -diff merge=ours 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Issues and PRs welcome. 2 | 3 | This is a TC39 proposal so is governed by the 4 | [TC39 Code of Conduct](https://github.com/tc39/code-of-conduct). 5 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build spec 2 | 3 | on: [pull_request, push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: ljharb/actions/node/install@main 12 | name: 'nvm install lts/* && npm install' 13 | with: 14 | node-version: lts/* 15 | - run: npm run build 16 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy gh-pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: ljharb/actions/node/install@main 15 | name: 'nvm install lts/* && npm install' 16 | with: 17 | node-version: lts/* 18 | - run: npm run build 19 | - uses: JamesIves/github-pages-deploy-action@v4.3.3 20 | with: 21 | branch: gh-pages 22 | folder: build 23 | clean: true 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # Only apps should have lockfiles 40 | yarn.lock 41 | package-lock.json 42 | npm-shrinkwrap.json 43 | 44 | # Emacs droppings 45 | *~ 46 | 47 | build 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Mike Samuel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "proposal-array-is-template-object", 4 | "version": "0.0.0", 5 | "description": "", 6 | "main": "index.js", 7 | "scripts": { 8 | "start": "npm run build-loose -- --watch", 9 | "build": "npm run build-loose -- --strict", 10 | "build-loose": "node -e 'fs.mkdirSync(\"build\", { recursive: true })' && ecmarkup --load-biblio @tc39/ecma262-biblio --verbose spec.emu build/index.html --lint-spec", 11 | "test": "mocha", 12 | "precommit": "npm run build && npm run test" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/tc39/proposal-array-is-template-object.git" 17 | }, 18 | "author": "Mike Samuel", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/tc39/proposal-array-is-template-object/issues" 22 | }, 23 | "homepage": "https://github.com/tc39/proposal-array-is-template-object#readme", 24 | "devDependencies": { 25 | "@tc39/ecma262-biblio": "^2.1.2789", 26 | "chai": "^5.1.1", 27 | "ecmarkup": "^20.0.0", 28 | "is-template-object": "^1.0.1", 29 | "mocha": "^10.7.3" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/sanity_check_test262_test.js: -------------------------------------------------------------------------------- 1 | // Check that the test262 tests do something sensible when run against 2 | // a known problematic stub implementation of Reflect.isTemplateObject. 3 | 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const vm = require('vm'); 7 | const { expect } = require('chai'); 8 | 9 | describe('README.md', () => { 10 | describe('test262', () => { 11 | beforeEach(() => { 12 | Reflect.isTemplateObject = require('is-template-object').implementation; 13 | }); 14 | afterEach(() => { 15 | delete Reflect.isTemplateObject; 16 | }); 17 | 18 | it('runs, kind of', () => { 19 | const testContent = fs.readFileSync( 20 | path.join( 21 | __dirname, '..', 22 | 'test262', 'test', 'built-ins', 'Reflect', 'is-template-object.js'), 23 | { encoding: 'UTF-8' }); 24 | 25 | // Stub out some of the API provided at 26 | // https://github.com/tc39/test262/blob/master/INTERPRETING.md#host-defined-functions 27 | const test262stubErrorList = []; 28 | function $ERROR(msg) { 29 | test262stubErrorList[test262stubErrorList.length] = msg; 30 | } 31 | 32 | const $262 = { 33 | createRealm() { 34 | return { global: vm.runInNewContext('this') }; 35 | }, 36 | } 37 | 38 | // Evaluate the test content. 39 | new Function( 40 | '$ERROR', '$262', testContent)( 41 | ($ERROR), ($262)); 42 | 43 | expect(test262stubErrorList) 44 | .to.deep.equal([ 45 | '#2: Reflect.isTemplateObject producing spurious negative results:' 46 | + ' proxy,forgery,argument not poked' 47 | ]); 48 | }); 49 | }); 50 | }); 51 | 52 | 53 | -------------------------------------------------------------------------------- /test262/test/built-ins/Reflect/is-template-object.js: -------------------------------------------------------------------------------- 1 | /*--- 2 | info: | 3 | Reflect.isTemplateObject returns true given a 4 | template tag strings object and false otherwise. 5 | es5id: TBD 6 | description: Applies Reflect.isTemplateObject to various inputs. 7 | --*/ 8 | 9 | // A template tag that applies the function under test 10 | // and returns its result. 11 | function directTag(strings) { 12 | return Reflect.isTemplateObject(strings); 13 | } 14 | 15 | // A template tag that does the same but passes its 16 | // argument via normal function application. 17 | function indirectTag(strings) { 18 | return directTag(strings); 19 | } 20 | 21 | // A template object that escapes the tag function body. 22 | var escapedTemplateObject = null; 23 | ((x) => (escapedTemplateObject = x))`foo ${ null } bar`; 24 | 25 | var foreignTemplateObject = null; 26 | (() => { 27 | const realm = $262.createRealm(); 28 | foreignTemplateObject = 29 | (new realm.global.Function('return ((x) => x)`foreign strings`;'))(); 30 | })(); 31 | 32 | // Things that ought be recognized as template objects. 33 | // Elements are [ description, candidate value ] pairs. 34 | var posTestCases = [ 35 | [ 'direct', () => directTag`foo` ], 36 | // It doesn't matter whether the strings were used with the tag that's running. 37 | [ 'indirect', () => indirectTag`bar` ], 38 | // Or whether there is even a tag function on the stack. 39 | [ 'escaped', () => Reflect.isTemplateObject(escapedTemplateObject) ], 40 | [ 41 | 'called with null this', 42 | () => Reflect.apply(Reflect.isTemplateObject, null, [ escapedTemplateObject ]), 43 | ], 44 | // IsTemplateObject is realm-agnostic 45 | [ 46 | 'cross-realm template objects', 47 | () => Reflect.isTemplateObject(foreignTemplateObject), 48 | ], 49 | ]; 50 | 51 | var falsePositives = []; 52 | 53 | for (const [ message, f ] of posTestCases) { 54 | let result = null; 55 | try { 56 | result = f(); 57 | } catch (e) { 58 | falsePositives.push(message + ' threw'); 59 | continue; 60 | } 61 | if (result !== true) { 62 | falsePositives.push(message); 63 | } 64 | } 65 | 66 | // Things that should not be recognized as template objects. 67 | // Elements are [ description, candidate value ] pairs. 68 | var negTestCases = [ 69 | // Common values are not template string objects. 70 | [ 'zero args', () => directTag() ], 71 | [ 'null', () => directTag(null) ], 72 | [ 'undefined', () => directTag(undefined) ], 73 | [ 'zero', () => directTag(0) ], 74 | [ '-zero', () => directTag(-0) ], 75 | [ 'number', () => directTag(123) ], 76 | [ 'NaN', () => directTag(NaN) ], 77 | [ '+Inf', () => directTag(+Infinity) ], 78 | [ '-Inf', () => directTag(-Infinity) ], 79 | [ 'false', () => directTag(false) ], 80 | [ 'true', () => directTag(true) ], 81 | [ '{}', () => directTag({}) ], 82 | [ '[ "x" ]', () => directTag([ "x" ]) ], 83 | [ 'empty string', () => directTag('') ], 84 | [ 'string', () => directTag('foo') ], 85 | [ 'function', () => directTag(directTag) ], 86 | // A proxy over a template string object is not a template string object. 87 | [ 'proxy', () => directTag(new Proxy(escapedTemplateObject, {})) ], 88 | [ 'Array.prototype', () => Reflect.isTemplateObject(Array.prototype) ], 89 | // User code can't distinguish this case which is why this proposal adds value. 90 | [ 91 | 'forgery', 92 | () => { 93 | let arr = [ 'really really real' ]; 94 | Object.defineProperty(arr, 'raw', { value: arr }); 95 | Object.freeze(arr); 96 | return directTag(arr); 97 | } 98 | ], 99 | // The implementation shouldn't muck with its argument. 100 | [ 101 | 'argument not poked', () => { 102 | let poked = false; 103 | // Use a proxy to see if isTemplateObject 104 | // mucks with arg in an observable way. 105 | let arg = new Proxy( 106 | [], 107 | // The proxy handler is itself a proxy which 108 | // flips the poked bit if any proxy trap is 109 | // invoked. 110 | new Proxy( 111 | {}, 112 | { 113 | has(...args) { 114 | poked = true; 115 | return Reflect.has(...args); 116 | }, 117 | get(...args) { 118 | poked = true; 119 | return Reflect.get(...args); 120 | }, 121 | getPropertyDescriptor(...args) { 122 | poked = true; 123 | return Reflect.getPropertyDescriptor(...args); 124 | }, 125 | getPrototypeOf(...args) { 126 | poked = true; 127 | return Reflect.getPrototypeOf(...args); 128 | }, 129 | })); 130 | return Reflect.isTemplateObject(arg) || poked; 131 | } 132 | ], 133 | // Since a motivating use case is to identify strings that 134 | // originated within the current origin, the idiom from the spec note 135 | // shouldn't return true for a template object that originated in a 136 | // different realm. 137 | [ 138 | 'same-realm template object idiom', 139 | () => 140 | Reflect.isTemplateObject(foreignTemplateObject) 141 | && foreignTemplateObject instanceof Array, 142 | ], 143 | ]; 144 | 145 | var falseNegatives = []; 146 | 147 | for (const [ message, f ] of negTestCases) { 148 | let result = null; 149 | try { 150 | result = f(); 151 | } catch (e) { 152 | falseNegatives.push(message + ' threw'); 153 | continue; 154 | } 155 | if (result !== false) { 156 | falseNegatives.push(message); 157 | } 158 | } 159 | 160 | if (falsePositives.length) { 161 | $ERROR(`#1: Reflect.isTemplateObject producing spurious positive results: ${ falsePositives }`); 162 | } 163 | if (falseNegatives.length) { 164 | $ERROR(`#2: Reflect.isTemplateObject producing spurious negative results: ${ falseNegatives }`); 165 | } 166 | if (typeof Reflect.isTemplateObject !== 'function') { 167 | $ERROR('#3: Reflect.isTemplateObject has wrong typeof'); 168 | } 169 | if (Reflect.isTemplateObject.length !== 1) { 170 | $ERROR('#4: Reflect.isTemplateObject has wrong length'); 171 | } 172 | if (Reflect.isTemplateObject.name !== 'isTemplateObject') { 173 | $ERROR('#5: Reflect.isTemplateObject has wrong name'); 174 | } 175 | if (Object.prototype.toString.call(Reflect.isTemplateObject) !== '[object Function]') { 176 | $ERROR('#6: Reflect.isTemplateObject is not a normal function'); 177 | } 178 | -------------------------------------------------------------------------------- /spec.emu: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | title: Reflect.isTemplateObject 8 | stage: 2 9 | copyright: false 10 | contributors: Mike Samuel, Krzysztof Kotowicz, Jordan Harband, Daniel Ehrenberg 11 |12 | 13 |
The creation of a template object cannot result in an abrupt completion.
103 |Each |TemplateLiteral| in the program code of a realm is associated with a unique template object that is used in the evaluation of tagged Templates (
Future editions of this specification may define additional non-enumerable properties of template objects.
109 |When the `isTemplateObject` method is called with argument _value_ the following steps are taken:
125 | 126 |IsTemplateObject is realm-agnostic. Since template objects are frozen before escaping GetTemplateObject, testing (IsTemplateObject(_x_) and _x_.[[Prototype]] is the _realm_'s %Array.prototype%) is sufficient to determine whether an _x_ is a template object in a particular _realm_.
134 |In user code, `Reflect.isTemplateObject(x) && x instanceof Array` is an equivalent test, assuming no changes to builtins.
135 |