UNPKG

tslint-clean-code

Version:
486 lines (426 loc) 19.4 kB
"use strict"; var _ = require('underscore'); module.exports = function(grunt) { let additionalMetadata; let allCweDescriptions; function getAllRules() { const contribRules = grunt.file.expand('dist/build/*Rule.js'); const baseRules = grunt.file.expand('node_modules/tslint/lib/rules/*Rule.js'); return contribRules.concat(baseRules); } function hash(input) { // initialized with a prime number let hash = 31; let i = 0; for (i = 0; i < input.length; i++) { // multiply by prime so to get the better distribution of the values hash = 31 * hash + input.charCodeAt(i); // run the hash function on all chars hash = hash | 0; // convert to 32 bit signed integer } return Math.abs(hash).toString(32).toUpperCase(); } function getMetadataFromFile(ruleFile) { const moduleName = './' + ruleFile.replace(/\.js$/, ''); const module = require(moduleName); if (module.Rule.metadata == null) { grunt.fail.warn('No metadata found for ' + moduleName); return; } return module.Rule.metadata; } function createCweDescription(metadata) { allCweDescriptions = allCweDescriptions || grunt.file.readJSON('./cwe_descriptions.json', {encoding: 'UTF-8'}); const cwe = getMetadataValue(metadata, 'commonWeaknessEnumeration', true, true); if (cwe === '') { return ''; } let result = ''; cwe.split(',').forEach(function (cweNumber) { cweNumber = cweNumber.trim(); const description = allCweDescriptions[cweNumber]; if (description == null) { grunt.fail.warn(`Cannot find description of ${cweNumber} for rule ${metadata['ruleName']} in cwe_descriptions.json`) } if (result !== '') { result = result + '\n'; } result = result + `CWE ${cweNumber} - ${description}` }); if (result !== '') { return '"' + result + '"'; } return result; } function getMetadataValue(metadata, name, allowEmpty, doNotEscape) { additionalMetadata = additionalMetadata || grunt.file.readJSON('./additional_rule_metadata.json', {encoding: 'UTF-8'}); let value = metadata[name]; if (value == null) { if (additionalMetadata[metadata.ruleName] == null) { if (allowEmpty == false) { grunt.fail.warn(`Could not read metadata for rule ${metadata.ruleName} from additional_rule_metadata.json`); } else { return ''; } } value = additionalMetadata[metadata.ruleName][name]; if (value == null) { if (allowEmpty == false) { grunt.fail.warn(`Could not read attribute ${name} of rule ${metadata.ruleName}`); } return ''; } } if (doNotEscape == true) { return value; } value = value.replace(/^\n+/, ''); // strip leading newlines value = value.replace(/\n/, ' '); // convert newlines if (value.indexOf(',') > -1) { return '"' + value + '"'; } else { return value; } } function camelize(input) { return _(input).reduce(function(memo, element) { if (element.toLowerCase() === element) { memo = memo + element; } else { memo = memo + '-' + element.toLowerCase(); } return memo; }, ''); } function getAllRuleNames(options) { options = options || { skipTsLintRules: false } var convertToRuleNames = function(filename) { filename = filename .replace(/Rule\..*/, '') // file extension plus Rule name .replace(/.*\//, ''); // leading path return camelize(filename); }; var contribRules = _(grunt.file.expand('src/*Rule.ts')).map(convertToRuleNames); var baseRules = []; if (!options.skipTsLintRules) { baseRules = _(grunt.file.expand('node_modules/tslint/lib/rules/*Rule.js')).map(convertToRuleNames); } var allRules = baseRules.concat(contribRules); allRules.sort(); return allRules; } function getAllFormatterNames() { var convertToRuleNames = function(filename) { filename = filename .replace(/Formatter\..*/, '') // file extension plus Rule name .replace(/.*\//, ''); // leading path return camelize(filename); }; var formatters = _(grunt.file.expand('src/*Formatter.ts')).map(convertToRuleNames); formatters.sort(); return formatters; } function camelCase(input) { return input.toLowerCase().replace(/-(.)/g, function(match, group1) { return group1.toUpperCase(); }); } grunt.initConfig({ pkg: grunt.file.readJSON('package.json'), clean: { src: ['dist'], options: { force: true } }, copy: { options: { encoding: 'UTF-8' }, package: { files: [ { expand: true, cwd: 'dist/src', src: [ '**/*.js', '**/*.json', '!tests/**', 'tests/TestHelper.js', 'tests/TestHelper.d.ts', '!references.js' ], dest: 'dist/build' }, { expand: true, cwd: '.', src: [ 'README.md', 'recommended_ruleset.js' ], dest: 'dist/build' } ] }, json: { expand: true, cwd: '.', src: ['src/**/*.json'], dest: 'dist' } }, mochaTest: { test: { src: ['dist/src/tests/**/*.js'] } }, ts: { default: { tsconfig: { tsconfig: './tsconfig.json', passThrough: true, updateFiles: true, overwriteFiles: true, } }, 'test-data': { tsconfig: { tsconfig: './tsconfig.testdata.json', passThrough: true, updateFiles: true, overwriteFiles: true } } }, tslint: { options: { rulesDirectory: 'dist/src' }, prod: { options: { configuration: grunt.file.readJSON("tslint.json", { encoding: 'UTF-8' }) }, files: { src: [ 'src/**/*.ts', '!src/tests/**' ] } }, tests: { options: { configuration: Object.assign({}, grunt.file.readJSON("tslint.json", { encoding: 'UTF-8' }), grunt.file.readJSON("src/tests/tslint.json", { encoding: 'UTF-8' }) ), }, files: { src: [ 'src/tests/**/*.ts', '!src/tests/references.ts' ] } } }, watch: { scripts: { files: [ './src/**/*.ts', './tests/**/*.ts' ], tasks: [ 'ts', 'mochaTest', 'tslint' ] } } }); require('load-grunt-tasks')(grunt); // loads all grunt-* npm tasks require('time-grunt')(grunt); grunt.registerTask('create-package-json-for-npm', 'A task that creates a package.json file for the npm module', function () { var basePackageJson = grunt.file.readJSON('package.json', { encoding: 'UTF-8' }); delete basePackageJson.devDependencies; grunt.file.write('dist/build/package.json', JSON.stringify(basePackageJson, null, 2), { encoding: 'UTF-8' }); }); grunt.registerTask('validate-debug-mode', 'A task that makes sure ErrorTolerantWalker.DEBUG is false', function () { // DON'T MAKE A RELEASE IN DEBUG MODE var fileText = grunt.file.read('src/utils/ErrorTolerantWalker.ts', { encoding: 'UTF-8' }); if (fileText.indexOf('DEBUG: boolean = false') === -1) { grunt.fail.warn('ErrorTolerantWalker.DEBUG is turned on. Turn off debugging to make a release'); } }); grunt.registerTask('validate-documentation', 'A task that validates that all rules defined in src are documented in README.md\n' + 'and validates that the package.json version is the same version defined in README.md', function () { var readmeText = grunt.file.read('README.md', { encoding: 'UTF-8' }); var packageJson = grunt.file.readJSON('package.json', { encoding: 'UTF-8' }); getAllRuleNames({ skipTsLintRules: true }).forEach(function(ruleName) { if (readmeText.indexOf(ruleName) === -1) { grunt.fail.warn('A rule was found that is not documented in README.md: ' + ruleName); } }); getAllFormatterNames().forEach(function(formatterName) { if (readmeText.indexOf(formatterName) === -1) { grunt.fail.warn('A formatter was found that is not documented in README.md: ' + formatterName); } }); // if (readmeText.indexOf('\nVersion ' + packageJson.version + ' ') === -1) { // grunt.fail.warn('Version not documented in README.md correctly.\n' + // 'package.json declares: ' + packageJson.version + '\n' + // 'README.md declares something different.'); // } }); grunt.registerTask('validate-config', 'A task that makes sure all the rules in the project are defined to run during the build.', function () { var tslintConfig = grunt.file.readJSON('tslint.json', { encoding: 'UTF-8' }); var rulesToSkip = { 'ban-types': true, 'match-default-export-name': true, // requires type checking 'newline-before-return': true, // kind of a silly rule 'no-non-null-assertion': true, // in fact we prefer the opposite rule 'prefer-template': true, // rule does not handle multi-line strings nicely 'return-undefined': true, // requires type checking 'no-unused-variable': true, // requires type checking 'no-unexternalized-strings': true, // this is a VS Code specific rule 'no-relative-imports': true, // this project uses relative imports 'no-empty-line-after-opening-brace': true, // too strict 'align': true, // no need 'comment-format': true, // no need 'interface-name': true, // no need 'max-file-line-count': true, // no need 'member-ordering': true, // too strict 'no-inferrable-types': true, // we prefer the opposite 'ordered-imports': true, // too difficult to turn on 'typedef-whitespace': true, // too strict 'completed-docs': true, // no need 'cyclomatic-complexity': true, // too strict 'file-header': true, // no need 'max-classes-per-file': true // no need }; var errors = []; getAllRuleNames().forEach(function(ruleName) { if (rulesToSkip[ruleName]) { return; } if (tslintConfig.rules[ruleName] !== true && tslintConfig.rules[ruleName] !== false) { if (tslintConfig.rules[ruleName] == null || tslintConfig.rules[ruleName][0] !== true) { errors.push('A rule was found that is not enabled on the project: ' + ruleName); } } }); if (errors.length > 0) { grunt.fail.warn(errors.join('\n')); } }); grunt.registerTask('generate-sdl-report', 'A task that generates an SDL report in csv format', function () { const rows = []; const resolution = 'See description on the tslint or tslint-microsoft-contrib website'; const procedure = 'TSLint Procedure'; const header = 'Title,Description,ErrorID,Tool,IssueClass,IssueType,SDL Bug Bar Severity,' + 'SDL Level,Resolution,SDL Procedure,CWE,CWE Description'; getAllRules().forEach(function(ruleFile) { const metadata = getMetadataFromFile(ruleFile); const issueClass = getMetadataValue(metadata, 'issueClass'); if (issueClass === 'Ignored') { return; } const ruleName = getMetadataValue(metadata, 'ruleName'); const errorId = 'TSLINT' + hash(ruleName); const issueType = getMetadataValue(metadata, 'issueType'); const severity = getMetadataValue(metadata, 'severity'); const level = getMetadataValue(metadata, 'level'); const description = getMetadataValue(metadata, 'description'); const cwe = getMetadataValue(metadata, 'commonWeaknessEnumeration', true, false); const cweDescription = createCweDescription(metadata); const row = `${ruleName},${description},${errorId},tslint,${issueClass},${issueType},${severity},${level},${resolution},${procedure},${cwe},${cweDescription}`; rows.push(row); }); rows.sort(); rows.unshift(header); grunt.file.write('tslint-warnings.csv', rows.join('\n'), {encoding: 'UTF-8'}); }); grunt.registerTask('generate-recommendations', 'A task that generates the recommended_ruleset.js file', function () { const groupedRows = { 'Security': [], 'Correctness': [], 'Clarity': [], 'Whitespace': [], 'Configurable': [], 'Deprecated': [], 'Accessibility': [], }; const warnings = []; getAllRules().forEach(function(ruleFile) { const metadata = getMetadataFromFile(ruleFile); const groupName = getMetadataValue(metadata, 'group'); if (groupName === 'Ignored') { return; } if (groupName === '') { warnings.push('Could not generate recommendation for rule file: ' + ruleFile); } let recommendation = getMetadataValue(metadata, 'recommendation', true, true); if (recommendation === '') { recommendation = 'true,'; } const ruleName = getMetadataValue(metadata, 'ruleName'); groupedRows[groupName].push(` "${ruleName}": ${recommendation}`); }); if (warnings.length > 0) { grunt.fail.warn('\n' + warnings.join('\n')); } _.values(groupedRows).forEach(function (element) { element.sort(); }); let data = grunt.file.read('./templates/recommended_ruleset.js.snippet', {encoding: 'UTF-8'}); data = data.replace('%security_rules%', groupedRows['Security'].join('\n')); data = data.replace('%correctness_rules%', groupedRows['Correctness'].join('\n')); data = data.replace('%clarity_rules%', groupedRows['Clarity'].join('\n')); data = data.replace('%whitespace_rules%', groupedRows['Whitespace'].join('\n')); data = data.replace('%configurable_rules%', groupedRows['Configurable'].join('\n')); data = data.replace('%deprecated_rules%', groupedRows['Deprecated'].join('\n')); data = data.replace('%accessibilityy_rules%', groupedRows['Accessibility'].join('\n')); grunt.file.write('recommended_ruleset.js', data, {encoding: 'UTF-8'}); }); grunt.registerTask('generate-default-tslint-json', 'A task that converts recommended_ruleset.js to ./dist/build/tslint.json', function () { const data = require('./recommended_ruleset.js'); data['rulesDirectory'] = './'; grunt.file.write('./dist/build/tslint.json', JSON.stringify(data, null, 2), {encoding: 'UTF-8'}); }); grunt.registerTask('create-rule', 'A task that creates a new rule from the rule templates. --rule-name parameter required', function () { function applyTemplates(source) { return source.replace(/%RULE_NAME%/gm, ruleName) .replace(/%RULE_FILE_NAME%/gm, ruleFile) .replace(/%WALKER_NAME%/gm, walkerName); } var ruleName = grunt.option('rule-name'); if (!ruleName) { grunt.fail.warn('--rule-name parameter is required'); } else { var ruleFile = camelCase(ruleName) + 'Rule'; var sourceFileName = './src/' + ruleFile + '.ts'; var testFileName = './src/tests/' + ruleFile.charAt(0).toUpperCase() + ruleFile.substr(1) + 'Tests.ts'; var walkerName = ruleFile.charAt(0).toUpperCase() + ruleFile.substr(1) + 'Walker'; var ruleTemplateText = grunt.file.read('./templates/rule.snippet', {encoding: 'UTF-8'}); var testTemplateText = grunt.file.read('./templates/rule-tests.snippet', {encoding: 'UTF-8'}); grunt.file.write(sourceFileName, applyTemplates(ruleTemplateText), {encoding: 'UTF-8'}); grunt.file.write(testFileName, applyTemplates(testTemplateText), {encoding: 'UTF-8'}); var currentRuleset = grunt.file.readJSON('./tslint.json', {encoding: 'UTF-8'}); currentRuleset.rules[ruleName] = true; grunt.file.write('./tslint.json', JSON.stringify(currentRuleset, null, 2), {encoding: 'UTF-8'}); } }); grunt.registerTask('all', 'Performs a cleanup and a full build with all tasks', [ 'clean', 'copy:json', 'ts', 'mochaTest', 'tslint', 'validate-documentation', 'validate-config', 'validate-debug-mode', 'copy:package', 'generate-recommendations', 'generate-default-tslint-json', 'generate-sdl-report', 'create-package-json-for-npm' ]); grunt.registerTask('default', ['all']); };