UNPKG

19.4 kBJavaScriptView Raw
1"use strict";
2
3var _ = require('underscore');
4
5module.exports = function(grunt) {
6
7 let additionalMetadata;
8 let allCweDescriptions;
9
10 function getAllRules() {
11 const contribRules = grunt.file.expand('dist/build/*Rule.js');
12 const baseRules = grunt.file.expand('node_modules/tslint/lib/rules/*Rule.js');
13 return contribRules.concat(baseRules);
14 }
15
16 function hash(input) {
17 // initialized with a prime number
18 let hash = 31;
19 let i = 0;
20 for (i = 0; i < input.length; i++) {
21 // multiply by prime so to get the better distribution of the values
22 hash = 31 * hash + input.charCodeAt(i); // run the hash function on all chars
23 hash = hash | 0; // convert to 32 bit signed integer
24 }
25 return Math.abs(hash).toString(32).toUpperCase();
26 }
27
28 function getMetadataFromFile(ruleFile) {
29 const moduleName = './' + ruleFile.replace(/\.js$/, '');
30 const module = require(moduleName);
31 if (module.Rule.metadata == null) {
32 grunt.fail.warn('No metadata found for ' + moduleName);
33 return;
34 }
35 return module.Rule.metadata;
36 }
37
38 function createCweDescription(metadata) {
39 allCweDescriptions = allCweDescriptions || grunt.file.readJSON('./cwe_descriptions.json', {encoding: 'UTF-8'});
40
41 const cwe = getMetadataValue(metadata, 'commonWeaknessEnumeration', true, true);
42 if (cwe === '') {
43 return '';
44 }
45
46 let result = '';
47 cwe.split(',').forEach(function (cweNumber) {
48 cweNumber = cweNumber.trim();
49 const description = allCweDescriptions[cweNumber];
50 if (description == null) {
51 grunt.fail.warn(`Cannot find description of ${cweNumber} for rule ${metadata['ruleName']} in cwe_descriptions.json`)
52 }
53 if (result !== '') {
54 result = result + '\n';
55 }
56 result = result + `CWE ${cweNumber} - ${description}`
57 });
58 if (result !== '') {
59 return '"' + result + '"';
60 }
61 return result;
62 }
63
64 function getMetadataValue(metadata, name, allowEmpty, doNotEscape) {
65 additionalMetadata = additionalMetadata || grunt.file.readJSON('./additional_rule_metadata.json', {encoding: 'UTF-8'});
66
67 let value = metadata[name];
68 if (value == null) {
69 if (additionalMetadata[metadata.ruleName] == null) {
70 if (allowEmpty == false) {
71 grunt.fail.warn(`Could not read metadata for rule ${metadata.ruleName} from additional_rule_metadata.json`);
72 } else {
73 return '';
74 }
75 }
76 value = additionalMetadata[metadata.ruleName][name];
77 if (value == null) {
78 if (allowEmpty == false) {
79 grunt.fail.warn(`Could not read attribute ${name} of rule ${metadata.ruleName}`);
80 }
81 return '';
82 }
83 }
84 if (doNotEscape == true) {
85 return value;
86 }
87 value = value.replace(/^\n+/, ''); // strip leading newlines
88 value = value.replace(/\n/, ' '); // convert newlines
89 if (value.indexOf(',') > -1) {
90 return '"' + value + '"';
91 } else {
92 return value;
93 }
94 }
95
96 function camelize(input) {
97 return _(input).reduce(function(memo, element) {
98 if (element.toLowerCase() === element) {
99 memo = memo + element;
100 } else {
101 memo = memo + '-' + element.toLowerCase();
102 }
103 return memo;
104 }, '');
105 }
106
107 function getAllRuleNames(options) {
108 options = options || { skipTsLintRules: false }
109
110 var convertToRuleNames = function(filename) {
111 filename = filename
112 .replace(/Rule\..*/, '') // file extension plus Rule name
113 .replace(/.*\//, ''); // leading path
114 return camelize(filename);
115 };
116
117 var contribRules = _(grunt.file.expand('src/*Rule.ts')).map(convertToRuleNames);
118 var baseRules = [];
119 if (!options.skipTsLintRules) {
120 baseRules = _(grunt.file.expand('node_modules/tslint/lib/rules/*Rule.js')).map(convertToRuleNames);
121 }
122 var allRules = baseRules.concat(contribRules);
123 allRules.sort();
124 return allRules;
125 }
126
127 function getAllFormatterNames() {
128
129 var convertToRuleNames = function(filename) {
130 filename = filename
131 .replace(/Formatter\..*/, '') // file extension plus Rule name
132 .replace(/.*\//, ''); // leading path
133 return camelize(filename);
134 };
135
136 var formatters = _(grunt.file.expand('src/*Formatter.ts')).map(convertToRuleNames);
137 formatters.sort();
138 return formatters;
139 }
140
141 function camelCase(input) {
142 return input.toLowerCase().replace(/-(.)/g, function(match, group1) {
143 return group1.toUpperCase();
144 });
145 }
146
147 grunt.initConfig({
148 pkg: grunt.file.readJSON('package.json'),
149
150 clean: {
151 src: ['dist'],
152 options: {
153 force: true
154 }
155 },
156
157 copy: {
158 options: {
159 encoding: 'UTF-8'
160 },
161 package: {
162 files: [
163 {
164 expand: true,
165 cwd: 'dist/src',
166 src: [
167 '**/*.js',
168 '**/*.json',
169 '!tests/**',
170 'tests/TestHelper.js',
171 'tests/TestHelper.d.ts',
172 '!references.js'
173 ],
174 dest: 'dist/build'
175 },
176 {
177 expand: true,
178 cwd: '.',
179 src: [
180 'README.md',
181 'recommended_ruleset.js'
182 ],
183 dest: 'dist/build'
184 }
185 ]
186 },
187 json: {
188 expand: true,
189 cwd: '.',
190 src: ['src/**/*.json'],
191 dest: 'dist'
192 }
193 },
194
195 mochaTest: {
196 test: {
197 src: ['dist/src/tests/**/*.js']
198 }
199 },
200
201 ts: {
202 default: {
203 tsconfig: {
204 tsconfig: './tsconfig.json',
205 passThrough: true,
206 updateFiles: true,
207 overwriteFiles: true,
208 }
209 },
210 'test-data': {
211 tsconfig: {
212 tsconfig: './tsconfig.testdata.json',
213 passThrough: true,
214 updateFiles: true,
215 overwriteFiles: true
216 }
217 }
218 },
219
220 tslint: {
221 options: {
222 rulesDirectory: 'dist/src'
223 },
224 prod: {
225 options: {
226 configuration: grunt.file.readJSON("tslint.json", { encoding: 'UTF-8' })
227 },
228 files: {
229 src: [
230 'src/**/*.ts',
231 '!src/tests/**'
232 ]
233 }
234 },
235 tests: {
236 options: {
237 configuration: Object.assign({},
238 grunt.file.readJSON("tslint.json", { encoding: 'UTF-8' }),
239 grunt.file.readJSON("src/tests/tslint.json", { encoding: 'UTF-8' })
240 ),
241 },
242 files: {
243 src: [
244 'src/tests/**/*.ts',
245 '!src/tests/references.ts'
246 ]
247 }
248 }
249 },
250
251 watch: {
252 scripts: {
253 files: [
254 './src/**/*.ts',
255 './tests/**/*.ts'
256 ],
257 tasks: [
258 'ts',
259 'mochaTest',
260 'tslint'
261 ]
262 }
263 }
264
265 });
266
267 require('load-grunt-tasks')(grunt); // loads all grunt-* npm tasks
268 require('time-grunt')(grunt);
269
270 grunt.registerTask('create-package-json-for-npm', 'A task that creates a package.json file for the npm module', function () {
271 var basePackageJson = grunt.file.readJSON('package.json', { encoding: 'UTF-8' });
272 delete basePackageJson.devDependencies;
273 grunt.file.write('dist/build/package.json', JSON.stringify(basePackageJson, null, 2), { encoding: 'UTF-8' });
274 });
275
276 grunt.registerTask('validate-debug-mode', 'A task that makes sure ErrorTolerantWalker.DEBUG is false', function () {
277 // DON'T MAKE A RELEASE IN DEBUG MODE
278 var fileText = grunt.file.read('src/utils/ErrorTolerantWalker.ts', { encoding: 'UTF-8' });
279 if (fileText.indexOf('DEBUG: boolean = false') === -1) {
280 grunt.fail.warn('ErrorTolerantWalker.DEBUG is turned on. Turn off debugging to make a release');
281 }
282 });
283
284 grunt.registerTask('validate-documentation', 'A task that validates that all rules defined in src are documented in README.md\n' +
285 'and validates that the package.json version is the same version defined in README.md', function () {
286
287 var readmeText = grunt.file.read('README.md', { encoding: 'UTF-8' });
288 var packageJson = grunt.file.readJSON('package.json', { encoding: 'UTF-8' });
289 getAllRuleNames({ skipTsLintRules: true }).forEach(function(ruleName) {
290 if (readmeText.indexOf(ruleName) === -1) {
291 grunt.fail.warn('A rule was found that is not documented in README.md: ' + ruleName);
292 }
293 });
294 getAllFormatterNames().forEach(function(formatterName) {
295 if (readmeText.indexOf(formatterName) === -1) {
296 grunt.fail.warn('A formatter was found that is not documented in README.md: ' + formatterName);
297 }
298 });
299
300 // if (readmeText.indexOf('\nVersion ' + packageJson.version + ' ') === -1) {
301 // grunt.fail.warn('Version not documented in README.md correctly.\n' +
302 // 'package.json declares: ' + packageJson.version + '\n' +
303 // 'README.md declares something different.');
304 // }
305 });
306
307 grunt.registerTask('validate-config', 'A task that makes sure all the rules in the project are defined to run during the build.', function () {
308
309 var tslintConfig = grunt.file.readJSON('tslint.json', { encoding: 'UTF-8' });
310 var rulesToSkip = {
311 'ban-types': true,
312 'match-default-export-name': true, // requires type checking
313 'newline-before-return': true, // kind of a silly rule
314 'no-non-null-assertion': true, // in fact we prefer the opposite rule
315 'prefer-template': true, // rule does not handle multi-line strings nicely
316 'return-undefined': true, // requires type checking
317 'no-unused-variable': true, // requires type checking
318 'no-unexternalized-strings': true, // this is a VS Code specific rule
319 'no-relative-imports': true, // this project uses relative imports
320 'no-empty-line-after-opening-brace': true, // too strict
321 'align': true, // no need
322 'comment-format': true, // no need
323 'interface-name': true, // no need
324 'max-file-line-count': true, // no need
325 'member-ordering': true, // too strict
326 'no-inferrable-types': true, // we prefer the opposite
327 'ordered-imports': true, // too difficult to turn on
328 'typedef-whitespace': true, // too strict
329 'completed-docs': true, // no need
330 'cyclomatic-complexity': true, // too strict
331 'file-header': true, // no need
332 'max-classes-per-file': true // no need
333 };
334 var errors = [];
335 getAllRuleNames().forEach(function(ruleName) {
336 if (rulesToSkip[ruleName]) {
337 return;
338 }
339 if (tslintConfig.rules[ruleName] !== true && tslintConfig.rules[ruleName] !== false) {
340 if (tslintConfig.rules[ruleName] == null || tslintConfig.rules[ruleName][0] !== true) {
341 errors.push('A rule was found that is not enabled on the project: ' + ruleName);
342 }
343 }
344 });
345
346 if (errors.length > 0) {
347 grunt.fail.warn(errors.join('\n'));
348 }
349 });
350
351 grunt.registerTask('generate-sdl-report', 'A task that generates an SDL report in csv format', function () {
352
353 const rows = [];
354 const resolution = 'See description on the tslint or tslint-microsoft-contrib website';
355 const procedure = 'TSLint Procedure';
356 const header = 'Title,Description,ErrorID,Tool,IssueClass,IssueType,SDL Bug Bar Severity,' +
357 'SDL Level,Resolution,SDL Procedure,CWE,CWE Description';
358 getAllRules().forEach(function(ruleFile) {
359 const metadata = getMetadataFromFile(ruleFile);
360
361 const issueClass = getMetadataValue(metadata, 'issueClass');
362 if (issueClass === 'Ignored') {
363 return;
364 }
365 const ruleName = getMetadataValue(metadata, 'ruleName');
366 const errorId = 'TSLINT' + hash(ruleName);
367 const issueType = getMetadataValue(metadata, 'issueType');
368 const severity = getMetadataValue(metadata, 'severity');
369 const level = getMetadataValue(metadata, 'level');
370 const description = getMetadataValue(metadata, 'description');
371 const cwe = getMetadataValue(metadata, 'commonWeaknessEnumeration', true, false);
372 const cweDescription = createCweDescription(metadata);
373
374 const row = `${ruleName},${description},${errorId},tslint,${issueClass},${issueType},${severity},${level},${resolution},${procedure},${cwe},${cweDescription}`;
375 rows.push(row);
376 });
377 rows.sort();
378 rows.unshift(header);
379 grunt.file.write('tslint-warnings.csv', rows.join('\n'), {encoding: 'UTF-8'});
380
381 });
382
383 grunt.registerTask('generate-recommendations', 'A task that generates the recommended_ruleset.js file', function () {
384
385 const groupedRows = {
386 'Security': [],
387 'Correctness': [],
388 'Clarity': [],
389 'Whitespace': [],
390 'Configurable': [],
391 'Deprecated': [],
392 'Accessibility': [],
393 };
394 const warnings = [];
395
396 getAllRules().forEach(function(ruleFile) {
397 const metadata = getMetadataFromFile(ruleFile);
398
399 const groupName = getMetadataValue(metadata, 'group');
400 if (groupName === 'Ignored') {
401 return;
402 }
403 if (groupName === '') {
404 warnings.push('Could not generate recommendation for rule file: ' + ruleFile);
405 }
406
407 let recommendation = getMetadataValue(metadata, 'recommendation', true, true);
408 if (recommendation === '') {
409 recommendation = 'true,';
410 }
411 const ruleName = getMetadataValue(metadata, 'ruleName');
412 groupedRows[groupName].push(` "${ruleName}": ${recommendation}`);
413 });
414
415 if (warnings.length > 0) {
416 grunt.fail.warn('\n' + warnings.join('\n'));
417 }
418 _.values(groupedRows).forEach(function (element) { element.sort(); });
419
420 let data = grunt.file.read('./templates/recommended_ruleset.js.snippet', {encoding: 'UTF-8'});
421 data = data.replace('%security_rules%', groupedRows['Security'].join('\n'));
422 data = data.replace('%correctness_rules%', groupedRows['Correctness'].join('\n'));
423 data = data.replace('%clarity_rules%', groupedRows['Clarity'].join('\n'));
424 data = data.replace('%whitespace_rules%', groupedRows['Whitespace'].join('\n'));
425 data = data.replace('%configurable_rules%', groupedRows['Configurable'].join('\n'));
426 data = data.replace('%deprecated_rules%', groupedRows['Deprecated'].join('\n'));
427 data = data.replace('%accessibilityy_rules%', groupedRows['Accessibility'].join('\n'));
428 grunt.file.write('recommended_ruleset.js', data, {encoding: 'UTF-8'});
429 });
430
431 grunt.registerTask('generate-default-tslint-json', 'A task that converts recommended_ruleset.js to ./dist/build/tslint.json', function () {
432 const data = require('./recommended_ruleset.js');
433 data['rulesDirectory'] = './';
434 grunt.file.write('./dist/build/tslint.json', JSON.stringify(data, null, 2), {encoding: 'UTF-8'});
435 });
436
437 grunt.registerTask('create-rule', 'A task that creates a new rule from the rule templates. --rule-name parameter required', function () {
438
439 function applyTemplates(source) {
440 return source.replace(/%RULE_NAME%/gm, ruleName)
441 .replace(/%RULE_FILE_NAME%/gm, ruleFile)
442 .replace(/%WALKER_NAME%/gm, walkerName);
443 }
444
445 var ruleName = grunt.option('rule-name');
446 if (!ruleName) {
447 grunt.fail.warn('--rule-name parameter is required');
448 } else {
449
450 var ruleFile = camelCase(ruleName) + 'Rule';
451 var sourceFileName = './src/' + ruleFile + '.ts';
452 var testFileName = './src/tests/' + ruleFile.charAt(0).toUpperCase() + ruleFile.substr(1) + 'Tests.ts';
453 var walkerName = ruleFile.charAt(0).toUpperCase() + ruleFile.substr(1) + 'Walker';
454
455 var ruleTemplateText = grunt.file.read('./templates/rule.snippet', {encoding: 'UTF-8'});
456 var testTemplateText = grunt.file.read('./templates/rule-tests.snippet', {encoding: 'UTF-8'});
457
458 grunt.file.write(sourceFileName, applyTemplates(ruleTemplateText), {encoding: 'UTF-8'});
459 grunt.file.write(testFileName, applyTemplates(testTemplateText), {encoding: 'UTF-8'});
460
461 var currentRuleset = grunt.file.readJSON('./tslint.json', {encoding: 'UTF-8'});
462 currentRuleset.rules[ruleName] = true;
463 grunt.file.write('./tslint.json', JSON.stringify(currentRuleset, null, 2), {encoding: 'UTF-8'});
464 }
465 });
466
467 grunt.registerTask('all', 'Performs a cleanup and a full build with all tasks', [
468 'clean',
469 'copy:json',
470 'ts',
471 'mochaTest',
472 'tslint',
473 'validate-documentation',
474 'validate-config',
475 'validate-debug-mode',
476 'copy:package',
477 'generate-recommendations',
478 'generate-default-tslint-json',
479 'generate-sdl-report',
480 'create-package-json-for-npm'
481 ]);
482
483 grunt.registerTask('default', ['all']);
484
485};