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

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 (1) 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'},
Raymond Toy 2017/05/11 21:50:28 Haha. I assume you ran clang-format on this file w
hongchan 2017/05/11 22:50:52 :|
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);
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 {Task} task Associated Task object.
115 * @return {Promise} Processed code as string.
116 * @resolve {String} clang-formatted JS code as string.
117 * @reject {Error}
118 */
119 runClangFormat: (codeString, clangFormatOption, task) => {
120 let clangFormatBinary = __dirname + '/node_modules/clang-format/bin/';
121 clangFormatBinary += (os.platform() === 'win32') ?
122 'win32/clang-format.exe' :
123 os.platform() + '_' + os.arch() + '/clang-format';
124
125 return new Promise((resolve, reject) => {
126 // Be sure to pipe the result, not to stdio.
Raymond Toy 2017/05/11 21:50:28 What does this mean?
hongchan 2017/05/11 22:50:52 Expanded the comment.
127 let result = '';
128 let clangFormat = cp.spawn(
129 clangFormatBinary, clangFormatOption,
130 {stdio: ['pipe', 'pipe', process.stderr]});
131
132 // Capture the data when it's arrived at the pipe.
133 clangFormat.stdout.on('data', (data) => {
134 result += data;
135 });
136
137 // For debug purpose:
138 // clangFormat.stdout.pipe(process.stdout);
139
140 clangFormat.stdout.on('close', (exitCode) => {
141 if (exitCode) {
142 Util.logAndExit('Module.runClangFormat', 'exit code = 1');
143 } else {
144 task.addLog('Module.runClangFormat', 'clang-format was successful.');
145 resolve(result);
146 }
147 });
148
149 clangFormat.stdin.setEncoding('utf-8');
Raymond Toy 2017/05/11 21:50:28 As we mentioned offline, when we read the file, we
hongchan 2017/05/11 22:50:52 Done.
150 clangFormat.stdin.write(codeString);
151 clangFormat.stdin.end();
152 });
153 },
154
155 /**
156 * Detect line overflow and record the line number to the task log.
157 * @param {String} pageOrCodeString HTML page or JS code data in string.
158 * @param {TidyTask} task Associated TidyTask object.
159 */
160 detectLineOverflow: (pageOrCodeString, task) => {
161 let currentLineNumber = 0;
162 let index0 = 0;
163 let index1 = 0;
164 while (index0 < pageOrCodeString.length - 1) {
165 index1 = pageOrCodeString.indexOf('\n', index0);
166 if (index1 - index0 > 80) {
167 task.addLog(
168 'Module.detectLineOverflow',
169 'Overflow (> 80 cols.) at line ' + currentLineNumber + '.');
170 }
171 currentLineNumber++;
172 index0 = index1 + 1;
173 }
174 }
175
176 };
177
178
179 /**
180 * DOM utilities. Process DOM processing after parsing the string by JSDOM.
181 */
182 const DOMUtil = {
183
184 /**
185 * Parse string, generate JSDOM object and return |document| element.
186 * @param {String} pageString An HTML page in string.
187 * @return {Document} A |document| object.
188 */
189 getJSDOMFromStringSync: (pageString) => {
190 return new JSDOM(`${pageString}`);
191 // return jsdom_.window.document;
192 },
193
194 /**
195 * In-place tidy up head element.
196 * @param {Document} document A |document| object.
197 * @param {Task} task An associated Task object.
198 * @return {Void}
199 */
200 tidyHeadElementSync: (document, task) => {
201 try {
202 // If the title is missing, add one from the file name.
203 let titleElement = document.querySelector('title');
204 if (!titleElement) {
205 titleElement = document.createElement('title');
206 titleElement.textContent = path.basename(task.targetFilePath_);
207 task.addLog(
208 'DOMUtil.tidyHeadElementSync',
209 'Title element was missing thus a new one was added.');
210 }
211
212 // The title element should be the first.
213 let headElement = document.querySelector('head');
214 headElement.insertBefore(titleElement, headElement.firstChild);
215
216 // If a script element in body does not have JS code, move to the head
217 // section.
218 let scriptElementsInBody = document.body.querySelectorAll('script');
219 scriptElementsInBody.forEach((scriptElement) => {
220 if (!scriptElement.textContent)
221 headElement.appendChild(scriptElement);
222 });
223 } catch (error) {
224 task.addLog('DOMUtil.tidyHeadElementSync', error.toString());
225 }
226 },
227
228 /**
229 * Sanitize and extract |script| element with JS test code.
230 * @param {Document} document A |document| object.
231 * @param {Task} task An associated Task object.
232 * @return {ScriptElement}
233 */
234 getElementWithTestCodeSync: (document, task) => {
235 let numberOfElementsWithCode = 0;
236 let elementWithTestCode;
237 let scriptElements = document.querySelectorAll('script');
238
239 scriptElements.forEach(function(element) {
Raymond Toy 2017/05/11 21:50:28 Not element => { ?
hongchan 2017/05/11 22:50:52 Done. Also renamed variables for clarification.
240 // We don't want type attribute.
241 element.removeAttribute('type');
242
243 if (element.textContent.length > 0) {
244 ++numberOfElementsWithCode;
245 elementWithTestCode = element;
246 element.id = 'layout-test-code';
247 // If the element belongs to something else other than body, move it to
248 // the body. This fixes script elements that are located in weird
249 // positions. (e.g outside of body or head)
250 if (element.parentElement !== document.body)
251 document.body.appendChild(element);
252 }
253 });
254
255 if (numberOfElementsWithCode !== 1) {
256 task.addLog(
257 'DOMUtil.getElementWithTestCodeSync',
258 numberOfElementsWithCode + ' <script> element(s) with JS ' +
259 'code were found.');
260 elementWithTestCode = null;
261 }
262
263 return elementWithTestCode;
264 }
265
266 };
267
268
269 /**
270 * @class TidyTask
271 * @description Per-file processing task. This object should be constructed
272 * directly. The task runner creates this when it is necessary.
273 */
274 class TidyTask {
275 /**
276 * @param {String} targetFilePath A path to file to be processed.
277 * @param {Object} options Task options.
278 * @param {Boolean} options.inplace |true| for in-place processing directly
279 * writing into the target file. By default,
280 * this is |false| and the result is piped
281 * into the stdout.
282 * @param {Boolean} options.verbose Prints out warnings and logs from the
283 * process when |true|. |false| by default.
284 */
285 constructor(targetFilePath, options) {
286 this.targetFilePath_ = targetFilePath;
287 this.options_ = options;
288
289 this.fileType_ = path.extname(this.targetFilePath_);
290 this.pageString_ = Util.loadFileToStringSync(this.targetFilePath_);
291 this.jsdom_ = null;
292 this.logs_ = {};
293 }
294
295 /**
296 * Run processing sequence. Don't call this directly.
297 * @param {Function} taskDone Task runner callback function.
298 */
299 run(taskDone) {
300 switch (this.fileType_) {
301 case '.html':
302 this.processHTML_(taskDone);
303 break;
304 case '.js':
305 this.processJS_(taskDone);
306 break;
307 default:
308 Util.logAndExit(
309 'TidyTask.constructor', 'Invalid file type: ' + this.fileType_);
310 break;
311 }
312 }
313
314 /**
315 * Process HTML file. The processing performs the following in order:
316 * - DOM parsing to sanitize invalid/incorrect markup structure.
317 * - Extract JS code, apply clang-format and inject the code to element.
318 * - Apply HTMLTidy to the markup.
319 * - RegExp substitution.
320 * - Detect any line overflows 80 columns.
321 * @param {Function} taskDone completion callback.
322 */
323 processHTML_(taskDone) {
324 // Parse page string into JSDOM.element object.
325 this.jsdom_ = DOMUtil.getJSDOMFromStringSync(this.pageString_);
326
327 // Clean up the head element section.
328 DOMUtil.tidyHeadElementSync(this.jsdom_.window.document, this);
329
330 let scriptElement =
331 DOMUtil.getElementWithTestCodeSync(this.jsdom_.window.document, this);
332
333 if (!scriptElement)
334 Util.logAndExit('TidyTask.processHTML_', 'Invalid <script> element.');
335
336 // Extract JS code string with additional braces for indentation hack.
337 let codeString = '{{{/*start*/' + scriptElement.textContent + '/*end*/}}}';
338
339 // Start with clang-foramt, then HTMLTidy and RegExp substitution.
340 Module.runClangFormat(codeString, OPTIONS.ClangFormat, this)
341 .then((formattedCodeString) => {
342 // Remove extra braces for the indentation hack.
343 formattedCodeString = formattedCodeString.substring(
344 formattedCodeString.indexOf('/*start*/') + 10);
345 formattedCodeString = formattedCodeString.substring(
346 0, formattedCodeString.indexOf('/*end*/'));
Raymond Toy 2017/05/11 21:50:28 This is not very robust. What if the code actuall
hongchan 2017/05/11 22:50:52 Done.
347
348 // Replace the original code with clang-formatted code.
349 scriptElement.textContent = formattedCodeString;
350
351 // Then tidy the text data from JSDOM. After this point, DOM
352 // manipulation is not possible anymore.
353 let pageString = this.jsdom_.serialize();
354 pageString =
355 Module.runHTMLTidySync(pageString, OPTIONS.HTMLTidy, this);
356 pageString = Module.runRegExpSwapSync(
357 pageString, OPTIONS.RegExpSwapCollection);
358
359 // Detect any line goes over column 80.
360 Module.detectLineOverflow(pageString, this);
361
362 this.finish_(pageString, taskDone);
363 });
364 }
365
366 /**
367 * Process JS file. The processing performs the following in order:
368 * - Extract JS code, apply clang-format and inject the code to element.
369 * - RegExp substitution.
370 * - Detect any line overflows 80 columns.
371 * @param {Function} taskDone completion callback.
372 */
373 processJS_(taskDone) {
374 // The file is a JS code: run clang-format, RegExp substitution and check
375 // for overflowed lines.
376 Module.runClangFormat(this.pageString_, OPTIONS.ClangFormat, this)
377 .then((formattedCodeString) => {
378 formattedCodeString = Module.runRegExpSwapSync(
379 formattedCodeString, [OPTIONS.RegExpSwapCollection[0]]);
380 Module.detectLineOverflow(formattedCodeString, this);
381 this.finish_(formattedCodeString, taskDone);
382 });
383 }
384
385 finish_(resultString, taskDone) {
386 if (this.options_.inplace) {
387 Util.writeStringToFileSync(resultString, this.targetFilePath_);
388 } else {
389 process.stdout.write(resultString);
390 }
391
392 this.printLog();
393 taskDone();
394 }
395
396 /**
397 * Adding log message.
398 * @param {String} location Caller information.
399 * @param {String} message Log message.
400 */
401 addLog(location, message) {
402 if (!this.logs_.hasOwnProperty(location))
403 this.logs_[location] = [];
404 this.logs_[location].push(message);
405 }
406
407 /**
408 * Print log messages at the end of task.
409 */
410 printLog() {
411 if (!this.options_.verbose)
412 return;
413
414 console.log('> Logs from: ' + this.targetFilePath_);
415 for (let location in this.logs_) {
416 console.log(' [] ' + location);
417 this.logs_[location].forEach((message) => {
418 if (Array.isArray(message)) {
419 message.forEach((subMessage) => {
420 if (subMessage.length > 0)
421 console.log(' - ' + subMessage);
422 });
423 } else {
424 console.log(' - ' + message);
425 }
426 });
427 }
428 }
429 }
430
431
432 /**
433 * @class TidyTaskRunner
434 */
435 class TidyTaskRunner {
436 /**
437 * @param {Array} files A list of file paths.
438 * @param {Object} options Task options.
439 * @param {Boolean} options.inplace |true| for in-place processing directly
440 * writing into the target file. By default,
441 * this is |false| and the result is piped
442 * into the stdout.
443 * @param {Boolean} options.verbose Prints out warnings and logs from the
444 * process when |true|. |false| by default.
445 * @return {TidyTaskRunner} A task runner object.
446 */
447 constructor(files, options) {
448 this.targetFiles_ = files;
449 this.options_ = options;
450 this.tasks_ = [];
451 this.currentTask_ = 0;
452 }
453
454 startProcessing() {
455 this.targetFiles_.forEach((filePath) => {
456 this.tasks_.push(new TidyTask(filePath, this.options_));
457 });
458 this.log_('Task runner started: ' + this.targetFiles_.length + ' file(s).');
459 this.runTask_();
460 }
461
462 runTask_() {
463 this.log_(
464 'Running task #' + (this.currentTask_ + 1) + ': ' +
465 this.targetFiles_[this.currentTask_] +
466 (this.options_.inplace ? ' (IN-PLACE)' : ''));
467 this.tasks_[this.currentTask_].run(this.done_.bind(this));
468 }
469
470 done_() {
471 this.log_('Task #' + (this.currentTask_ + 1) + ' completed.');
472 this.currentTask_++;
473 if (this.currentTask_ < this.tasks_.length) {
474 this.runTask_();
475 } else {
476 this.log_(
477 'Task runner completed: ' + this.targetFiles_.length +
478 ' file(s) processed.');
479 }
480 }
481
482 log_(message) {
483 if (this.options_.verbose)
484 console.log('[layout-test-tidy] ' + message);
485 }
486 }
487
488
489 // Entry point.
490 function main() {
491 let args = process.argv.slice(2);
492
493 // Extract options from the arguments.
494 let optionArgs = args.filter((arg, index) => {
495 if (arg.startsWith('-') || arg.startsWith('--')) {
496 args[index] = null;
497 return true;
498 }
499 });
500
501 args = args.filter(arg => arg);
502
503 // A single target (a file or a directory) is allowed.
504 if (args.length !== 1) {
505 Util.logAndExit('main', 'Please specify a single target. (' + args + ')');
506 }
507
508 // Populate options flags.
509 let options = {
510 inPlace: optionArgs.includes('-i') || optionArgs.includes('--in-place'),
511 verbose: optionArgs.includes('-v') || optionArgs.includes('--verbose')
512 };
513
514 // Collect target file(s) from the file system.
515 let targetPath = args[0];
516 let files = [];
517 if (targetPath) {
518 try {
519 let stat = fs.lstatSync(targetPath);
520 if (stat.isFile()) {
521 files.push(targetPath);
522 } else if (stat.isDirectory()) {
523 files = glob.sync(
524 targetPath + '/**/*.{html,js}', {ignore: ['**/node_modules/**/*']});
525 }
526 } catch (error) {
527 let errorMessage = 'Invalid file path. (' + targetPath + ')\n' +
528 ' > ' + error.toString();
529 Util.logAndExit('main', errorMessage);
530 }
531 }
532
533 // Files to be skipped.
534 let filesToBeSkipped =
535 Util.loadFileToStringSync('skip-tidy').toString().split('\n');
536 filesToBeSkipped.forEach((fileSkipped) => {
537 let index = files.indexOf(fileSkipped);
538 if (index > -1) {
539 files.splice(index, 1);
540 }
541 });
542
543 if (files.length > 0) {
544 let taskRunner = new TidyTaskRunner(files, options);
545 taskRunner.startProcessing();
546 } else {
547 Util.logAndExit('main', 'No files to process.');
548 }
549 }
550
551 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