Skip to content

Commit 7110de3

Browse files
authored
warn for ambiguous colons in formats (#2335)
1 parent c1784c4 commit 7110de3

11 files changed

+156
-71
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Please see [CONTRIBUTING.md](./CONTRIBUTING.md) on how to contribute to Cucumber
1010
## [Unreleased]
1111
### Fixed
1212
- Improve handling of formatter paths ([#2315](https://github.com/cucumber/cucumber-js/pull/2315))
13+
- Warn on ambiguous colons in formatter paths ([#2335](https://github.com/cucumber/cucumber-js/pull/2335))
1314

1415
## [9.5.1] - 2023-09-06
1516
### Fixed

docs/deprecations.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,17 @@ The `publishQuiet` option (or `--publish-quiet` on the CLI) was used to hide the
5454

5555
To adapt, remove the option from your configuration files and CLI commands (especially the latter, since the CLI will fail on unrecognised options).
5656

57+
### Ambiguous colons in formats
58+
59+
Deprecated in `9.6.0`. Will be removed in `11.0.0` or later.
60+
61+
User-specified formats where either the formatter name/path or the target path (or both) contains colon(s) are ambiguous because the separator between the two parts is also a colon. Cucumber tries to detect and handle things like Windows drives and `file://` URLs on a best-effort basis, but this logic is being removed in favour of wrapping values in double-quotes.
62+
63+
| Before | After |
64+
|----------------------------------------------|--------------------------------------------------|
65+
| `html:file://hostname/formatter/report.html` | `"html":"file://hostname/formatter/report.html"` |
66+
| `file://C:\custom\formatter` | `"file://C:\custom\formatter"` |
67+
5768
## Previous deprecations
5869

5970
For deprecations that have been completed (i.e. the functionality removed), see [UPGRADING.md](../UPGRADING.md).

docs/formatters.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ cucumber-js provides many built-in Formatters, plus building blocks with which y
77
You can specify one or more formats via the `format` configuration option:
88

99
- In a configuration file `{ format: ['progress-bar', ['html', 'cucumber-report.html']] }`
10-
- On the CLI `$ cucumber-js --format progress-bar --format html:cucumber-report.html`
10+
- On the CLI `$ cucumber-js --format progress-bar --format "html":"cucumber-report.html"`
1111

1212
For each format you specify, you have to provide one or two values. The first (required) is to identify the formatter. It can take a few forms:
1313

@@ -18,7 +18,7 @@ For each format you specify, you have to provide one or two values. The first (r
1818

1919
Without a second value, the formatter will print to `stdout`. The second value, if present, is a path to where the formatter output should be written. If the path includes directories that do not yet exist, they will be created.
2020

21-
On the CLI, when specifying both a name and path, you'll need to use `:` as a delimiter. In a configuration file you can do this too, but you can also provide an array with the two values as separate strings, which is recommended.
21+
On the CLI, when specifying both a name and path, you'll need to use `:` as a delimiter and wrap each side of it with double quotes. In a configuration file you can do this too, but you can also provide an array with the two values as separate strings, which is recommended.
2222

2323
Some notes on specifying Formatters:
2424

src/api/convert_configuration.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import {
22
IConfiguration,
33
isTruthyString,
4-
OptionSplitter,
4+
splitFormatDescriptor,
55
} from '../configuration'
66
import { IPublishConfig } from '../formatter'
77
import { IRunConfiguration } from './types'
8+
import { ILogger } from '../logger'
89

910
export async function convertConfiguration(
11+
logger: ILogger,
1012
flatConfiguration: IConfiguration,
1113
env: NodeJS.ProcessEnv
1214
): Promise<IRunConfiguration> {
@@ -33,16 +35,17 @@ export async function convertConfiguration(
3335
strict: flatConfiguration.strict,
3436
worldParameters: flatConfiguration.worldParameters,
3537
},
36-
formats: convertFormats(flatConfiguration, env),
38+
formats: convertFormats(logger, flatConfiguration, env),
3739
}
3840
}
3941

4042
function convertFormats(
43+
logger: ILogger,
4144
flatConfiguration: IConfiguration,
4245
env: NodeJS.ProcessEnv
4346
) {
4447
const splitFormats: string[][] = flatConfiguration.format.map((item) =>
45-
Array.isArray(item) ? item : OptionSplitter.split(item)
48+
Array.isArray(item) ? item : splitFormatDescriptor(logger, item)
4649
)
4750
return {
4851
stdout:

src/api/convert_configuration_spec.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,15 @@ import { describe, it } from 'mocha'
22
import { expect } from 'chai'
33
import { convertConfiguration } from './convert_configuration'
44
import { DEFAULT_CONFIGURATION } from '../configuration'
5+
import { FakeLogger } from '../../test/fake_logger'
56

67
describe('convertConfiguration', () => {
78
it('should convert defaults correctly', async () => {
8-
const result = await convertConfiguration(DEFAULT_CONFIGURATION, {})
9+
const result = await convertConfiguration(
10+
new FakeLogger(),
11+
DEFAULT_CONFIGURATION,
12+
{}
13+
)
914

1015
expect(result).to.eql({
1116
formats: {
@@ -41,6 +46,7 @@ describe('convertConfiguration', () => {
4146

4247
it('should map multiple formatters with string notation', async () => {
4348
const result = await convertConfiguration(
49+
new FakeLogger(),
4450
{
4551
...DEFAULT_CONFIGURATION,
4652
format: [
@@ -66,6 +72,7 @@ describe('convertConfiguration', () => {
6672

6773
it('should map multiple formatters with array notation', async () => {
6874
const result = await convertConfiguration(
75+
new FakeLogger(),
6976
{
7077
...DEFAULT_CONFIGURATION,
7178
format: [
@@ -91,6 +98,7 @@ describe('convertConfiguration', () => {
9198

9299
it('should map formatters correctly when file:// urls are involved', async () => {
93100
const result = await convertConfiguration(
101+
new FakeLogger(),
94102
{
95103
...DEFAULT_CONFIGURATION,
96104
format: [

src/api/load_configuration.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export async function loadConfiguration(
4444
)
4545
logger.debug('Resolved configuration:', original)
4646
validateConfiguration(original, logger)
47-
const runnable = await convertConfiguration(original, env)
47+
const runnable = await convertConfiguration(logger, original, env)
4848
return {
4949
useConfiguration: original,
5050
runConfiguration: runnable,

src/cli/helpers.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { EventEmitter } from 'events'
33
import PickleFilter from '../pickle_filter'
44
import { EventDataCollector } from '../formatter/helpers'
55
import { doesHaveValue } from '../value_checker'
6-
import { OptionSplitter } from '../configuration'
76
import { Readable } from 'stream'
87
import os from 'os'
98
import * as messages from '@cucumber/messages'
@@ -71,7 +70,7 @@ export function orderPickles<T = string>(
7170
order: PickleOrder,
7271
logger: ILogger
7372
): void {
74-
const [type, seed] = OptionSplitter.split(order)
73+
const [type, seed] = splitOrder(order)
7574
switch (type) {
7675
case 'defined':
7776
break
@@ -91,6 +90,13 @@ export function orderPickles<T = string>(
9190
}
9291
}
9392

93+
function splitOrder(order: string) {
94+
if (!order.includes(':')) {
95+
return [order, '']
96+
}
97+
return order.split(':')
98+
}
99+
94100
export async function emitMetaMessage(
95101
eventBroadcaster: EventEmitter,
96102
env: NodeJS.ProcessEnv

src/configuration/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@ export * from './default_configuration'
33
export * from './from_file'
44
export * from './helpers'
55
export * from './merge_configurations'
6-
export * from './option_splitter'
6+
export * from './split_format_descriptor'
77
export * from './types'

src/configuration/option_splitter.ts

Lines changed: 0 additions & 57 deletions
This file was deleted.
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { ILogger } from '../logger'
2+
3+
export function splitFormatDescriptor(
4+
logger: ILogger,
5+
option: string
6+
): string[] {
7+
const doWarning = (result: string[]) => {
8+
let expected = `"${result[0]}"`
9+
if (result[1]) {
10+
expected += `:"${result[1]}"`
11+
}
12+
logger.warn(
13+
`Each part of a user-specified format should be quoted; see https://github.com/cucumber/cucumber-js/blob/main/docs/deprecations.md#ambiguous-colons-in-formats
14+
Change to ${expected}`
15+
)
16+
}
17+
let result: string[]
18+
let match1, match2
19+
20+
// "foo":"bar" or "foo":bar
21+
if ((match1 = option.match(/^"([^"]*)":(.*)$/)) !== null) {
22+
// "foo":"bar"
23+
if ((match2 = match1[2].match(/^"([^"]*)"$/)) !== null) {
24+
result = [match1[1], match2[1]]
25+
}
26+
// "foo":bar
27+
else {
28+
result = [match1[1], match1[2]]
29+
if (result[1].includes(':')) {
30+
doWarning(result)
31+
}
32+
}
33+
}
34+
// foo:"bar"
35+
else if ((match1 = option.match(/^(.*):"([^"]*)"$/)) !== null) {
36+
result = [match1[1], match1[2]]
37+
if (result[0].includes(':')) {
38+
doWarning(result)
39+
}
40+
}
41+
// "foo"
42+
else if ((match1 = option.match(/^"([^"]*)"$/)) !== null) {
43+
result = [match1[1], '']
44+
}
45+
// file://foo or file:///foo or file://C:/foo or file://C:\foo or file:///C:/foo or file:///C:\foo
46+
else if (
47+
(match1 = option.match(/^(file:\/{2,3}(?:[a-zA-Z]:[/\\])?)(.*)$/)) !== null
48+
) {
49+
// file://foo:bar
50+
if ((match2 = match1[2].match(/^([^:]*):(.*)$/)) !== null) {
51+
result = [match1[1] + match2[1], match2[2]]
52+
} else {
53+
result = [option, '']
54+
}
55+
doWarning(result)
56+
}
57+
// C:\foo or C:/foo
58+
else if ((match1 = option.match(/^([a-zA-Z]:[/\\])(.*)$/)) !== null) {
59+
// C:\foo:bar or C:/foo:bar
60+
if ((match2 = match1[2].match(/^([^:]*):(.*)$/)) !== null) {
61+
result = [match1[1] + match2[1], match2[2]]
62+
} else {
63+
result = [option, '']
64+
}
65+
doWarning(result)
66+
}
67+
// foo:bar
68+
else if ((match1 = option.match(/^([^:]*):(.*)$/)) !== null) {
69+
result = [match1[1], match1[2]]
70+
if (option.split(':').length > 2) {
71+
doWarning(result)
72+
}
73+
}
74+
// foo
75+
else {
76+
result = [option, '']
77+
}
78+
79+
return result
80+
}

0 commit comments

Comments
 (0)