Skip to content

Commit

Permalink
Initial implementation, tests, readme
Browse files Browse the repository at this point in the history
  • Loading branch information
ljharb committed Apr 20, 2023
1 parent 9cfa07b commit 31b8e70
Show file tree
Hide file tree
Showing 13 changed files with 354 additions and 7 deletions.
15 changes: 15 additions & 0 deletions .eslintrc
@@ -0,0 +1,15 @@
{
"root": true,

"extends": "@ljharb",

"rules": {
"id-length": "off",
"max-lines-per-function": "off",
"new-cap": ["error", {
"capIsNewExceptions": [
"GetIntrinsic",
],
}],
},
}
12 changes: 12 additions & 0 deletions .github/FUNDING.yml
@@ -0,0 +1,12 @@
# These are supported funding model platforms

github: [ljharb]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: npm/safe-array-concat
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
18 changes: 18 additions & 0 deletions .github/workflows/node-aught.yml
@@ -0,0 +1,18 @@
name: 'Tests: node.js < 10'

on: [pull_request, push]

jobs:
tests:
uses: ljharb/actions/.github/workflows/node.yml@main
with:
range: '< 10'
type: minors
command: npm run tests-only

node:
name: 'node < 10'
needs: [tests]
runs-on: ubuntu-latest
steps:
- run: 'echo tests completed'
7 changes: 7 additions & 0 deletions .github/workflows/node-pretest.yml
@@ -0,0 +1,7 @@
name: 'Tests: pretest/posttest'

on: [pull_request, push]

jobs:
tests:
uses: ljharb/actions/.github/workflows/pretest.yml@main
18 changes: 18 additions & 0 deletions .github/workflows/node-tens.yml
@@ -0,0 +1,18 @@
name: 'Tests: node.js >= 10'

on: [pull_request, push]

jobs:
tests:
uses: ljharb/actions/.github/workflows/node.yml@main
with:
range: '>= 10'
type: minors
command: npm run tests-only

node:
name: 'node >= 10'
needs: [tests]
runs-on: ubuntu-latest
steps:
- run: 'echo tests completed'
22 changes: 22 additions & 0 deletions .github/workflows/rebase.yml
@@ -0,0 +1,22 @@
name: Automatic Rebase

on: [pull_request_target]

permissions:
contents: read

jobs:
_:
permissions:
contents: write # for ljharb/rebase to push code to rebase
pull-requests: read # for ljharb/rebase to get info about PR

name: "Automatic Rebase"

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
- uses: ljharb/rebase@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
12 changes: 12 additions & 0 deletions .github/workflows/require-allow-edits.yml
@@ -0,0 +1,12 @@
name: Require “Allow Edits”

on: [pull_request_target]

jobs:
_:
name: "Require “Allow Edits”"

runs-on: ubuntu-latest

steps:
- uses: ljharb/require-allow-edits@main
2 changes: 2 additions & 0 deletions .gitignore
Expand Up @@ -107,3 +107,5 @@ dist
npm-shrinkwrap.json
package-lock.json
yarn.lock

.npmignore
13 changes: 13 additions & 0 deletions .nycrc
@@ -0,0 +1,13 @@
{
"all": true,
"check-coverage": false,
"reporter": ["text-summary", "text", "html", "json"],
"lines": 86,
"statements": 85.93,
"functions": 82.43,
"branches": 76.06,
"exclude": [
"coverage",
"test"
]
}
55 changes: 53 additions & 2 deletions README.md
@@ -1,2 +1,53 @@
# safe-array-concat
Array.prototype.concat, but is safe by ignoring Symbol.isConcatSpreadable
# safe-array-concat <sup>[![Version Badge][npm-version-svg]][package-url]</sup>

[![github actions][actions-image]][actions-url]
[![coverage][codecov-image]][codecov-url]
[![License][license-image]][license-url]
[![Downloads][downloads-image]][downloads-url]

[![npm badge][npm-badge-png]][package-url]

`Array.prototype.concat`, but made safe by ignoring Symbol.isConcatSpreadable

## Getting started

```sh
npm install --save safe-array-concat
```

## Usage/Examples

```js
var safeConcat = require('safe-array-concat');
var assert = require('assert');

assert.deepEqual([].concat([1, 2], 3, [[4]]), [1, 2, 3, [4]], 'arrays spread as expected with normal concat');
assert.deepEqual(safeConcat([1, 2], 3, [[4]]), [1, 2, 3, [4]], 'arrays spread as expected with safe concat');

String.prototype[Symbol.isConcatSpreadable] = true;
assert.deepEqual([].concat('foo', Object('bar')), ['foo', 'b', 'a', 'r'], 'spreadable String objects are spread with normal concat!!!');
assert.deepEqual(safeConcat('foo', Object('bar')), ['foo', Object('bar')], 'spreadable String objects are not spread with safe concat');

Array.prototype[Symbol.isConcatSpreadable] = false;
assert.deepEqual([].concat([1, 2], 3, [[4]]), [[], [1, 2], 3, [[4]]], 'non-concat-spreadable arrays do not spread with normal concat!!!');
assert.deepEqual(safeConcat([1, 2], 3, [[4]]), [1, 2, 3, [4]], 'non-concat-spreadable arrays still spread with safe concat');
```

## Tests
Simply clone the repo, `npm install`, and run `npm test`

[package-url]: https://npmjs.org/package/safe-array-concat
[npm-version-svg]: https://versionbadg.es/ljharb/safe-array-concat.svg
[deps-svg]: https://david-dm.org/ljharb/safe-array-concat.svg
[deps-url]: https://david-dm.org/ljharb/safe-array-concat
[dev-deps-svg]: https://david-dm.org/ljharb/safe-array-concat/dev-status.svg
[dev-deps-url]: https://david-dm.org/ljharb/safe-array-concat#info=devDependencies
[npm-badge-png]: https://nodei.co/npm/safe-array-concat.png?downloads=true&stars=true
[license-image]: https://img.shields.io/npm/l/safe-array-concat.svg
[license-url]: LICENSE
[downloads-image]: https://img.shields.io/npm/dm/safe-array-concat.svg
[downloads-url]: https://npm-stat.com/charts.html?package=safe-array-concat
[codecov-image]: https://codecov.io/gh/ljharb/safe-array-concat/branch/main/graphs/badge.svg
[codecov-url]: https://app.codecov.io/gh/ljharb/safe-array-concat/
[actions-image]: https://img.shields.io/endpoint?url=https://github-actions-badge-u3jn4tfpocch.runkit.sh/ljharb/safe-array-concat
[actions-url]: https://github.com/ljharb/safe-array-concat/actions
36 changes: 36 additions & 0 deletions index.js
@@ -0,0 +1,36 @@
'use strict';

var GetIntrinsic = require('get-intrinsic');
var $concat = GetIntrinsic('%Array.prototype.concat%');

var callBind = require('call-bind');

var callBound = require('call-bind/callBound');
var $slice = callBound('Array.prototype.slice');

var hasSymbols = require('has-symbols/shams')();
var isConcatSpreadable = hasSymbols && Symbol.isConcatSpreadable;

var empty = [];
if (isConcatSpreadable) {
empty[isConcatSpreadable] = true;
}
var $concatApply = isConcatSpreadable ? callBind.apply($concat, empty) : null;
var $concatCall = isConcatSpreadable ? null : callBind($concat, empty);

var isArray = isConcatSpreadable ? require('isarray') : null;

module.exports = isConcatSpreadable
// eslint-disable-next-line no-unused-vars
? function safeArrayConcat(item) {
for (var i = 0; i < arguments.length; i += 1) {
var arg = arguments[i];
if (arg && typeof arg === 'object' && typeof arg[isConcatSpreadable] === 'boolean') {
var arr = isArray(arg) ? $slice(arg) : [arg];
arr[isConcatSpreadable] = true; // shadow the property. TODO: use [[Define]]
arguments[i] = arr;
}
}
return $concatApply(arguments);
}
: $concatCall;
62 changes: 57 additions & 5 deletions package.json
@@ -1,19 +1,36 @@
{
"name": "safe-array-concat",
"version": "0.0.0",
"description": "Array.prototype.concat, but is safe by ignoring Symbol.isConcatSpreadable",
"description": "`Array.prototype.concat`, but made safe by ignoring Symbol.isConcatSpreadable",
"main": "index.js",
"exports": {
".": "./index.js",
"./package.json": "./package.json"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"prepack": "npmignore --auto --commentLines=autogenerated",
"version": "auto-changelog && git add CHANGELOG.md",
"postversion": "auto-changelog && git add CHANGELOG.md && git commit --no-edit --amend && git tag -f \"v$(node -e \"console.log(require('./package.json').version)\")\"",
"lint": "eslint --ext=js,mjs .",
"postlint": "evalmd README.md",
"prepublish": "not-in-publish || npm run prepublishOnly",
"prepublishOnly": "safe-publish-latest",
"pretest": "npm run lint",
"tests-only": "nyc tape test",
"test": "npm run tests-only",
"posttest": "aud --production"
},
"keywords": [
"safe",
"array",
"Array",
"concat",
"push",
"isconcatspreadable"
"isConcatSpreadable"
],
"author": "Jordan Harband <ljharb@gmail.com>",
"funding": {
"url": "https://github.com/sponsors/ljharb"
},
"license": "MIT",
"repository": {
"type": "git",
Expand All @@ -22,5 +39,40 @@
"bugs": {
"url": "https://github.com/ljharb/safe-array-concat/issues"
},
"homepage": "https://github.com/ljharb/safe-array-concat#readme"
"homepage": "https://github.com/ljharb/safe-array-concat#readme",
"devDependencies": {
"@ljharb/eslint-config": "^21.0.1",
"aud": "^2.0.2",
"auto-changelog": "^2.4.0",
"eslint": "=8.8.0",
"evalmd": "^0.0.19",
"in-publish": "^2.0.1",
"mock-property": "^1.0.0",
"npmignore": "^0.3.0",
"nyc": "^10.3.2",
"safe-publish-latest": "^2.0.0",
"tape": "^5.6.3"
},
"dependencies": {
"call-bind": "^1.0.2",
"get-intrinsic": "^1.2.0",
"has-symbols": "^1.0.3",
"isarray": "^2.0.5"
},
"auto-changelog": {
"output": "CHANGELOG.md",
"template": "keepachangelog",
"unreleased": false,
"commitLimit": false,
"backfillLimit": false,
"hideCredit": true
},
"publishConfig": {
"ignore": [
".github/workflows"
]
},
"engines": {
"node": ">=0.4"
}
}
89 changes: 89 additions & 0 deletions test/index.js
@@ -0,0 +1,89 @@
'use strict';

var test = require('tape');
var mockProperty = require('mock-property');
var hasSymbols = require('has-symbols/shams')();
var isConcatSpreadable = hasSymbols && Symbol.isConcatSpreadable;
var species = hasSymbols && Symbol.species;

var boundFnsHaveConfigurableLengths = Object.getOwnPropertyDescriptor && Object.getOwnPropertyDescriptor(function () {}.bind(), 'length').configurable;

var safeConcat = require('../');

test('safe-array-concat', function (t) {
t.equal(typeof safeConcat, 'function', 'is a function');
t.equal(
safeConcat.length,
boundFnsHaveConfigurableLengths ? 1 : 0,
'has a length of ' + (boundFnsHaveConfigurableLengths ? 1 : '0 (function lengths are not configurable)'),
'length is as expected'
);

t.deepEqual(
safeConcat([1, 2], [3, 4], 'foo', 5, 6, [[7]]),
[1, 2, 3, 4, 'foo', 5, 6, [7]],
'works with flat and nested arrays'
);

t.deepEqual(
safeConcat(undefined, 1, 2),
[undefined, 1, 2],
'first item as undefined is not the concat receiver, which would throw via ToObject'
);
t.deepEqual(
safeConcat(null, 1, 2),
[null, 1, 2],
'first item as null is not the concat receiver, which would throw via ToObject'
);

var arr = [1, 2];
arr.constructor = function C() {
return { args: arguments };
};
t.deepEqual(
safeConcat(arr, 3, 4),
[1, 2, 3, 4],
'first item as an array with a nonArray .constructor; ignores constructor'
);

t.test('has Symbol.species', { skip: !species }, function (st) {
var speciesArr = [1, 2];
speciesArr.constructor = {};
speciesArr.constructor[species] = function Species() {
return { args: arguments };
};

st.deepEqual(
safeConcat(speciesArr, 3, 4),
[1, 2, 3, 4],
'first item as an array with a .constructor object with a Symbol.species; ignores constructor and species'
);

st.end();
});

t.test('has isConcatSpreadable', { skip: !isConcatSpreadable }, function (st) {
st.teardown(mockProperty(String.prototype, isConcatSpreadable, { value: true }));

var nonSpreadable = [1, 2];
nonSpreadable[isConcatSpreadable] = false;

st.deepEqual(
safeConcat(nonSpreadable, 3, 4, 'foo', Object('bar')),
[1, 2, 3, 4, 'foo', Object('bar')],
'a non-concat-spreadable array is spreaded, and a concat-spreadable String is not spreaded'
);

st.teardown(mockProperty(Array.prototype, isConcatSpreadable, { value: false }));

st.deepEqual(
safeConcat([1, 2], 3, 4, 'foo', Object('bar')),
[1, 2, 3, 4, 'foo', Object('bar')],
'all arrays marked non-concat-spreadable are still spreaded, and a concat-spreadable String is not spreaded'
);

st.end();
});

t.end();
});

0 comments on commit 31b8e70

Please sign in to comment.