Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/build/hygiene.js
3520 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
// @ts-check
6
7
const filter = require('gulp-filter');
8
const es = require('event-stream');
9
const VinylFile = require('vinyl');
10
const vfs = require('vinyl-fs');
11
const path = require('path');
12
const fs = require('fs');
13
const pall = require('p-all');
14
15
const { all, copyrightFilter, unicodeFilter, indentationFilter, tsFormattingFilter, eslintFilter, stylelintFilter } = require('./filters');
16
17
const copyrightHeaderLines = [
18
'/*---------------------------------------------------------------------------------------------',
19
' * Copyright (c) Microsoft Corporation. All rights reserved.',
20
' * Licensed under the MIT License. See License.txt in the project root for license information.',
21
' *--------------------------------------------------------------------------------------------*/',
22
];
23
24
/**
25
* @param {string[] | NodeJS.ReadWriteStream} some
26
* @param {boolean} linting
27
*/
28
function hygiene(some, linting = true) {
29
const eslint = require('./gulp-eslint');
30
const gulpstylelint = require('./stylelint');
31
const formatter = require('./lib/formatter');
32
33
let errorCount = 0;
34
35
const productJson = es.through(function (file) {
36
const product = JSON.parse(file.contents.toString('utf8'));
37
38
if (product.extensionsGallery) {
39
console.error(`product.json: Contains 'extensionsGallery'`);
40
errorCount++;
41
}
42
43
this.emit('data', file);
44
});
45
46
const unicode = es.through(function (file) {
47
/** @type {string[]} */
48
const lines = file.contents.toString('utf8').split(/\r\n|\r|\n/);
49
file.__lines = lines;
50
const allowInComments = lines.some(line => /allow-any-unicode-comment-file/.test(line));
51
let skipNext = false;
52
lines.forEach((line, i) => {
53
if (/allow-any-unicode-next-line/.test(line)) {
54
skipNext = true;
55
return;
56
}
57
if (skipNext) {
58
skipNext = false;
59
return;
60
}
61
// If unicode is allowed in comments, trim the comment from the line
62
if (allowInComments) {
63
if (line.match(/\s+(\*)/)) { // Naive multi-line comment check
64
line = '';
65
} else {
66
const index = line.indexOf('\/\/');
67
line = index === -1 ? line : line.substring(0, index);
68
}
69
}
70
// Please do not add symbols that resemble ASCII letters!
71
// eslint-disable-next-line no-misleading-character-class
72
const m = /([^\t\n\r\x20-\x7E⊃⊇✔︎✓🎯🧪✍️⚠️🛑🔴🚗🚙🚕🎉✨❗⇧⌥⌘×÷¦⋯…↑↓→→←↔⟷·•●◆▼⟪⟫┌└├⏎↩√φ]+)/g.exec(line);
73
if (m) {
74
console.error(
75
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`
76
);
77
errorCount++;
78
}
79
});
80
81
this.emit('data', file);
82
});
83
84
const indentation = es.through(function (file) {
85
/** @type {string[]} */
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) {
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, 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;
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(all)); // split this up to not unnecessarily filter all a second time
146
} else {
147
input = vfs.src(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(unicodeFilter, { restore: true });
157
158
const result = input
159
.pipe(filter((f) => !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(indentationFilter))
169
.pipe(indentation)
170
.pipe(filter(copyrightFilter))
171
.pipe(copyrights);
172
173
/** @type {import('stream').Stream[]} */
174
const streams = [
175
result.pipe(filter(tsFormattingFilter)).pipe(formatting)
176
];
177
178
if (linting) {
179
streams.push(
180
result
181
.pipe(filter(eslintFilter))
182
.pipe(
183
eslint((results) => {
184
errorCount += results.warningCount;
185
errorCount += results.errorCount;
186
})
187
)
188
);
189
streams.push(
190
result.pipe(filter(stylelintFilter)).pipe(gulpstylelint(((message, isError) => {
191
if (isError) {
192
console.error(message);
193
errorCount++;
194
} else {
195
console.warn(message);
196
}
197
})))
198
);
199
}
200
201
let count = 0;
202
return es.merge(...streams).pipe(
203
es.through(
204
function (data) {
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
module.exports.hygiene = hygiene;
229
230
/**
231
* @param {string[]} paths
232
*/
233
function createGitIndexVinyls(paths) {
234
const cp = require('child_process');
235
const repositoryPath = process.cwd();
236
237
const fns = paths.map((relativePath) => () =>
238
new Promise((c, e) => {
239
const fullPath = path.join(repositoryPath, relativePath);
240
241
fs.stat(fullPath, (err, stat) => {
242
if (err && err.code === 'ENOENT') {
243
// ignore deletions
244
return c(null);
245
} else if (err) {
246
return e(err);
247
}
248
249
cp.exec(
250
process.platform === 'win32' ? `git show :${relativePath}` : `git show ':${relativePath}'`,
251
{ maxBuffer: stat.size, encoding: 'buffer' },
252
(err, out) => {
253
if (err) {
254
return e(err);
255
}
256
257
c(
258
new VinylFile({
259
path: fullPath,
260
base: repositoryPath,
261
contents: out,
262
stat,
263
})
264
);
265
}
266
);
267
});
268
})
269
);
270
271
return pall(fns, { concurrency: 4 }).then((r) => r.filter((p) => !!p));
272
}
273
274
// this allows us to run hygiene as a git pre-commit hook
275
if (require.main === module) {
276
const cp = require('child_process');
277
278
process.on('unhandledRejection', (reason, p) => {
279
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
280
process.exit(1);
281
});
282
283
if (process.argv.length > 2) {
284
hygiene(process.argv.slice(2)).on('error', (err) => {
285
console.error();
286
console.error(err);
287
process.exit(1);
288
});
289
} else {
290
cp.exec(
291
'git diff --cached --name-only',
292
{ maxBuffer: 2000 * 1024 },
293
(err, out) => {
294
if (err) {
295
console.error();
296
console.error(err);
297
process.exit(1);
298
}
299
300
const some = out.split(/\r?\n/).filter((l) => !!l);
301
302
if (some.length > 0) {
303
console.log('Reading git index versions...');
304
305
createGitIndexVinyls(some)
306
.then(
307
(vinyls) => {
308
/** @type {Promise<void>} */
309
return (new Promise((c, e) =>
310
hygiene(es.readArray(vinyls).pipe(filter(all)))
311
.on('end', () => c())
312
.on('error', e)
313
))
314
}
315
)
316
.catch((err) => {
317
console.error();
318
console.error(err);
319
process.exit(1);
320
});
321
}
322
}
323
);
324
}
325
}
326
327