Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/build/hygiene.ts
4770 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import cp from 'child_process';
7
import es from 'event-stream';
8
import fs from 'fs';
9
import filter from 'gulp-filter';
10
import pall from 'p-all';
11
import path from 'path';
12
import VinylFile from 'vinyl';
13
import vfs from 'vinyl-fs';
14
import { all, copyrightFilter, eslintFilter, indentationFilter, stylelintFilter, tsFormattingFilter, unicodeFilter } from './filters.ts';
15
import eslint from './gulp-eslint.ts';
16
import * as formatter from './lib/formatter.ts';
17
import gulpstylelint from './stylelint.ts';
18
19
const copyrightHeaderLines = [
20
'/*---------------------------------------------------------------------------------------------',
21
' * Copyright (c) Microsoft Corporation. All rights reserved.',
22
' * Licensed under the MIT License. See License.txt in the project root for license information.',
23
' *--------------------------------------------------------------------------------------------*/',
24
];
25
26
interface VinylFileWithLines extends VinylFile {
27
__lines: string[];
28
}
29
30
/**
31
* Main hygiene function that runs checks on files
32
*/
33
export function hygiene(some: NodeJS.ReadWriteStream | string[] | undefined, runEslint = true): NodeJS.ReadWriteStream {
34
console.log('Starting hygiene...');
35
let errorCount = 0;
36
37
const productJson = es.through(function (file: VinylFile) {
38
const product = JSON.parse(file.contents!.toString('utf8'));
39
40
if (product.extensionsGallery) {
41
console.error(`product.json: Contains 'extensionsGallery'`);
42
errorCount++;
43
}
44
45
this.emit('data', file);
46
});
47
48
const unicode = es.through(function (file: VinylFileWithLines) {
49
const lines = file.contents!.toString('utf8').split(/\r\n|\r|\n/);
50
file.__lines = lines;
51
const allowInComments = lines.some(line => /allow-any-unicode-comment-file/.test(line));
52
let skipNext = false;
53
lines.forEach((line, i) => {
54
if (/allow-any-unicode-next-line/.test(line)) {
55
skipNext = true;
56
return;
57
}
58
if (skipNext) {
59
skipNext = false;
60
return;
61
}
62
// If unicode is allowed in comments, trim the comment from the line
63
if (allowInComments) {
64
if (line.match(/\s+(\*)/)) { // Naive multi-line comment check
65
line = '';
66
} else {
67
const index = line.indexOf('//');
68
line = index === -1 ? line : line.substring(0, index);
69
}
70
}
71
// Please do not add symbols that resemble ASCII letters!
72
// eslint-disable-next-line no-misleading-character-class
73
const m = /([^\t\n\r\x20-\x7E⊃⊇✔︎✓🎯🧪✍️⚠️🛑🔴🚗🚙🚕🎉✨❗⇧⌥⌘×÷¦⋯…↑↓→→←↔⟷·•●◆▼⟪⟫┌└├⏎↩√φ]+)/g.exec(line);
74
if (m) {
75
console.error(
76
file.relative + `(${i + 1},${m.index + 1}): Unexpected unicode character: "${m[0]}" (charCode: ${m[0].charCodeAt(0)}). To suppress, use // allow-any-unicode-next-line`
77
);
78
errorCount++;
79
}
80
});
81
82
this.emit('data', file);
83
});
84
85
const indentation = es.through(function (file: VinylFileWithLines) {
86
const lines = file.__lines || file.contents!.toString('utf8').split(/\r\n|\r|\n/);
87
file.__lines = lines;
88
89
lines.forEach((line, i) => {
90
if (/^\s*$/.test(line)) {
91
// empty or whitespace lines are OK
92
} else if (/^[\t]*[^\s]/.test(line)) {
93
// good indent
94
} else if (/^[\t]* \*/.test(line)) {
95
// block comment using an extra space
96
} else {
97
console.error(
98
file.relative + '(' + (i + 1) + ',1): Bad whitespace indentation'
99
);
100
errorCount++;
101
}
102
});
103
104
this.emit('data', file);
105
});
106
107
const copyrights = es.through(function (file: VinylFileWithLines) {
108
const lines = file.__lines;
109
110
for (let i = 0; i < copyrightHeaderLines.length; i++) {
111
if (lines[i] !== copyrightHeaderLines[i]) {
112
console.error(file.relative + ': Missing or bad copyright statement');
113
errorCount++;
114
break;
115
}
116
}
117
118
this.emit('data', file);
119
});
120
121
const formatting = es.map(function (file: any, cb) {
122
try {
123
const rawInput = file.contents!.toString('utf8');
124
const rawOutput = formatter.format(file.path, rawInput);
125
126
const original = rawInput.replace(/\r\n/gm, '\n');
127
const formatted = rawOutput.replace(/\r\n/gm, '\n');
128
if (original !== formatted) {
129
console.error(
130
`File not formatted. Run the 'Format Document' command to fix it:`,
131
file.relative
132
);
133
errorCount++;
134
}
135
cb(undefined, file);
136
} catch (err) {
137
cb(err);
138
}
139
});
140
141
let input: NodeJS.ReadWriteStream;
142
if (Array.isArray(some) || typeof some === 'string' || !some) {
143
const options = { base: '.', follow: true, allowEmpty: true };
144
if (some) {
145
input = vfs.src(some, options).pipe(filter(Array.from(all))); // split this up to not unnecessarily filter all a second time
146
} else {
147
input = vfs.src(Array.from(all), options);
148
}
149
} else {
150
input = some;
151
}
152
153
const productJsonFilter = filter('product.json', { restore: true });
154
const snapshotFilter = filter(['**', '!**/*.snap', '!**/*.snap.actual']);
155
const yarnLockFilter = filter(['**', '!**/yarn.lock']);
156
const unicodeFilterStream = filter(Array.from(unicodeFilter), { restore: true });
157
158
const result = input
159
.pipe(filter((f) => Boolean(f.stat && !f.stat.isDirectory())))
160
.pipe(snapshotFilter)
161
.pipe(yarnLockFilter)
162
.pipe(productJsonFilter)
163
.pipe(process.env['BUILD_SOURCEVERSION'] ? es.through() : productJson)
164
.pipe(productJsonFilter.restore)
165
.pipe(unicodeFilterStream)
166
.pipe(unicode)
167
.pipe(unicodeFilterStream.restore)
168
.pipe(filter(Array.from(indentationFilter)))
169
.pipe(indentation)
170
.pipe(filter(Array.from(copyrightFilter)))
171
.pipe(copyrights);
172
173
const streams: NodeJS.ReadWriteStream[] = [
174
result.pipe(filter(Array.from(tsFormattingFilter))).pipe(formatting)
175
];
176
177
if (runEslint) {
178
streams.push(
179
result
180
.pipe(filter(Array.from(eslintFilter)))
181
.pipe(
182
eslint((results) => {
183
errorCount += results.warningCount;
184
errorCount += results.errorCount;
185
})
186
)
187
);
188
}
189
190
streams.push(
191
result.pipe(filter(Array.from(stylelintFilter))).pipe(gulpstylelint(((message: string, isError: boolean) => {
192
if (isError) {
193
console.error(message);
194
errorCount++;
195
} else {
196
console.warn(message);
197
}
198
})))
199
);
200
201
let count = 0;
202
return es.merge(...streams).pipe(
203
es.through(
204
function (data: unknown) {
205
count++;
206
if (process.env['TRAVIS'] && count % 10 === 0) {
207
process.stdout.write('.');
208
}
209
this.emit('data', data);
210
},
211
function () {
212
process.stdout.write('\n');
213
if (errorCount > 0) {
214
this.emit(
215
'error',
216
'Hygiene failed with ' +
217
errorCount +
218
` errors. Check 'build / gulpfile.hygiene.js'.`
219
);
220
} else {
221
this.emit('end');
222
}
223
}
224
)
225
);
226
}
227
228
function createGitIndexVinyls(paths: string[]): Promise<VinylFile[]> {
229
const repositoryPath = process.cwd();
230
231
const fns = paths.map((relativePath) => () =>
232
new Promise<VinylFile | null>((c, e) => {
233
const fullPath = path.join(repositoryPath, relativePath);
234
235
fs.stat(fullPath, (err, stat) => {
236
if (err && err.code === 'ENOENT') {
237
// ignore deletions
238
return c(null);
239
} else if (err) {
240
return e(err);
241
}
242
243
cp.exec(
244
process.platform === 'win32' ? `git show :${relativePath}` : `git show ':${relativePath}'`,
245
{ maxBuffer: stat.size, encoding: 'buffer' },
246
(err, out) => {
247
if (err) {
248
return e(err);
249
}
250
251
c(new VinylFile({
252
path: fullPath,
253
base: repositoryPath,
254
contents: out,
255
stat: stat,
256
}));
257
}
258
);
259
});
260
})
261
);
262
263
return pall(fns, { concurrency: 4 }).then((r) => r.filter((p): p is VinylFile => !!p));
264
}
265
266
// this allows us to run hygiene as a git pre-commit hook
267
if (import.meta.main) {
268
process.on('unhandledRejection', (reason: unknown, p: Promise<any>) => {
269
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
270
process.exit(1);
271
});
272
273
if (process.argv.length > 2) {
274
hygiene(process.argv.slice(2)).on('error', (err: Error) => {
275
console.error();
276
console.error(err);
277
process.exit(1);
278
});
279
} else {
280
cp.exec(
281
'git diff --cached --name-only',
282
{ maxBuffer: 2000 * 1024 },
283
(err, out) => {
284
if (err) {
285
console.error();
286
console.error(err);
287
process.exit(1);
288
}
289
290
const some = out.split(/\r?\n/).filter((l) => !!l);
291
292
if (some.length > 0) {
293
console.log('Reading git index versions...');
294
295
createGitIndexVinyls(some)
296
.then(
297
(vinyls) => {
298
return new Promise<void>((c, e) =>
299
hygiene(es.readArray(vinyls).pipe(filter(Array.from(all))))
300
.on('end', () => c())
301
.on('error', e)
302
);
303
}
304
)
305
.catch((err: Error) => {
306
console.error();
307
console.error(err);
308
process.exit(1);
309
});
310
}
311
}
312
);
313
}
314
}
315
316