Skip to content

Commit ff60b40

Browse files
Add generic utils: DeepReadonly<T> and VersionInfo. (#13591)
This will be used in upcoming locator-related changes.
1 parent e7d7fdc commit ff60b40

File tree

4 files changed

+656
-2
lines changed

4 files changed

+656
-2
lines changed

src/client/common/utils/misc.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,20 @@ export async function usingAsync<T extends IAsyncDisposable, R>(
4141
}
4242
}
4343

44+
/**
45+
* Like `Readonly<>`, but recursive.
46+
*
47+
* See https://github.com/Microsoft/TypeScript/pull/21316.
48+
*/
49+
// tslint:disable-next-line:no-any
50+
export type DeepReadonly<T> = T extends any[] ? IDeepReadonlyArray<T[number]> : DeepReadonlyNonArray<T>;
51+
type DeepReadonlyNonArray<T> = T extends object ? DeepReadonlyObject<T> : T;
52+
interface IDeepReadonlyArray<T> extends ReadonlyArray<DeepReadonly<T>> {}
53+
type DeepReadonlyObject<T> = {
54+
readonly [P in NonFunctionPropertyNames<T>]: DeepReadonly<T[P]>;
55+
};
56+
type NonFunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? never : K }[keyof T];
57+
4458
// Information about a traced function/method call.
4559
export type TraceInfo = {
4660
elapsed: number; // milliseconds

src/client/common/utils/regexp.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,13 @@
1515
* indicated by "\s". Also, unlike with regular expression literals,
1616
* backslashes must be escaped. Conversely, forward slashes do not
1717
* need to be escaped.
18+
*
19+
* Line comments are also removed. A comment is two spaces followed
20+
* by `#` followed by a space and then the rest of the text to the
21+
* end of the line.
1822
*/
19-
export function verboseRegExp(pattern: string): RegExp {
23+
export function verboseRegExp(pattern: string, flags?: string): RegExp {
24+
pattern = pattern.replace(/(^| {2})# .*$/gm, '');
2025
pattern = pattern.replace(/\s+?/g, '');
21-
return RegExp(pattern);
26+
return RegExp(pattern, flags);
2227
}

src/client/common/utils/version.ts

Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,292 @@
33

44
'use strict';
55

6+
// tslint:disable:no-multiline-string
7+
68
import * as semver from 'semver';
9+
import { verboseRegExp } from './regexp';
10+
11+
//===========================
12+
// basic version info
13+
14+
/**
15+
* basic version information
16+
*
17+
* A normalized object will only have non-negative numbers, or `-1`,
18+
* in its properties. A `-1` value is an indicator that the property
19+
* is not set. Lower properties will not be set if a higher property
20+
* is not.
21+
*
22+
* Note that any object can be forced to look like a VersionInfo and
23+
* any of the properties may be forced to hold a non-number value.
24+
* To resolve this situation, pass the object through
25+
* `normalizeVersionInfo()` and then `validateVersionInfo()`.
26+
*/
27+
export type BasicVersionInfo = {
28+
major: number;
29+
minor: number;
30+
micro: number;
31+
// There is also a hidden `unnormalized` property.
32+
};
33+
34+
type ErrorMsg = string;
35+
36+
function normalizeVersionPart(part: unknown): [number, ErrorMsg] {
37+
// Any -1 values where the original is not a number are handled in validation.
38+
if (typeof part === 'number') {
39+
if (isNaN(part)) {
40+
return [-1, 'missing'];
41+
}
42+
if (part < 0) {
43+
// We leave this as a marker.
44+
return [-1, ''];
45+
}
46+
return [part, ''];
47+
}
48+
if (typeof part === 'string') {
49+
const parsed = parseInt(part, 10);
50+
if (isNaN(parsed)) {
51+
return [-1, 'string not numeric'];
52+
}
53+
if (parsed < 0) {
54+
return [-1, ''];
55+
}
56+
return [parsed, ''];
57+
}
58+
if (part === undefined || part === null) {
59+
return [-1, 'missing'];
60+
}
61+
return [-1, 'unsupported type'];
62+
}
63+
64+
type RawBasicVersionInfo = BasicVersionInfo & {
65+
unnormalized?: {
66+
major?: ErrorMsg;
67+
minor?: ErrorMsg;
68+
micro?: ErrorMsg;
69+
};
70+
};
71+
72+
export const EMPTY_VERSION: RawBasicVersionInfo = {
73+
major: -1,
74+
minor: -1,
75+
micro: -1,
76+
unnormalized: {
77+
major: undefined,
78+
minor: undefined,
79+
micro: undefined
80+
}
81+
};
82+
83+
/**
84+
* Make a copy and set all the properties properly.
85+
*
86+
* Only the "basic" version info will be normalized. The caller
87+
* is responsible for any other properties beyond that.
88+
*/
89+
export function normalizeBasicVersionInfo<T extends BasicVersionInfo>(info: T | undefined): T {
90+
if (!info) {
91+
return EMPTY_VERSION as T;
92+
}
93+
const norm: T = { ...info };
94+
const raw = (norm as unknown) as RawBasicVersionInfo;
95+
// Do not normalize if it has already been normalized.
96+
if (raw.unnormalized === undefined) {
97+
raw.unnormalized = {};
98+
[norm.major, raw.unnormalized.major] = normalizeVersionPart(norm.major);
99+
[norm.minor, raw.unnormalized.minor] = normalizeVersionPart(norm.minor);
100+
[norm.micro, raw.unnormalized.micro] = normalizeVersionPart(norm.micro);
101+
}
102+
return norm;
103+
}
104+
105+
function validateVersionPart(prop: string, part: number, unnormalized?: ErrorMsg) {
106+
// We expect a normalized version part here, so there's no need
107+
// to check for NaN or non-numbers here.
108+
if (part === 0 || part > 0) {
109+
return;
110+
}
111+
if (!unnormalized || unnormalized === '') {
112+
return;
113+
}
114+
throw Error(`invalid ${prop} version (failed to normalize; ${unnormalized})`);
115+
}
116+
117+
/**
118+
* Fail if any properties are not set properly.
119+
*
120+
* The info is expected to be normalized already.
121+
*
122+
* Only the "basic" version info will be validated. The caller
123+
* is responsible for any other properties beyond that.
124+
*/
125+
export function validateBasicVersionInfo<T extends BasicVersionInfo>(info: T) {
126+
const raw = (info as unknown) as RawBasicVersionInfo;
127+
validateVersionPart('major', info.major, raw.unnormalized?.major);
128+
validateVersionPart('minor', info.minor, raw.unnormalized?.minor);
129+
validateVersionPart('micro', info.micro, raw.unnormalized?.micro);
130+
if (info.major < 0) {
131+
throw Error('missing major version');
132+
}
133+
if (info.minor < 0) {
134+
if (info.micro === 0 || info.micro > 0) {
135+
throw Error('missing minor version');
136+
}
137+
}
138+
}
139+
140+
/**
141+
* Convert the info to a simple string.
142+
*
143+
* Any negative parts are ignored.
144+
*
145+
* The object is expected to be normalized.
146+
*/
147+
export function getVersionString<T extends BasicVersionInfo>(info: T): string {
148+
if (info.major < 0) {
149+
return '';
150+
} else if (info.minor < 0) {
151+
return `${info.major}`;
152+
} else if (info.micro < 0) {
153+
return `${info.major}.${info.minor}`;
154+
}
155+
return `${info.major}.${info.minor}.${info.micro}`;
156+
}
157+
158+
export type ParseResult<T extends BasicVersionInfo = BasicVersionInfo> = {
159+
version: T;
160+
before: string;
161+
after: string;
162+
};
163+
164+
const basicVersionPattern = `
165+
^
166+
(.*?) # <before>
167+
(\\d+) # <major>
168+
(?:
169+
[.]
170+
(\\d+) # <minor>
171+
(?:
172+
[.]
173+
(\\d+) # <micro>
174+
)?
175+
)?
176+
([^\\d].*)? # <after>
177+
$
178+
`;
179+
const basicVersionRegexp = verboseRegExp(basicVersionPattern, 's');
180+
181+
/**
182+
* Extract a version from the given text.
183+
*
184+
* If the version is surrounded by other text then that is provided
185+
* as well.
186+
*/
187+
export function parseBasicVersionInfo<T extends BasicVersionInfo>(verStr: string): ParseResult<T> | undefined {
188+
const match = verStr.match(basicVersionRegexp);
189+
if (!match) {
190+
return undefined;
191+
}
192+
// Ignore the first element (the full match).
193+
const [, before, majorStr, minorStr, microStr, after] = match;
194+
if (before && before.endsWith('.')) {
195+
return undefined;
196+
}
197+
198+
if (after && after !== '') {
199+
if (after === '.') {
200+
return undefined;
201+
}
202+
// Disallow a plain version with trailing text if it isn't complete
203+
if (!before || before === '') {
204+
if (!microStr || microStr === '') {
205+
return undefined;
206+
}
207+
}
208+
}
209+
const major = parseInt(majorStr, 10);
210+
const minor = minorStr ? parseInt(minorStr, 10) : -1;
211+
const micro = microStr ? parseInt(microStr, 10) : -1;
212+
return {
213+
// This is effectively normalized.
214+
version: ({ major, minor, micro } as unknown) as T,
215+
before: before || '',
216+
after: after || ''
217+
};
218+
}
219+
220+
/**
221+
* Returns true if the given version appears to be not set.
222+
*
223+
* The object is expected to already be normalized.
224+
*/
225+
export function isVersionInfoEmpty<T extends BasicVersionInfo>(info: T): boolean {
226+
if (!info) {
227+
return false;
228+
}
229+
if (typeof info.major !== 'number' || typeof info.minor !== 'number' || typeof info.micro !== 'number') {
230+
return false;
231+
}
232+
return info.major < 0 && info.minor < 0 && info.micro < 0;
233+
}
234+
235+
//===========================
236+
// base version info
237+
238+
/**
239+
* basic version information
240+
*
241+
* @prop raw - the unparsed version string, if any
242+
*/
243+
export type VersionInfo = BasicVersionInfo & {
244+
raw?: string;
245+
};
246+
247+
/**
248+
* Make a copy and set all the properties properly.
249+
*/
250+
export function normalizeVersionInfo<T extends VersionInfo>(info: T): T {
251+
const basic = normalizeBasicVersionInfo(info);
252+
if (!info) {
253+
basic.raw = '';
254+
return basic;
255+
}
256+
const norm = { ...info, ...basic };
257+
if (!norm.raw) {
258+
norm.raw = '';
259+
}
260+
return norm;
261+
}
262+
263+
/**
264+
* Fail if any properties are not set properly.
265+
*
266+
* Optional properties that are not set are ignored.
267+
*
268+
* This assumes that the info has already been normalized.
269+
*/
270+
export function validateVersionInfo<T extends VersionInfo>(info: T) {
271+
validateBasicVersionInfo(info);
272+
// `info.raw` can be anything.
273+
}
274+
275+
/**
276+
* Extract a version from the given text.
277+
*
278+
* If the version is surrounded by other text then that is provided
279+
* as well.
280+
*/
281+
export function parseVersionInfo<T extends VersionInfo>(verStr: string): ParseResult<T> | undefined {
282+
const result = parseBasicVersionInfo<T>(verStr);
283+
if (result === undefined) {
284+
return undefined;
285+
}
286+
result.version.raw = verStr;
287+
return result;
288+
}
289+
290+
//===========================
291+
// semver
7292

8293
export function parseVersion(raw: string): semver.SemVer {
9294
raw = raw.replace(/\.00*(?=[1-9]|0\.)/, '.');

0 commit comments

Comments
 (0)