Skip to content

Commit 2213894

Browse files
karthiknadigpull[bot]
authored andcommitted
Move replaceAll and splitLines out of string extensions. (#20626)
For #18871
1 parent 95e1f75 commit 2213894

File tree

14 files changed

+102
-101
lines changed

14 files changed

+102
-101
lines changed

src/client/common/extensions.ts

Lines changed: 0 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,7 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4-
/**
5-
* @typedef {Object} SplitLinesOptions
6-
* @property {boolean} [trim=true] - Whether to trim the lines.
7-
* @property {boolean} [removeEmptyEntries=true] - Whether to remove empty entries.
8-
*/
9-
10-
// https://stackoverflow.com/questions/39877156/how-to-extend-string-prototype-and-use-it-next-in-typescript
11-
124
declare interface String {
13-
/**
14-
* Split a string using the cr and lf characters and return them as an array.
15-
* By default lines are trimmed and empty lines are removed.
16-
* @param {SplitLinesOptions=} splitOptions - Options used for splitting the string.
17-
*/
18-
splitLines(splitOptions?: { trim: boolean; removeEmptyEntries?: boolean }): string[];
195
/**
206
* Appropriately formats a string so it can be used as an argument for a command in a shell.
217
* E.g. if an argument contains a space, then it will be enclosed within double quotes.
@@ -37,33 +23,8 @@ declare interface String {
3723
* Removes leading and trailing quotes from a string
3824
*/
3925
trimQuotes(): string;
40-
41-
/**
42-
* String.replaceAll implementation
43-
* Replaces all instances of a substring with a new string
44-
*/
45-
replaceAll(substr: string, newSubstr: string): string;
4626
}
4727

48-
/**
49-
* Split a string using the cr and lf characters and return them as an array.
50-
* By default lines are trimmed and empty lines are removed.
51-
* @param {SplitLinesOptions=} splitOptions - Options used for splitting the string.
52-
*/
53-
String.prototype.splitLines = function (
54-
this: string,
55-
splitOptions: { trim: boolean; removeEmptyEntries: boolean } = { removeEmptyEntries: true, trim: true },
56-
): string[] {
57-
let lines = this.split(/\r?\n/g);
58-
if (splitOptions && splitOptions.trim) {
59-
lines = lines.map((line) => line.trim());
60-
}
61-
if (splitOptions && splitOptions.removeEmptyEntries) {
62-
lines = lines.filter((line) => line.length > 0);
63-
}
64-
return lines;
65-
};
66-
6728
/**
6829
* Appropriately formats a string so it can be used as an argument for a command in a shell.
6930
* E.g. if an argument contains a space, then it will be enclosed within double quotes.
@@ -102,26 +63,6 @@ String.prototype.trimQuotes = function (this: string): string {
10263
return this.replace(/(^['"])|(['"]$)/g, '');
10364
};
10465

105-
/**
106-
* String.replaceAll implementation
107-
* Replaces all instances of a substring with a new substring.
108-
*/
109-
String.prototype.replaceAll = function (this: string, substr: string, newSubstr: string): string {
110-
if (!this) {
111-
return this;
112-
}
113-
114-
/** Escaping function from the MDN web docs site
115-
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
116-
* Escapes all the following special characters in a string . * + ? ^ $ { } ( ) | \ \\ */
117-
118-
function escapeRegExp(unescapedStr: string): string {
119-
return unescapedStr.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
120-
}
121-
122-
return this.replace(new RegExp(escapeRegExp(substr), 'g'), newSubstr);
123-
};
124-
12566
declare interface Promise<T> {
12667
/**
12768
* Catches task error and ignores them.

src/client/common/process/logger.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { Logging } from '../utils/localize';
1111
import { getOSType, getUserHomeDir, OSType } from '../utils/platform';
1212
import { IProcessLogger, SpawnOptions } from './types';
1313
import { escapeRegExp } from 'lodash';
14+
import { replaceAll } from '../stringUtils';
1415

1516
@injectable()
1617
export class ProcessLogger implements IProcessLogger {
@@ -57,7 +58,7 @@ function replaceMatchesWithCharacter(original: string, match: string, character:
5758
let pattern = escapeRegExp(match);
5859
if (getOSType() === OSType.Windows) {
5960
// Match both forward and backward slash versions of 'match' for Windows.
60-
pattern = pattern.replaceAll('\\\\', '(\\\\|/)');
61+
pattern = replaceAll(pattern, '\\\\', '(\\\\|/)');
6162
}
6263
let regex = new RegExp(pattern, 'ig');
6364
return regex;

src/client/common/stringUtils.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
export interface SplitLinesOptions {
5+
trim?: boolean;
6+
removeEmptyEntries?: boolean;
7+
}
8+
9+
/**
10+
* Split a string using the cr and lf characters and return them as an array.
11+
* By default lines are trimmed and empty lines are removed.
12+
* @param {SplitLinesOptions=} splitOptions - Options used for splitting the string.
13+
*/
14+
export function splitLines(
15+
source: string,
16+
splitOptions: SplitLinesOptions = { removeEmptyEntries: true, trim: true },
17+
): string[] {
18+
let lines = source.split(/\r?\n/g);
19+
if (splitOptions?.trim) {
20+
lines = lines.map((line) => line.trim());
21+
}
22+
if (splitOptions?.removeEmptyEntries) {
23+
lines = lines.filter((line) => line.length > 0);
24+
}
25+
return lines;
26+
}
27+
28+
/**
29+
* Replaces all instances of a substring with a new substring.
30+
*/
31+
export function replaceAll(source: string, substr: string, newSubstr: string): string {
32+
if (!source) {
33+
return source;
34+
}
35+
36+
/** Escaping function from the MDN web docs site
37+
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
38+
* Escapes all the following special characters in a string . * + ? ^ $ { } ( ) | \ \\
39+
*/
40+
41+
function escapeRegExp(unescapedStr: string): string {
42+
return unescapedStr.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
43+
}
44+
45+
return source.replace(new RegExp(escapeRegExp(substr), 'g'), newSubstr);
46+
}

src/client/debugger/extension/configuration/dynamicdebugConfigurationService.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { CancellationToken, DebugConfiguration, WorkspaceFolder } from 'vscode';
1010
import { IDynamicDebugConfigurationService } from '../types';
1111
import { DebuggerTypeName } from '../../constants';
1212
import { asyncFilter } from '../../../common/utils/arrayUtils';
13+
import { replaceAll } from '../../../common/stringUtils';
1314

1415
const workspaceFolderToken = '${workspaceFolder}';
1516

@@ -62,7 +63,7 @@ export class DynamicPythonDebugConfigurationService implements IDynamicDebugConf
6263

6364
let fastApiPath = await DynamicPythonDebugConfigurationService.getFastApiPath(folder);
6465
if (fastApiPath) {
65-
fastApiPath = path.relative(folder.uri.fsPath, fastApiPath).replaceAll(path.sep, '.').replace('.py', '');
66+
fastApiPath = replaceAll(path.relative(folder.uri.fsPath, fastApiPath), path.sep, '.').replace('.py', '');
6667
providers.push({
6768
name: 'Python: FastAPI',
6869
type: DebuggerTypeName,

src/client/linters/baseLinter.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { IWorkspaceService } from '../common/application/types';
99
import { isTestExecution } from '../common/constants';
1010
import '../common/extensions';
1111
import { IPythonToolExecutionService } from '../common/process/types';
12+
import { splitLines } from '../common/stringUtils';
1213
import {
1314
ExecutionInfo,
1415
Flake8CategorySeverity,
@@ -184,7 +185,7 @@ export abstract class BaseLinter implements ILinter {
184185
_token: vscode.CancellationToken,
185186
regEx: string,
186187
): Promise<ILintMessage[]> {
187-
const outputLines = output.splitLines({ removeEmptyEntries: false, trim: false });
188+
const outputLines = splitLines(output, { removeEmptyEntries: false, trim: false });
188189
return this.parseLines(outputLines, regEx);
189190
}
190191

src/client/pythonEnvironments/common/environmentManagers/conda.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { cache } from '../../../common/utils/decorators';
2121
import { isTestExecution } from '../../../common/constants';
2222
import { traceError, traceVerbose } from '../../../logging';
2323
import { OUTPUT_MARKER_SCRIPT } from '../../../common/process/internal/scripts';
24+
import { splitLines } from '../../../common/stringUtils';
2425

2526
export const AnacondaCompanyName = 'Anaconda, Inc.';
2627
export const CONDAPATH_SETTING_KEY = 'condaPath';
@@ -185,7 +186,7 @@ export async function getPythonVersionFromConda(interpreterPath: string): Promis
185186
for (const configPath of configPaths) {
186187
if (await pathExists(configPath)) {
187188
try {
188-
const lines = (await readFile(configPath)).splitLines();
189+
const lines = splitLines(await readFile(configPath));
189190

190191
// Sample data:
191192
// +defaults/linux-64::pip-20.2.4-py38_0

src/client/pythonEnvironments/common/environmentManagers/poetry.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { StopWatch } from '../../../common/utils/stopWatch';
1919
import { cache } from '../../../common/utils/decorators';
2020
import { isTestExecution } from '../../../common/constants';
2121
import { traceError, traceVerbose } from '../../../logging';
22+
import { splitLines } from '../../../common/stringUtils';
2223

2324
/**
2425
* Global virtual env dir for a project is named as:
@@ -213,7 +214,7 @@ export class Poetry {
213214
*/
214215
const activated = '(Activated)';
215216
const res = await Promise.all(
216-
result.stdout.splitLines().map(async (line) => {
217+
splitLines(result.stdout).map(async (line) => {
217218
if (line.endsWith(activated)) {
218219
line = line.slice(0, -activated.length);
219220
}

src/client/pythonEnvironments/common/environmentManagers/simplevirtualenvs.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import * as fsapi from 'fs-extra';
55
import * as path from 'path';
66
import '../../../common/extensions';
7+
import { splitLines } from '../../../common/stringUtils';
78
import { getEnvironmentVariable, getOSType, getUserHomeDir, OSType } from '../../../common/utils/platform';
89
import { PythonVersion, UNKNOWN_PYTHON_VERSION } from '../../base/info';
910
import { comparePythonVersionSpecificity } from '../../base/info/env';
@@ -136,7 +137,7 @@ export async function getPythonVersionFromPyvenvCfg(interpreterPath: string): Pr
136137
for (const configPath of configPaths) {
137138
if (await pathExists(configPath)) {
138139
try {
139-
const lines = (await readFile(configPath)).splitLines();
140+
const lines = splitLines(await readFile(configPath));
140141

141142
const pythonVersions = lines
142143
.map((line) => {

src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
CONDA_ENV_CREATED_MARKER,
2828
CONDA_ENV_EXISTING_MARKER,
2929
} from './condaProgressAndTelemetry';
30+
import { splitLines } from '../../../common/stringUtils';
3031

3132
function generateCommandArgs(version?: string, options?: CreateEnvironmentOptions): string[] {
3233
let addGitIgnore = true;
@@ -112,7 +113,7 @@ async function createCondaEnv(
112113
let condaEnvPath: string | undefined;
113114
out.subscribe(
114115
(value) => {
115-
const output = value.out.splitLines().join('\r\n');
116+
const output = splitLines(value.out).join('\r\n');
116117
traceLog(output);
117118
if (output.includes(CONDA_ENV_CREATED_MARKER) || output.includes(CONDA_ENV_EXISTING_MARKER)) {
118119
condaEnvPath = getCondaEnvFromOutput(output);

src/client/pythonEnvironments/info/interpreter.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
InterpreterInfoJson,
99
} from '../../common/process/internal/scripts';
1010
import { ShellExecFunc } from '../../common/process/types';
11+
import { replaceAll } from '../../common/stringUtils';
1112
import { Architecture } from '../../common/utils/platform';
1213
import { copyPythonExecInfo, PythonExecInfo } from '../exec';
1314

@@ -68,7 +69,7 @@ export async function getInterpreterInfo(
6869
const argv = [info.command, ...info.args];
6970

7071
// Concat these together to make a set of quoted strings
71-
const quoted = argv.reduce((p, c) => (p ? `${p} "${c}"` : `"${c.replaceAll('\\', '\\\\')}"`), '');
72+
const quoted = argv.reduce((p, c) => (p ? `${p} "${c}"` : `"${replaceAll(c, '\\', '\\\\')}"`), '');
7273

7374
// Try shell execing the command, followed by the arguments. This will make node kill the process if it
7475
// takes too long.

src/client/testing/testController/unittest/runner.ts

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import { injectable, inject, named } from 'inversify';
55
import { Location, TestController, TestItem, TestMessage, TestRun, TestRunProfileKind } from 'vscode';
66
import * as internalScripts from '../../../common/process/internal/scripts';
7+
import { splitLines } from '../../../common/stringUtils';
78
import { IOutputChannel } from '../../../common/types';
89
import { noop } from '../../../common/utils/misc';
910
import { traceError, traceInfo } from '../../../logging';
@@ -23,6 +24,10 @@ interface ITestData {
2324
subtest?: string;
2425
}
2526

27+
function getTracebackForOutput(traceback: string): string {
28+
return splitLines(traceback, { trim: false, removeEmptyEntries: true }).join('\r\n');
29+
}
30+
2631
@injectable()
2732
export class UnittestRunner implements ITestsRunner {
2833
constructor(
@@ -111,9 +116,7 @@ export class UnittestRunner implements ITestsRunner {
111116
runInstance.appendOutput(fixLogLines(text));
112117
counts.passed += 1;
113118
} else if (data.outcome === 'failed' || data.outcome === 'passed-unexpected') {
114-
const traceback = data.traceback
115-
? data.traceback.splitLines({ trim: false, removeEmptyEntries: true }).join('\r\n')
116-
: '';
119+
const traceback = data.traceback ? getTracebackForOutput(data.traceback) : '';
117120
const text = `${rawTestCase.rawId} Failed: ${data.message ?? data.outcome}\r\n${traceback}\r\n`;
118121
const message = new TestMessage(text);
119122

@@ -128,9 +131,7 @@ export class UnittestRunner implements ITestsRunner {
128131
stopTesting = true;
129132
}
130133
} else if (data.outcome === 'error') {
131-
const traceback = data.traceback
132-
? data.traceback.splitLines({ trim: false, removeEmptyEntries: true }).join('\r\n')
133-
: '';
134+
const traceback = data.traceback ? getTracebackForOutput(data.traceback) : '';
134135
const text = `${rawTestCase.rawId} Failed with Error: ${data.message}\r\n${traceback}\r\n`;
135136
const message = new TestMessage(text);
136137

@@ -145,9 +146,7 @@ export class UnittestRunner implements ITestsRunner {
145146
stopTesting = true;
146147
}
147148
} else if (data.outcome === 'skipped') {
148-
const traceback = data.traceback
149-
? data.traceback.splitLines({ trim: false, removeEmptyEntries: true }).join('\r\n')
150-
: '';
149+
const traceback = data.traceback ? getTracebackForOutput(data.traceback) : '';
151150
const text = `${rawTestCase.rawId} Skipped: ${data.message}\r\n${traceback}\r\n`;
152151
runInstance.skipped(testCase);
153152
runInstance.appendOutput(fixLogLines(text));
@@ -196,9 +195,7 @@ export class UnittestRunner implements ITestsRunner {
196195

197196
if (data.subtest) {
198197
runInstance.appendOutput(fixLogLines(`${data.subtest} Failed\r\n`));
199-
const traceback = data.traceback
200-
? data.traceback.splitLines({ trim: false, removeEmptyEntries: true }).join('\r\n')
201-
: '';
198+
const traceback = data.traceback ? getTracebackForOutput(data.traceback) : '';
202199
const text = `${data.subtest} Failed: ${data.message ?? data.outcome}\r\n${traceback}\r\n`;
203200
runInstance.appendOutput(fixLogLines(text));
204201

@@ -226,9 +223,7 @@ export class UnittestRunner implements ITestsRunner {
226223
runInstance.errored(testCase, message);
227224
}
228225
} else if (data.outcome === 'error') {
229-
const traceback = data.traceback
230-
? data.traceback.splitLines({ trim: false, removeEmptyEntries: true }).join('\r\n')
231-
: '';
226+
const traceback = data.traceback ? getTracebackForOutput(data.traceback) : '';
232227
const text = `${data.test} Failed with Error: ${data.message}\r\n${traceback}\r\n`;
233228
runInstance.appendOutput(fixLogLines(text));
234229
}

src/client/testing/testController/workspaceTestAdapter.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
Uri,
1515
Location,
1616
} from 'vscode';
17+
import { splitLines } from '../../common/stringUtils';
1718
import { createDeferred, Deferred } from '../../common/utils/async';
1819
import { Testing } from '../../common/utils/localize';
1920
import { traceError } from '../../logging';
@@ -138,11 +139,12 @@ export class WorkspaceTestAdapter {
138139
rawTestExecData.result[keyTemp].outcome === 'subtest-failure' ||
139140
rawTestExecData.result[keyTemp].outcome === 'passed-unexpected'
140141
) {
141-
const traceback = rawTestExecData.result[keyTemp].traceback
142-
? rawTestExecData.result[keyTemp]
143-
.traceback!.splitLines({ trim: false, removeEmptyEntries: true })
144-
.join('\r\n')
145-
: '';
142+
const rawTraceback = rawTestExecData.result[keyTemp].traceback ?? '';
143+
const traceback = splitLines(rawTraceback, {
144+
trim: false,
145+
removeEmptyEntries: true,
146+
}).join('\r\n');
147+
146148
const text = `${rawTestExecData.result[keyTemp].test} failed: ${
147149
rawTestExecData.result[keyTemp].message ?? rawTestExecData.result[keyTemp].outcome
148150
}\r\n${traceback}\r\n`;

src/test/common/extensions.unit.test.ts

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -102,20 +102,6 @@ suite('String Extensions', () => {
102102
expect(quotedString3.trimQuotes()).to.be.equal(expectedString);
103103
expect(quotedString4.trimQuotes()).to.be.equal(expectedString);
104104
});
105-
test('String should replace all substrings with new substring', () => {
106-
const oldString = `foo \\ foo \\ foo`;
107-
const expectedString = `foo \\\\ foo \\\\ foo`;
108-
const oldString2 = `\\ foo \\ foo`;
109-
const expectedString2 = `\\\\ foo \\\\ foo`;
110-
const oldString3 = `\\ foo \\`;
111-
const expectedString3 = `\\\\ foo \\\\`;
112-
const oldString4 = `foo foo`;
113-
const expectedString4 = `foo foo`;
114-
expect(oldString.replaceAll('\\', '\\\\')).to.be.equal(expectedString);
115-
expect(oldString2.replaceAll('\\', '\\\\')).to.be.equal(expectedString2);
116-
expect(oldString3.replaceAll('\\', '\\\\')).to.be.equal(expectedString3);
117-
expect(oldString4.replaceAll('\\', '\\\\')).to.be.equal(expectedString4);
118-
});
119105
});
120106

121107
suite('Array extensions', () => {

0 commit comments

Comments
 (0)