Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions classes/range.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ class Range {
debug('hyphen replace', range)

// `> 1.2.3 < 1.2.5` => `>1.2.3 <1.2.5`
range = range.replace(re[t.COMPARATORTRIM], comparatorTrimReplace)
range = trimComparatorWhitespace(range)
debug('comparator trim', range)

// `~ 1.2.3` => `~1.2.3`
Expand Down Expand Up @@ -224,11 +224,11 @@ const parseOptions = require('../internal/parse-options')
const Comparator = require('./comparator')
const debug = require('../internal/debug')
const SemVer = require('./semver')
const trimComparatorWhitespace = require('../internal/trim-comparator-whitespace')
const {
safeRe: re,
src,
t,
comparatorTrimReplace,
tildeTrimReplace,
caretTrimReplace,
} = require('../internal/re')
Expand Down
91 changes: 91 additions & 0 deletions internal/trim-comparator-whitespace.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
'use strict'

const isWhitespace = char => {
const code = char && char.charCodeAt(0)
return (
(code >= 0x0009 && code <= 0x000d) ||
code === 0x0020 ||
code === 0x00a0 ||
code === 0x1680 ||
(code >= 0x2000 && code <= 0x200a) ||
code === 0x2028 ||
code === 0x2029 ||
code === 0x202f ||
code === 0x205f ||
code === 0x3000 ||
code === 0xfeff
)
}

const isVersionPrefix = char =>
char === 'v' ||
char === '=' ||
isWhitespace(char)

const isVersionStart = char =>
(char >= '0' && char <= '9') ||
char === 'x' ||
char === 'X' ||
char === '*'

// Normalize comparator whitespace without running the unanchored
// COMPARATORTRIM regex against the full range.
const trimComparatorWhitespace = range => {
const removals = []

for (let versionStart = 0; versionStart < range.length; versionStart++) {
if (!isVersionStart(range[versionStart])) {
continue
}

let prefixStart = versionStart
while (prefixStart > 0 && isVersionPrefix(range[prefixStart - 1])) {
prefixStart--
}

let operatorEnd
if (
prefixStart > 0 &&
(range[prefixStart - 1] === '<' || range[prefixStart - 1] === '>')
) {
operatorEnd = prefixStart
if (range[operatorEnd] === '=') {
operatorEnd++
}
} else {
operatorEnd = prefixStart
if (isWhitespace(range[operatorEnd])) {
operatorEnd++
}
if (range[operatorEnd] !== '=') {
if (
!isWhitespace(range[prefixStart]) ||
!isWhitespace(range[prefixStart + 1])
) {
continue
}
operatorEnd = prefixStart + 1
} else {
operatorEnd++
}
}

if (isWhitespace(range[operatorEnd])) {
removals.push([operatorEnd, operatorEnd + 1])
}
}

if (!removals.length) {
return range
}

let result = ''
let position = 0
for (const [start, end] of removals) {
result += range.slice(position, start)
position = end
}
return result + range.slice(position)
}

module.exports = trimComparatorWhitespace
17 changes: 17 additions & 0 deletions test/fixtures/range-parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,17 @@ module.exports = [
['<= 2.0.0', '<=2.0.0'],
['< 2.0.0', '<2.0.0'],
['<\t2.0.0', '<2.0.0'],
['<\u00a02.0.0', '<2.0.0'],
['<\ufeff2.0.0', '<2.0.0'],
['1.2.3 = 1.2.3', '1.2.3'],
['== 1', null],
['>v= 1.2.3', '1.2.3', { loose: true }],
['> +foo 1.2.3', null],
['> +foo 1.2.3', '1.2.3', { loose: true }],
['1 +a +b = 2', null],
['~ +build 1', '>=1.0.0 <2.0.0-0'],
['~> +build 1', '>=1.0.0 <2.0.0-0'],
['^ +build 1', '>=1.0.0 <2.0.0-0'],
['>=0.1.97', '>=0.1.97'],
['0.1.20 || 1.2.4', '0.1.20||1.2.4'],
['>=0.2.3 || <0.0.1', '>=0.2.3||<0.0.1'],
Expand All @@ -41,10 +52,16 @@ module.exports = [
['1.x.5', null],
['1.*.5', null],
['1.x.5 || 2.x', null],
['> invalid', null],
['x.1', null],
['x.1.2', null],
['x.x.1', null],
['x', '*'],
['vvv1', '>=1.0.0 <2.0.0-0'],
['>= vvv1', '>=1.0.0'],
['vvv1.2.3', '1.2.3', { loose: true }],
['===1.2.3', '1.2.3', { loose: true }],
['v=1.2.3', '1.2.3', { loose: true }],
['2.*.*', '>=2.0.0 <3.0.0-0'],
['1.2.*', '>=1.2.0 <1.3.0-0'],
['1.2.* || 2.*', '>=1.2.0 <1.3.0-0||>=2.0.0 <3.0.0-0'],
Expand Down
21 changes: 21 additions & 0 deletions test/integration/whitespace.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ const validRange = require('../../ranges/valid')
const minVersion = require('../../ranges/min-version')
const minSatisfying = require('../../ranges/min-satisfying')
const maxSatisfying = require('../../ranges/max-satisfying')
const satisfies = require('../../functions/satisfies')

const wsMedium = ' '.repeat(125)
const wsLarge = ' '.repeat(500000)
const zeroLarge = '0'.repeat(500000)
const versionPrefixLarge = 'v'.repeat(500000)

test('range with whitespace', (t) => {
// a range with these extra characters would take a few minutes to process if
Expand All @@ -23,6 +25,7 @@ test('range with whitespace', (t) => {
t.equal(minVersion(r).version, '1.2.3')
t.equal(minSatisfying(['1.2.3'], r), '1.2.3')
t.equal(maxSatisfying(['1.2.3'], r), '1.2.3')
t.throws(() => new Range('> invalid'))
t.end()
})

Expand All @@ -36,6 +39,24 @@ test('range with 0', (t) => {
t.end()
})

test('range with repeated version prefix', (t) => {
t.throws(() => new Range(versionPrefixLarge))
t.equal(validRange(versionPrefixLarge), null)
t.equal(satisfies('1.2.3', versionPrefixLarge), false)
t.throws(() => minVersion(versionPrefixLarge))
t.equal(minSatisfying(['1.2.3'], versionPrefixLarge), null)
t.equal(maxSatisfying(['1.2.3'], versionPrefixLarge), null)

const r = `>= ${versionPrefixLarge}1`
t.equal(new Range(r).range, '>=1.0.0')
t.equal(validRange(r), '>=1.0.0')
t.equal(satisfies('1.2.3', r), true)
t.equal(minVersion(r).version, '1.0.0')
t.equal(minSatisfying(['1.2.3'], r), '1.2.3')
t.equal(maxSatisfying(['1.2.3'], r), '1.2.3')
t.end()
})

test('semver version', (t) => {
const v = `${wsMedium}1.2.3${wsMedium}`
const tooLong = `${wsLarge}1.2.3${wsLarge}`
Expand Down
86 changes: 86 additions & 0 deletions test/internal/trim-comparator-whitespace.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
'use strict'

const { test } = require('tap')
const trimComparatorWhitespace =
require('../../internal/trim-comparator-whitespace')
const {
safeRe,
t: tokens,
comparatorTrimReplace,
} = require('../../internal/re')

const whitespace = [
'\t',
'\n',
'\v',
'\f',
'\r',
' ',
'\u00a0',
'\u1680',
'\u2000',
'\u2001',
'\u2002',
'\u2003',
'\u2004',
'\u2005',
'\u2006',
'\u2007',
'\u2008',
'\u2009',
'\u200a',
'\u2028',
'\u2029',
'\u202f',
'\u205f',
'\u3000',
'\ufeff',
]

const operators = ['', '=', '==', '<', '<=', '>', '>=']
const versions = ['0', '1', '1.2', '1.2.3', 'x', 'X', '*']

test('matches comparator trim behavior', t => {
const mismatches = []
let checked = 0

for (const ws of whitespace) {
const prefixCharacters = ['v', '=', ws]
const prefixes = ['', 'vvv', '===', 'v=v']

for (const first of prefixCharacters) {
prefixes.push(first)
for (const second of prefixCharacters) {
prefixes.push(first + second)
}
}

for (const operator of operators) {
for (const prefix of prefixes) {
for (const version of versions) {
const inputs = [
`${operator}${ws}${prefix}${version}`,
`${ws}${operator}${ws}${prefix}${version}`,
`1.2.3${ws}${operator}${ws}${prefix}${version}`,
`${operator}${ws}${prefix}${version}${ws}<${ws}2.0.0`,
]

for (const input of inputs) {
const expected = input.replace(
safeRe[tokens.COMPARATORTRIM],
comparatorTrimReplace
)
const actual = trimComparatorWhitespace(input)
checked++
if (actual !== expected && mismatches.length < 20) {
mismatches.push({ input, expected, actual })
}
}
}
}
}
}

t.strictSame(mismatches, [], `matches ${checked} bounded legacy cases`)
t.end()
})
Loading