Skip to content

Commit 0b95ef8

Browse files
Extract Windows device-name check and rename script template
Move the Windows reserved-device-name test out of the script filename validator and into a shared isWindowsReservedDeviceName helper in common/utils/pathUtils, so the rule lives with the other path utilities and can be reused. newScriptProject now calls the helper and no longer imports isWindows directly. Add focused unit coverage for the helper. Rename the internal template folder new723ScriptTemplate to newInlineScriptTemplate and update all creator and test references. Only the folder name changes; the template content and the user-facing creator, class, and command are untouched. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1487b95c-ac14-455f-9b7f-9770cf65e11e
1 parent 53b0fd1 commit 0b95ef8

5 files changed

Lines changed: 60 additions & 10 deletions

File tree

File renamed without changes.

src/common/utils/pathUtils.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,22 @@ export function isSameOrParentPath(parentPath: string, candidatePath: string): b
8787
);
8888
}
8989

90+
/**
91+
* Determines whether `value` maps to a reserved Windows device name (e.g. `CON`,
92+
* `PRN`, `AUX`, `NUL`, `COM1`-`COM9`, `LPT1`-`LPT9`).
93+
*
94+
* The check inspects the portion of the name before the first dot, since Windows
95+
* disallows these names regardless of extension, and only reports `true` on
96+
* Windows, where the restriction applies.
97+
*
98+
* @param value The base file name (without directory) to test.
99+
* @returns `true` on Windows when `value` resolves to a reserved device name.
100+
*/
101+
export function isWindowsReservedDeviceName(value: string): boolean {
102+
const deviceBaseName = value.split('.')[0];
103+
return isWindows() && /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i.test(deviceBaseName);
104+
}
105+
90106
export function getResourceUri(resourcePath: string, root?: string): Uri | undefined {
91107
try {
92108
if (!resourcePath) {

src/features/creators/newScriptProject.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@ import { commands, l10n, MarkdownString, QuickInputButtons, Uri, window, Workspa
44
import { PythonProject, PythonProjectCreator, PythonProjectCreatorOptions } from '../../api';
55
import { NEW_PROJECT_TEMPLATES_FOLDER } from '../../common/constants';
66
import { traceError } from '../../common/logging';
7-
import { isSameOrParentPath } from '../../common/utils/pathUtils';
8-
import { isWindows } from '../../common/utils/platformUtils';
7+
import { isSameOrParentPath, isWindowsReservedDeviceName } from '../../common/utils/pathUtils';
98
import { showErrorMessage, showInputBoxWithButtons, showTextDocument } from '../../common/window.apis';
109
import { getWorkspaceFolder, getWorkspaceFolders } from '../../common/workspace.apis';
1110
import { PythonProjectManager } from '../../internal.api';
@@ -20,8 +19,7 @@ function validateScriptFileName(value: string): string | null {
2019
return l10n.t('Script name must end with ".py".');
2120
}
2221
const baseName = value.replace(/\.py$/, '');
23-
const deviceBaseName = baseName.split('.')[0];
24-
if (isWindows() && /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i.test(deviceBaseName)) {
22+
if (isWindowsReservedDeviceName(baseName)) {
2523
return l10n.t('Script name uses a reserved Windows device name.');
2624
}
2725
// following PyPI (PEP 508) rules for package names
@@ -97,7 +95,7 @@ export class NewScriptProject implements PythonProjectCreator {
9795
}
9896

9997
// 1. Copy template file
100-
const newScriptTemplateFile = path.join(NEW_PROJECT_TEMPLATES_FOLDER, 'new723ScriptTemplate', 'script.py');
98+
const newScriptTemplateFile = path.join(NEW_PROJECT_TEMPLATES_FOLDER, 'newInlineScriptTemplate', 'script.py');
10199
if (!(await fs.pathExists(newScriptTemplateFile))) {
102100
window.showErrorMessage(l10n.t('Template file does not exist, aborting creation.'));
103101
traceError(`Template file not found at: ${newScriptTemplateFile}`);

src/test/common/pathUtils.unit.test.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import assert from 'node:assert';
22
import * as sinon from 'sinon';
33
import { Uri } from 'vscode';
4-
import { getResourceUri, normalizePath } from '../../common/utils/pathUtils';
4+
import { getResourceUri, isWindowsReservedDeviceName, normalizePath } from '../../common/utils/pathUtils';
55
import * as utils from '../../common/utils/platformUtils';
66

77
suite('Path Utilities', () => {
@@ -128,4 +128,40 @@ suite('Path Utilities', () => {
128128
assert.strictEqual(result, 'C:/Path/To/File.txt');
129129
});
130130
});
131+
132+
suite('isWindowsReservedDeviceName', () => {
133+
let isWindowsStub: sinon.SinonStub;
134+
135+
setup(() => {
136+
isWindowsStub = sinon.stub(utils, 'isWindows');
137+
});
138+
139+
teardown(() => {
140+
sinon.restore();
141+
});
142+
143+
test('flags reserved device names on Windows regardless of extension', () => {
144+
isWindowsStub.returns(true);
145+
146+
for (const name of ['CON', 'prn', 'aux', 'nul', 'com1', 'LPT9', 'con.py', 'nul.foo']) {
147+
assert.strictEqual(isWindowsReservedDeviceName(name), true, `${name} should be reserved`);
148+
}
149+
});
150+
151+
test('allows names that only resemble reserved device names on Windows', () => {
152+
isWindowsStub.returns(true);
153+
154+
for (const name of ['console', 'com10', 'lpt0', 'printer', 'aux_helper']) {
155+
assert.strictEqual(isWindowsReservedDeviceName(name), false, `${name} should be allowed`);
156+
}
157+
});
158+
159+
test('never flags reserved device names off Windows', () => {
160+
isWindowsStub.returns(false);
161+
162+
for (const name of ['CON', 'nul', 'com1', 'lpt9']) {
163+
assert.strictEqual(isWindowsReservedDeviceName(name), false, `${name} should be allowed off Windows`);
164+
}
165+
});
166+
});
131167
});

src/test/features/creators/newScriptProject.unit.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ const TEMPLATE_PATH = path.join(
2828
'..',
2929
'files',
3030
'templates',
31-
'new723ScriptTemplate',
31+
'newInlineScriptTemplate',
3232
'script.py',
3333
);
3434

@@ -40,7 +40,7 @@ function asRemoteUri(fsPath: string, authority = 'ssh-remote+test-host'): Uri {
4040
});
4141
}
4242

43-
suite('new723ScriptTemplate / NewScriptProject', () => {
43+
suite('newInlineScriptTemplate / NewScriptProject', () => {
4444
let tmpDir: string;
4545
let getWorkspaceFolderStub: sinon.SinonStub;
4646
let getWorkspaceFoldersStub: sinon.SinonStub;
@@ -64,7 +64,7 @@ suite('new723ScriptTemplate / NewScriptProject', () => {
6464

6565
function stubSuccessfulFileCreation() {
6666
const templateFile = path.resolve(
67-
path.join(NEW_PROJECT_TEMPLATES_FOLDER, 'new723ScriptTemplate', 'script.py'),
67+
path.join(NEW_PROJECT_TEMPLATES_FOLDER, 'newInlineScriptTemplate', 'script.py'),
6868
);
6969
const showTextDocumentStub = sinon
7070
.stub(windowApis, 'showTextDocument')
@@ -250,7 +250,7 @@ suite('new723ScriptTemplate / NewScriptProject', () => {
250250
const scriptDestination = path.resolve(rootUri.fsPath, scriptFileName);
251251
const expectedTemplatePath = path.join(
252252
NEW_PROJECT_TEMPLATES_FOLDER,
253-
'new723ScriptTemplate',
253+
'newInlineScriptTemplate',
254254
'script.py',
255255
);
256256
const addStub = sinon.stub().resolves();

0 commit comments

Comments
 (0)