1 | "use strict";
|
2 |
|
3 | var _ = require('underscore');
|
4 |
|
5 | module.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 |
|
18 | let hash = 31;
|
19 | let i = 0;
|
20 | for (i = 0; i < input.length; i++) {
|
21 |
|
22 | hash = 31 * hash + input.charCodeAt(i);
|
23 | hash = hash | 0;
|
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+/, '');
|
88 | value = value.replace(/\n/, ' ');
|
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\..*/, '')
|
113 | .replace(/.*\//, '');
|
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\..*/, '')
|
132 | .replace(/.*\//, '');
|
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);
|
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 |
|
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 |
|
301 |
|
302 |
|
303 |
|
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,
|
313 | 'newline-before-return': true,
|
314 | 'no-non-null-assertion': true,
|
315 | 'prefer-template': true,
|
316 | 'return-undefined': true,
|
317 | 'no-unused-variable': true,
|
318 | 'no-unexternalized-strings': true,
|
319 | 'no-relative-imports': true,
|
320 | 'no-empty-line-after-opening-brace': true,
|
321 | 'align': true,
|
322 | 'comment-format': true,
|
323 | 'interface-name': true,
|
324 | 'max-file-line-count': true,
|
325 | 'member-ordering': true,
|
326 | 'no-inferrable-types': true,
|
327 | 'ordered-imports': true,
|
328 | 'typedef-whitespace': true,
|
329 | 'completed-docs': true,
|
330 | 'cyclomatic-complexity': true,
|
331 | 'file-header': true,
|
332 | 'max-classes-per-file': true
|
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 | };
|