Repository: microsoft/vscode (TypeScript)
Date: 2021-07-13 19:50:37+00:00
Task type: Bug Fix

Issue:
All time-based snippets variables should resolve using the same time
Issue Type: <b>Bug</b>
Does this issue occur when all extensions are disabled?: Yes

---

Snippets with multiple references to time-based snippet variables (eg. `$CURRENT_YEAR`, `$CURRENT_MINUTE`, `$CURRENT_SECOND`) are not guaranteed to resolve to the same values due to the passage of time during the resolving process.

---

1. Create a snippet with with multiple references to `$CURRENT_SECOND`

```jsonc
  "Print to console": {
    "scope": "javascript,typescript",
    "prefix": "second",
    "body": [
      "$CURRENT_SECOND",
      "$UUID",
      // Repeated "$UUID" lines 15,000 times. Not necessary, but helps increas odds of reproducing
      // ...
      "$UUID",
      "$CURRENT_SECOND",
    ],
    "description": "Show that time-based snippet variables can change during resolution."
  }
```

2. Use the snippet.

There is a chance that the first `$CURRENT_SECOND` will resolve to a different value than the second `$CURRENT_SECOND`.

I would expect that VSCode would save the time at the start of the resolution process and use it for the resolution of all time-based snippet variables.

This is a time-based issue, so it helps to slow your CPU with heavy load.
For example, I ran across this issue while running Folding@Home on my machine.

---

I'll submit a PR momentarily to fix this.

---

VS Code version: Code - Insiders 1.59.0-insider (Universal) (807dfb817fef0d0c5729c1924bd4889b04fa405b, 2021-07-13T09:15:26.467Z)
OS version: Darwin x64 20.5.0
Restricted Mode: Yes

<details>
<summary>System Info</summary>

|Item|Value|
|---|---|
|CPUs|Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz (12 x 2200)|
|GPU Status|2d_canvas: enabled<br>gpu_compositing: enabled<br>metal: disabled_off<br>multiple_raster_threads: enabled_on<br>oop_rasterization: enabled<br>opengl: enabled_on<br>rasterization: enabled<br>skia_renderer: disabled_off_ok<br>video_decode: enabled<br>webgl: enabled<br>webgl2: enabled|
|Load (avg)|10, 6, 8|
|Memory (System)|16.00GB (0.60GB free)|
|Process Argv|--crash-reporter-id 8e80114c-46de-44c7-87ff-0e5b2885669a|
|Screen Reader|no|
|VM|0%|
</details>Extensions: none<details>
<summary>A/B Experiments</summary>

```
vsliv695:30137379
vsins829:30139715
vsliv368cf:30146710
vsreu685:30147344
python383cf:30185419
pythonvspyt602:30291494
vspor879:30202332
vspor708:30202333
vspor363:30204092
pythonvspyt639:30291487
pythontb:30258533
pythonvspyt551:30291412
vspre833:30321513
pythonptprofiler:30281269
vshan820:30294714
pythondataviewer:30285072
vscus158:30321503
pythonvsuse255:30319630
vscod805cf:30301675
pythonvspyt200:30323110
vscextlangct:30310089
vsccppwtct:30312693
bridge0708:30335490

```

</details>

<!-- generated by issue reporter -->


Issue discussion:

Solution patch:
diff --git a/src/vs/editor/contrib/snippet/snippetVariables.ts b/src/vs/editor/contrib/snippet/snippetVariables.ts
--- a/src/vs/editor/contrib/snippet/snippetVariables.ts
+++ b/src/vs/editor/contrib/snippet/snippetVariables.ts
@@ -255,33 +255,37 @@ export class TimeBasedVariableResolver implements VariableResolver {
 	private static readonly monthNames = [nls.localize('January', "January"), nls.localize('February', "February"), nls.localize('March', "March"), nls.localize('April', "April"), nls.localize('May', "May"), nls.localize('June', "June"), nls.localize('July', "July"), nls.localize('August', "August"), nls.localize('September', "September"), nls.localize('October', "October"), nls.localize('November', "November"), nls.localize('December', "December")];
 	private static readonly monthNamesShort = [nls.localize('JanuaryShort', "Jan"), nls.localize('FebruaryShort', "Feb"), nls.localize('MarchShort', "Mar"), nls.localize('AprilShort', "Apr"), nls.localize('MayShort', "May"), nls.localize('JuneShort', "Jun"), nls.localize('JulyShort', "Jul"), nls.localize('AugustShort', "Aug"), nls.localize('SeptemberShort', "Sep"), nls.localize('OctoberShort', "Oct"), nls.localize('NovemberShort', "Nov"), nls.localize('DecemberShort', "Dec")];
 
+	constructor(private readonly _date: Date = new Date()) {
+		//
+	}
+
 	resolve(variable: Variable): string | undefined {
 		const { name } = variable;
 
 		if (name === 'CURRENT_YEAR') {
-			return String(new Date().getFullYear());
+			return String(this._date.getFullYear());
 		} else if (name === 'CURRENT_YEAR_SHORT') {
-			return String(new Date().getFullYear()).slice(-2);
+			return String(this._date.getFullYear()).slice(-2);
 		} else if (name === 'CURRENT_MONTH') {
-			return String(new Date().getMonth().valueOf() + 1).padStart(2, '0');
+			return String(this._date.getMonth().valueOf() + 1).padStart(2, '0');
 		} else if (name === 'CURRENT_DATE') {
-			return String(new Date().getDate().valueOf()).padStart(2, '0');
+			return String(this._date.getDate().valueOf()).padStart(2, '0');
 		} else if (name === 'CURRENT_HOUR') {
-			return String(new Date().getHours().valueOf()).padStart(2, '0');
+			return String(this._date.getHours().valueOf()).padStart(2, '0');
 		} else if (name === 'CURRENT_MINUTE') {
-			return String(new Date().getMinutes().valueOf()).padStart(2, '0');
+			return String(this._date.getMinutes().valueOf()).padStart(2, '0');
 		} else if (name === 'CURRENT_SECOND') {
-			return String(new Date().getSeconds().valueOf()).padStart(2, '0');
+			return String(this._date.getSeconds().valueOf()).padStart(2, '0');
 		} else if (name === 'CURRENT_DAY_NAME') {
-			return TimeBasedVariableResolver.dayNames[new Date().getDay()];
+			return TimeBasedVariableResolver.dayNames[this._date.getDay()];
 		} else if (name === 'CURRENT_DAY_NAME_SHORT') {
-			return TimeBasedVariableResolver.dayNamesShort[new Date().getDay()];
+			return TimeBasedVariableResolver.dayNamesShort[this._date.getDay()];
 		} else if (name === 'CURRENT_MONTH_NAME') {
-			return TimeBasedVariableResolver.monthNames[new Date().getMonth()];
+			return TimeBasedVariableResolver.monthNames[this._date.getMonth()];
 		} else if (name === 'CURRENT_MONTH_NAME_SHORT') {
-			return TimeBasedVariableResolver.monthNamesShort[new Date().getMonth()];
+			return TimeBasedVariableResolver.monthNamesShort[this._date.getMonth()];
 		} else if (name === 'CURRENT_SECONDS_UNIX') {
-			return String(Math.floor(Date.now() / 1000));
+			return String(Math.floor(this._date.getTime() / 1000));
 		}
 
 		return undefined;


Test patch:
diff --git a/src/vs/editor/contrib/snippet/test/snippetVariables.test.ts b/src/vs/editor/contrib/snippet/test/snippetVariables.test.ts
--- a/src/vs/editor/contrib/snippet/test/snippetVariables.test.ts
+++ b/src/vs/editor/contrib/snippet/test/snippetVariables.test.ts
@@ -17,6 +17,7 @@ import { Workspace } from 'vs/platform/workspace/test/common/testWorkspace';
 import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources';
 import { sep } from 'vs/base/common/path';
 import { toWorkspaceFolders } from 'vs/platform/workspaces/common/workspaces';
+import * as sinon from 'sinon';
 
 suite('Snippet Variables Resolver', function () {
 
@@ -291,6 +292,36 @@ suite('Snippet Variables Resolver', function () {
 		assertVariableResolve3(resolver, 'CURRENT_SECONDS_UNIX');
 	});
 
+	test('Time-based snippet variables resolve to the same values even as time progresses', async function () {
+		const snippetText = `
+			$CURRENT_YEAR
+			$CURRENT_YEAR_SHORT
+			$CURRENT_MONTH
+			$CURRENT_DATE
+			$CURRENT_HOUR
+			$CURRENT_MINUTE
+			$CURRENT_SECOND
+			$CURRENT_DAY_NAME
+			$CURRENT_DAY_NAME_SHORT
+			$CURRENT_MONTH_NAME
+			$CURRENT_MONTH_NAME_SHORT
+			$CURRENT_SECONDS_UNIX
+		`;
+
+		const clock = sinon.useFakeTimers();
+		try {
+			const resolver = new TimeBasedVariableResolver;
+
+			const firstResolve = new SnippetParser().parse(snippetText).resolveVariables(resolver);
+			clock.tick((365 * 24 * 3600 * 1000) + (24 * 3600 * 1000) + (3661 * 1000));  // 1 year + 1 day + 1 hour + 1 minute + 1 second
+			const secondResolve = new SnippetParser().parse(snippetText).resolveVariables(resolver);
+
+			assert.strictEqual(firstResolve.toString(), secondResolve.toString(), `Time-based snippet variables resolved differently`);
+		} finally {
+			clock.restore();
+		}
+	});
+
 	test('creating snippet - format-condition doesn\'t work #53617', function () {
 
 		const snippet = new SnippetParser().parse('${TM_LINE_NUMBER/(10)/${1:?It is:It is not}/} line 10', true);