Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(286)

Side by Side Diff: third_party/WebKit/LayoutTests/webaudio/tools/layout-test-tidy.js

Issue 2872393002: Add layout-test-tidy to LayoutTests/webaudio/tools (Closed)
Patch Set: Addressing feedback (2) Created 3 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 #!/usr/bin/env node
2
3 // Copyright 2017 The Chromium Authors. All rights reserved.
4 // Use of this source code is governed by a BSD-style license that can be
5 // found in the LICENSE file.
6
7 'use strict';
8
9 const os = require('os');
10 const fs = require('fs');
11 const path = require('path');
12 const glob = require('glob');
13 const libtidy = require('libtidy');
14 const cp = require('child_process');
15 const jsdom = require('jsdom');
16 const {JSDOM} = jsdom;
17
18
19 /**
20 * Options for sub modules.
21 */
22 const OPTIONS = {
23
24 HTMLTidy:
25 {'indent': 'yes', 'indent-spaces': '2', 'wrap': '80', 'tidy-mark': 'no'},
26
27 ClangFormat: ['-style=Chromium', '-assume-filename=a.js'],
28
29 // RegExp text swap collection (ordered key-value pair) for post-processing.
30 RegExpSwapCollection: [
31 // Replace |var| with |let|.
32 {regexp: /(\n\s{2,}|\()var /, replace: '$1let '},
33
34 // Move one line up the dangling closing script tags.
35 {regexp: /\>\n\s{2,}\<\/script\>\n/, replace: '></script>\n'},
36
37 // Remove all the empty lines in html.
38 {regexp: /\>\n{2,}/, replace: '>\n'}
39 ]
40 };
41
42
43 /**
44 * Basic utilities.
45 */
46 const Util = {
47
48 logAndExit: (moduleName, messageString) => {
49 console.error('[layout-test-tidy::' + moduleName + '] ' + messageString);
50 process.exit(1);
51 },
52
53 loadFileToStringSync: (filePath) => {
54 return fs.readFileSync(filePath, 'utf8').toString();
55 },
56
57 writeStringToFileSync: (pageString, filePath) => {
58 fs.writeFileSync(filePath, pageString);
59 }
60
61 };
62
63
64 /**
65 * Wrapper for external modules like HTMLTidy and clang format.
66 * @type {Object}
67 */
68 const Module = {
69
70 /**
71 * Perform a batch RegExp string substitution.
72 * @param {String} targetString Target string.
73 * @param {Array} swapCollection Array of key-value pairs. Each item is an
74 * object of { regexp_pattern: replace_string }.
75 * @return {String}
76 */
77 runRegExpSwapSync: (targetString, regExpSwapCollection) => {
78 let tempString = targetString;
79 regExpSwapCollection.forEach((item) => {
80 let re = new RegExp(item.regexp, 'g');
81 tempString = tempString.replace(re, item.replace);
82 });
83
84 return tempString;
85 },
86
87 /**
88 * Run HTMLTidy on input string with options.
89 * @param {String} pageString [description]
90 * @param {Object} options HTMLTidy option as key-value pair.
91 * @param {Task} task Associated Task object.
92 * @return {String}
93 */
94 runHTMLTidySync: (pageString, options, task) => {
95 let tidyDoc = new libtidy.TidyDoc();
96 for (let option in options)
97 tidyDoc.optSet(option, options[option]);
98
99 // This actually process the data inside of |tidyDoc|.
100 let logs = '';
101 logs += tidyDoc.parseBufferSync(Buffer(pageString));
102 logs += tidyDoc.cleanAndRepairSync();
103 logs += tidyDoc.runDiagnosticsSync();
104
105 task.addLog('Module.runHTMLTidySync', logs.split('\n'));
106
107 return tidyDoc.saveBufferSync().toString();
108 },
109
110 /**
111 * Run clang-format and return a promise.
112 * @param {String} codeString JS code to apply clang-format.
113 * @param {Array} clangFormatOption options array for clang-format.
114 * @param {Number} indentLevel Code indentation level.
115 * @param {Task} task Associated Task object.
116 * @return {Promise} Processed code as string.
117 * @resolve {String} clang-formatted JS code as string.
118 * @reject {Error}
119 */
120 runClangFormat: (codeString, clangFormatOption, indentLevel, task) => {
121 let clangFormatBinary = __dirname + '/node_modules/clang-format/bin/';
122 clangFormatBinary += (os.platform() === 'win32') ?
123 'win32/clang-format.exe' :
124 os.platform() + '_' + os.arch() + '/clang-format';
125
126 if (indentLevel > 0) {
127 codeString =
128 '{'.repeat(indentLevel) + codeString + '}'.repeat(indentLevel);
129 }
130
131 return new Promise((resolve, reject) => {
132 // Be sure to pipe the result to the child process, not to this process's
133 // stdout.
134 let result = '';
135 let clangFormat = cp.spawn(
136 clangFormatBinary, clangFormatOption,
137 {stdio: ['pipe', 'pipe', process.stderr]});
138
139 // Capture the data when it's arrived at the pipe.
140 clangFormat.stdout.on('data', (data) => {
141 result += data;
142 });
143
144 // For debug purpose:
145 // clangFormat.stdout.pipe(process.stdout);
146
147 clangFormat.stdout.on('close', (exitCode) => {
148 if (exitCode) {
149 Util.logAndExit('Module.runClangFormat', 'exit code = 1');
150 } else {
151 task.addLog('Module.runClangFormat', 'clang-format was successful.');
152
153 // Remove shim braces for indentation hack.
154 if (indentLevel > 0) {
155 let codeStart = 0;
156 let codeEnd = result.length - 1;
157 for (let i = 0; i < indentLevel; ++i) {
158 codeStart = result.indexOf('\n', codeStart + 1);
159 codeEnd = result.lastIndexOf('\n', codeEnd - 1);
160 }
161 result = result.substring(codeStart + 1, codeEnd);
162 }
163
164 resolve(result);
165 }
166 });
167
168 clangFormat.stdin.setEncoding('utf-8');
169 clangFormat.stdin.write(codeString);
170 clangFormat.stdin.end();
171 });
172 },
173
174 /**
175 * Detect line overflow and record the line number to the task log.
176 * @param {String} pageOrCodeString HTML page or JS code data in string.
177 * @param {TidyTask} task Associated TidyTask object.
178 */
179 detectLineOverflow: (pageOrCodeString, task) => {
180 let currentLineNumber = 0;
181 let index0 = 0;
182 let index1 = 0;
183 while (index0 < pageOrCodeString.length - 1) {
184 index1 = pageOrCodeString.indexOf('\n', index0);
185 if (index1 - index0 > 80) {
186 task.addLog(
187 'Module.detectLineOverflow',
188 'Overflow (> 80 cols.) at line ' + currentLineNumber + '.');
189 }
190 currentLineNumber++;
191 index0 = index1 + 1;
192 }
193 }
194
195 };
196
197
198 /**
199 * DOM utilities. Process DOM processing after parsing the string by JSDOM.
200 */
201 const DOMUtil = {
202
203 /**
204 * Parse string, generate JSDOM object and return |document| element.
205 * @param {String} pageString An HTML page in string.
206 * @return {Document} A |document| object.
207 */
208 getJSDOMFromStringSync: (pageString) => {
209 return new JSDOM(`${pageString}`);
210 // return jsdom_.window.document;
211 },
212
213 /**
214 * In-place tidy up head element.
215 * @param {Document} document A |document| object.
216 * @param {Task} task An associated Task object.
217 * @return {Void}
218 */
219 tidyHeadElementSync: (document, task) => {
220 try {
221 // If the title is missing, add one from the file name.
222 let titleElement = document.querySelector('title');
223 if (!titleElement) {
224 titleElement = document.createElement('title');
225 titleElement.textContent = path.basename(task.targetFilePath_);
226 task.addLog(
227 'DOMUtil.tidyHeadElementSync',
228 'Title element was missing thus a new one was added.');
229 }
230
231 // The title element should be the first.
232 let headElement = document.querySelector('head');
233 headElement.insertBefore(titleElement, headElement.firstChild);
234
235 // If a script element in body does not have JS code, move to the head
236 // section.
237 let scriptElementsInBody = document.body.querySelectorAll('script');
238 scriptElementsInBody.forEach((scriptElement) => {
239 if (!scriptElement.textContent)
240 headElement.appendChild(scriptElement);
241 });
242 } catch (error) {
243 task.addLog('DOMUtil.tidyHeadElementSync', error.toString());
244 }
245 },
246
247 /**
248 * Sanitize and extract |script| element with JS test code.
249 * @param {Document} document A |document| object.
250 * @param {Task} task An associated Task object.
251 * @return {ScriptElement}
252 */
253 getElementWithTestCodeSync: (document, task) => {
254 let numberOfScriptElementsWithCode = 0;
255 let scriptElementWithTestCode;
256 let scriptElements = document.querySelectorAll('script');
257
258 scriptElements.forEach((scriptElement) => {
259 // We don't want type attribute.
260 scriptElement.removeAttribute('type');
261
262 if (scriptElement.textContent.length > 0) {
263 ++numberOfScriptElementsWithCode;
264 scriptElementWithTestCode = scriptElement;
265 scriptElement.id = 'layout-test-code';
266 // If the element belongs to something else other than body, move it to
267 // the body. This fixes script elements that are located in weird
268 // positions. (e.g outside of body or head)
269 if (scriptElement.parentElement !== document.body)
270 document.body.appendChild(scriptElement);
271 }
272 });
273
274 if (numberOfScriptElementsWithCode !== 1) {
Raymond Toy 2017/05/12 16:19:01 I think I'd add a comment here that says it's perf
275 task.addLog(
276 'DOMUtil.getElementWithTestCodeSync',
277 numberOfScriptElementsWithCode + ' <script> element(s) with JS ' +
278 'code were found.');
279 scriptElementWithTestCode = null;
280 }
281
282 return scriptElementWithTestCode;
283 }
284
285 };
286
287
288 /**
289 * @class TidyTask
290 * @description Per-file processing task. This object should be constructed
291 * directly. The task runner creates this when it is necessary.
292 */
293 class TidyTask {
294 /**
295 * @param {String} targetFilePath A path to file to be processed.
296 * @param {Object} options Task options.
297 * @param {Boolean} options.inplace |true| for in-place processing directly
298 * writing into the target file. By default,
299 * this is |false| and the result is piped
300 * into the stdout.
301 * @param {Boolean} options.verbose Prints out warnings and logs from the
302 * process when |true|. |false| by default.
303 */
304 constructor(targetFilePath, options) {
305 this.targetFilePath_ = targetFilePath;
306 this.options_ = options;
307
308 this.fileType_ = path.extname(this.targetFilePath_);
309 this.pageString_ = Util.loadFileToStringSync(this.targetFilePath_);
310 this.jsdom_ = null;
311 this.logs_ = {};
312 }
313
314 /**
315 * Run processing sequence. Don't call this directly.
316 * @param {Function} taskDone Task runner callback function.
317 */
318 run(taskDone) {
319 switch (this.fileType_) {
320 case '.html':
321 this.processHTML_(taskDone);
322 break;
323 case '.js':
324 this.processJS_(taskDone);
325 break;
326 default:
327 Util.logAndExit(
328 'TidyTask.constructor', 'Invalid file type: ' + this.fileType_);
329 break;
330 }
331 }
332
333 /**
334 * Process HTML file. The processing performs the following in order:
335 * - DOM parsing to sanitize invalid/incorrect markup structure.
336 * - Extract JS code, apply clang-format and inject the code to element.
337 * - Apply HTMLTidy to the markup.
338 * - RegExp substitution.
339 * - Detect any line overflows 80 columns.
340 * @param {Function} taskDone completion callback.
341 */
342 processHTML_(taskDone) {
343 // Parse page string into JSDOM.element object.
344 this.jsdom_ = DOMUtil.getJSDOMFromStringSync(this.pageString_);
345
346 // Clean up the head element section.
347 DOMUtil.tidyHeadElementSync(this.jsdom_.window.document, this);
348
349 let scriptElement =
350 DOMUtil.getElementWithTestCodeSync(this.jsdom_.window.document, this);
351
352 if (!scriptElement)
353 Util.logAndExit('TidyTask.processHTML_', 'Invalid <script> element.');
354
355 // Start with clang-foramt, then HTMLTidy and RegExp substitution.
356 Module.runClangFormat(
357 scriptElement.textContent, OPTIONS.ClangFormat, 3, this)
358 .then((formattedCodeString) => {
359 // Replace the original code with clang-formatted code.
360 scriptElement.textContent = formattedCodeString;
361
362 // Then tidy the text data from JSDOM. After this point, DOM
363 // manipulation is not possible anymore.
364 let pageString = this.jsdom_.serialize();
365 pageString =
366 Module.runHTMLTidySync(pageString, OPTIONS.HTMLTidy, this);
367 pageString = Module.runRegExpSwapSync(
368 pageString, OPTIONS.RegExpSwapCollection);
369
370 // Detect any line goes over column 80.
371 Module.detectLineOverflow(pageString, this);
372
373 this.finish_(pageString, taskDone);
374 });
375 }
376
377 /**
378 * Process JS file. The processing performs the following in order:
379 * - Extract JS code, apply clang-format and inject the code to element.
380 * - RegExp substitution.
381 * - Detect any line overflows 80 columns.
382 * @param {Function} taskDone completion callback.
383 */
384 processJS_(taskDone) {
385 // The file is a JS code: run clang-format, RegExp substitution and check
386 // for overflowed lines.
387 Module.runClangFormat(this.pageString_, OPTIONS.ClangFormat, 0, this)
388 .then((formattedCodeString) => {
389 formattedCodeString = Module.runRegExpSwapSync(
390 formattedCodeString, [OPTIONS.RegExpSwapCollection[0]]);
391 Module.detectLineOverflow(formattedCodeString, this);
392 this.finish_(formattedCodeString, taskDone);
393 });
394 }
395
396 finish_(resultString, taskDone) {
397 if (this.options_.inplace) {
398 Util.writeStringToFileSync(resultString, this.targetFilePath_);
399 } else {
400 process.stdout.write(resultString);
401 }
402
403 this.printLog();
404 taskDone();
405 }
406
407 /**
408 * Adding log message.
409 * @param {String} location Caller information.
410 * @param {String} message Log message.
411 */
412 addLog(location, message) {
413 if (!this.logs_.hasOwnProperty(location))
414 this.logs_[location] = [];
415 this.logs_[location].push(message);
416 }
417
418 /**
419 * Print log messages at the end of task.
420 */
421 printLog() {
422 if (!this.options_.verbose)
423 return;
424
425 console.warn('> Logs from: ' + this.targetFilePath_);
426 for (let location in this.logs_) {
427 console.warn(' [] ' + location);
428 this.logs_[location].forEach((message) => {
429 if (Array.isArray(message)) {
430 message.forEach((subMessage) => {
431 if (subMessage.length > 0)
432 console.warn(' - ' + subMessage);
433 });
434 } else {
435 console.warn(' - ' + message);
436 }
437 });
438 }
439 }
440 }
441
442
443 /**
444 * @class TidyTaskRunner
445 */
446 class TidyTaskRunner {
447 /**
448 * @param {Array} files A list of file paths.
449 * @param {Object} options Task options.
450 * @param {Boolean} options.inplace |true| for in-place processing directly
451 * writing into the target file. By default,
452 * this is |false| and the result is piped
453 * into the stdout.
454 * @param {Boolean} options.verbose Prints out warnings and logs from the
455 * process when |true|. |false| by default.
456 * @return {TidyTaskRunner} A task runner object.
457 */
458 constructor(files, options) {
459 this.targetFiles_ = files;
460 this.options_ = options;
461 this.tasks_ = [];
462 this.currentTask_ = 0;
463 }
464
465 startProcessing() {
466 this.targetFiles_.forEach((filePath) => {
467 this.tasks_.push(new TidyTask(filePath, this.options_));
468 });
469 this.log_('Task runner started: ' + this.targetFiles_.length + ' file(s).');
470 this.runTask_();
471 }
472
473 runTask_() {
474 this.log_(
475 'Running task #' + (this.currentTask_ + 1) + ': ' +
476 this.targetFiles_[this.currentTask_] +
477 (this.options_.inplace ? ' (IN-PLACE)' : ''));
478 this.tasks_[this.currentTask_].run(this.done_.bind(this));
479 }
480
481 done_() {
482 this.log_('Task #' + (this.currentTask_ + 1) + ' completed.');
483 this.currentTask_++;
484 if (this.currentTask_ < this.tasks_.length) {
485 this.runTask_();
486 } else {
487 this.log_(
488 'Task runner completed: ' + this.targetFiles_.length +
489 ' file(s) processed.');
490 }
491 }
492
493 log_(message) {
494 if (this.options_.verbose)
495 console.warn('[layout-test-tidy] ' + message);
496 }
497 }
498
499
500 // Entry point.
501 function main() {
502 let args = process.argv.slice(2);
503
504 // Extract options from the arguments.
505 let optionArgs = args.filter((arg, index) => {
506 if (arg.startsWith('-') || arg.startsWith('--')) {
507 args[index] = null;
508 return true;
509 }
510 });
511
512 args = args.filter(arg => arg);
513
514 // A single target (a file or a directory) is allowed.
515 if (args.length !== 1) {
516 Util.logAndExit('main', 'Please specify a single target. (' + args + ')');
517 }
518
519 // Populate options flags.
520 let options = {
521 inPlace: optionArgs.includes('-i') || optionArgs.includes('--in-place'),
522 verbose: optionArgs.includes('-v') || optionArgs.includes('--verbose')
523 };
524
525 // Collect target file(s) from the file system.
526 let targetPath = args[0];
527 let files = [];
528 if (targetPath) {
529 try {
530 let stat = fs.lstatSync(targetPath);
531 if (stat.isFile()) {
532 files.push(targetPath);
533 } else if (stat.isDirectory()) {
534 files = glob.sync(
535 targetPath + '/**/*.{html,js}', {ignore: ['**/node_modules/**/*']});
536 }
537 } catch (error) {
538 let errorMessage = 'Invalid file path. (' + targetPath + ')\n' +
539 ' > ' + error.toString();
540 Util.logAndExit('main', errorMessage);
541 }
542 }
543
544 // Files to be skipped.
545 let filesToBeSkipped =
546 Util.loadFileToStringSync('skip-tidy').toString().split('\n');
547 filesToBeSkipped.forEach((fileSkipped) => {
548 let index = files.indexOf(fileSkipped);
549 if (index > -1) {
550 files.splice(index, 1);
551 }
552 });
553
554 if (files.length > 0) {
555 let taskRunner = new TidyTaskRunner(files, options);
556 taskRunner.startProcessing();
557 } else {
558 Util.logAndExit('main', 'No files to process.');
559 }
560 }
561
562 main();
OLDNEW
« no previous file with comments | « third_party/WebKit/LayoutTests/webaudio/tools/README.md ('k') | third_party/WebKit/LayoutTests/webaudio/tools/package.json » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698