script/check-code-style.js (194 lines of code) (raw):

#!/usr/bin/env node // eslint-disable-next-line @typescript-eslint/no-var-requires const fs = require('fs'); // eslint-disable-next-line @typescript-eslint/no-var-requires const path = require('path'); // eslint-disable-next-line @typescript-eslint/no-var-requires const exec = require('child_process').execSync; let exitCode = 0; const ignoreMemo = {}; const isIgnored = (file) => { if (file in ignoreMemo) { return ignoreMemo[file]; } try { // If this returns zero, it means the file is ignored by git; skip it. exec(`git check-ignore -q '${file}'`); ignoreMemo[file] = true; return true; } catch (e) { // It's tracked by git. ignoreMemo[file] = false; return false; } }; const walk = (dir) => { let results = []; if (dir.includes('.DS_Store')) { return results; } const list = fs.readdirSync(dir); list.forEach(function (file) { file = dir + '/' + file; let stat = fs.statSync(file); if (stat && stat.isDirectory()) { results = results.concat(walk(file)); } else { results.push(file); } }); return results; }; const failed = (file, reason, description) => { console.error('Failed:', file, reason); if (description) { console.error(description); } exitCode = 1; }; const dirs = () => { return fs .readdirSync('src') .filter( dir => path.basename(dir) !== 'tsconfig.json' && path.basename(dir) !== 'index.ts' && !path.basename(dir).startsWith('.') && !path.basename(dir).includes('signalingprotocol') && !path.basename(dir).includes('screensignalingprotocol') && !path.basename(dir).includes('utils') && !path.basename(dir).includes('cspmonitor') ); }; const tests = () => { return walk('test') .filter(file => file.endsWith('.test.ts')) .sort(); }; const allFiles = () => { const srcFiles = walk('src').filter( file => file.endsWith('.ts') && path.basename(file) !== 'index.ts' ); const testFiles = walk('test').filter(file => file.endsWith('.ts')); const demosFiles = walk('demos/browser/app').filter( file => file.endsWith('.ts') || file.endsWith('.js') ); return srcFiles.concat(testFiles).concat(demosFiles); }; tests().forEach(file => { if (file === 'test/global/Global.test.ts') { return; } if (isIgnored(file)) { return; } const fileText = fs.readFileSync(file, 'utf-8').toString(); if (fileText.includes('not.equal.null')) { failed( file, 'contains not.equal.null', 'Avoid using not.equal.null and convert it to assert.exists' ); } const basename = path.basename(file, '.test.ts'); let describeCount = 0; const lines = fileText.split('\n'); for (let i = 0; i < lines.length; i++) { if (lines[i].match(/^describe\(/)) { ++describeCount; } } if (describeCount !== 1) { const errorString = 'Ensure that each test file has one top-level describe'; failed(file, 'has more than one top-level describe', errorString); } }); const isDirIgnoredForInterfaceCheck = (dir) => { return ['applicationmetadata'].includes(dir); }; const isFileIgnoredForInterfaceCheck = (file) => { return ['SDP.ts', 'StatsCollector.ts', 'ClientMetricReport.ts'].includes(file); }; dirs().forEach(dir => { if (isIgnored(dir)) { return; } if (isDirIgnoredForInterfaceCheck(dir)) { return; } const dirPath = path.join('src', dir); fs.readdirSync(dirPath).forEach(file => { if (isFileIgnoredForInterfaceCheck(file)) { return; } let filePath = path.join(dirPath, file); if (path.basename(filePath, '.ts').toLowerCase() !== path.basename(dirPath)) { // not a interface return; } let fileText = fs.readFileSync(filePath, 'utf-8').toString(); // Ignore comment lines, so you can have a block comment followed by ts-ignore. let lines = fileText.split('\n').filter(s => !(s.match(/^\s*\/\//))); for (let i = 0; i < lines.length; i++) { if (lines[i].trim()[0] !== '*') { let commentStartRegex = new RegExp(/\s+\w+\(.*[:]/); if (commentStartRegex.test(lines[i])) { if (lines[i - 1].trim() !== '*/') { failed( dir, `interface ${file} method ${lines[i].trim()} is missing comment`, 'Ensure that each interface method has a comment explaining what it does and information about the possible inputs and expected outputs.' ); } } } } }); }); const spdx = '// SPDX-License-Identifier: Apache-2.0'; let allYears = []; allFiles().forEach(file => { if (file.endsWith('.d.ts') || isIgnored(file)) { return; } const copyright = `// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.`; const fileLines = fs .readFileSync(file) .toString() .split('\n'); if (fileLines[0].trim() !== copyright) { failed( `${file}:1`, 'header does not include correct copyright', `Ensure that header contains the following copyright: ${copyright}` ); } if (fileLines[1].trim() !== spdx) { failed( `${file}:1`, 'header does not include correct SPDX license code', `Ensure that header contains the following copyright: ${spdx}` ); } if (fileLines[2].trim() !== '') { failed( `${file}:3`, 'copyright file header is not followed by blank line', 'Ensure that header is followed by a blank line' ); } for (let i = 0; i < fileLines.length; i++) { const pos = `${file}:${i + 1}`; // Exclude demos and comment lines. Some of our block comments // include code examples that refer to console.log. if (fileLines[i].includes('console.log') && !fileLines[i].match(/^\s+(?:\*|\/[\*\/])/) && !file.includes('demos/')) { failed(pos, 'contains console.log: ' + fileLines[i], 'Ensure that source does not contain console.log'); } } }); const footerCopyright = `\nCopyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n`; for (const file of ['README.md', 'NOTICE']) { if ( !fs .readFileSync(file) .toString() .endsWith(footerCopyright) ) { failed(file, `Ensure that ${file} ends with the following copyright: ${footerCopyright}`); } } process.exit(exitCode);