Skip to content
This repository has been archived by the owner on Jul 15, 2023. It is now read-only.

Used inquirer to prompt for properties when creating a new rule. #679

Merged
merged 4 commits into from
Dec 18, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,13 @@ npm install
npm test
```

You can create new rule from template with `create-rule` script:
You can create a new rule from a template with the `create-rule` script:

```shell
npm run create-rule -- --rule-name=no-something-or-other
npm run create-rule
```

> NOTE: `--` is required before script arguments.

This script will create file for rule implementation (inside `src`) as well as folder with rule tests (inside `test`).
This will prompt you to enter the details of the new rule. Once you're done, it will create a file for the rule implementation (inside `src`) as well as folder with rule tests (inside `test`).

More information about writing rule tests can be found in [TSLint documentation](https://palantir.github.io/tslint/develop/testing-rules/).

Expand Down
176 changes: 121 additions & 55 deletions build-tasks/create-rule.js
Original file line number Diff line number Diff line change
@@ -1,79 +1,145 @@
const fs = require('fs');
const { red } = require('chalk');
const { readJSON, writeFile } = require('./common/files');

const ruleName = getRuleName();
validateAguments();

const ruleFile = camelCase(ruleName) + 'Rule';
const sourceFileName = 'src/' + ruleFile + '.ts';
const testsFolder = 'tests/' + ruleName;
const testFile = testsFolder + '/test.ts.lint';
const lintFile = testsFolder + '/tslint.json';

createImplementationFile();
createTestFiles();
addToConfig();

console.log('Rule created');
console.log('Rule source: ' + sourceFileName);
console.log('Test file: ' + testFile);

function getRuleName() {
const option = process.argv.find(str => str.startsWith('--rule-name'));

if (!option) {
return;
}

return option.split('=')[1];
}

function camelCase(input) {
return input.toLowerCase().replace(/-(.)/g, (match, group1) => group1.toUpperCase());
}

function validateAguments() {
const USAGE_EXAMPLE = '\nUsage example:\nnpm run create-rule -- --rule-name=no-something-or-other\n';

if (!ruleName) {
console.log(red('--rule-name parameter is required.' + USAGE_EXAMPLE));
process.exit(1);
const inquirer = require('inquirer');
const { execSync } = require('child_process');
const { writeFile } = require('./common/files');

const questions = [
{
name: 'name',
message: 'Name:',
type: 'input',
validate: value => {
if (!/^[a-z0-9]+(\-[a-z0-9]+)*$/.test(value)) {
return 'The name should consist of lowercase letters and numbers separated with "-" character.';
}

return true;
}
},
{
name: 'description',
message: 'Description:',
type: 'input',
validate: value => {
if (!!value && !!value.trim()) {
return true;
}
return 'Please enter a description for the rule.';
}
},
{
name: 'type',
message: 'Rule type:',
type: 'list',
choices: ['functionality', 'maintainability', 'style', 'typescript'],
default: 'maintainability'
},
{
name: 'issueClass',
message: 'Issue class:',
type: 'list',
choices: ['SDL', 'Non-SDL', 'Ignored'],
default: 'Non-SDL'
},
{
name: 'issueType',
message: 'Issue type:',
type: 'list',
choices: ['Error', 'Warning'],
default: 'Warning'
},
{
name: 'severity',
message: 'Severity:',
type: 'list',
choices: ['Critical', 'Important', 'Moderate', 'Low'],
default: 'Low'
},
{
name: 'level',
message: 'Level:',
type: 'list',
choices: ['Mandatory', 'Opportunity for Excellence'],
default: 'Opportunity for Excellence'
},
{
name: 'group',
message: 'Group:',
type: 'list',
choices: ['Clarity', 'Configurable', 'Correctness', 'Deprecated', 'Ignored', 'Security', 'Whitespace'],
default: 'Clarity'
}

if (!/^[a-z0-9]+(\-[a-z0-9]+)*$/.test(ruleName)) {
console.log(red('Rule name should consist of lowercase letters and numbers separated with "-" character.' + USAGE_EXAMPLE));
process.exit(1);
];

inquirer.prompt(questions).then(answers => {
const sourceFileName = createImplementationFile(answers);
const testFileName = createTestFiles(answers.name);

console.log(`Rule '${answers.name}' created.`);
console.log(`Source file: ${sourceFileName}`);
console.log(`Test file: ${testFileName}`);

// If we're running in the VS Code terminal, try to open the
// new files. If we can't do it, then it's not a big deal.
if (process.env.VSCODE_CWD) {
reduckted marked this conversation as resolved.
Show resolved Hide resolved
try {
execSync(`code "${testFileName}"`);
reduckted marked this conversation as resolved.
Show resolved Hide resolved
execSync(`code "${sourceFileName}"`);
} catch (ex) {
// Couldn't open VS Code.
console.log(ex);
}
}
}
});

function createImplementationFile() {
const walkerName = ruleFile.charAt(0).toUpperCase() + ruleFile.substr(1) + 'Walker';
function createImplementationFile(answers) {
const ruleFile = camelCase(answers.name) + 'Rule';
const sourceFileName = 'src/' + ruleFile + '.ts';
const walkerName = pascalCase(ruleFile) + 'Walker';

const ruleTemplate = require('./templates/rule.template');
const ruleSource = ruleTemplate({ ruleName, walkerName });
const ruleSource = ruleTemplate({
ruleName: answers.name,
walkerName,
type: answers.type,
description: answers.description,
issueClass: answers.issueClass,
issueType: answers.issueType,
severity: answers.severity,
level: answers.level,
group: answers.group
});

writeFile(sourceFileName, ruleSource);

return sourceFileName;
}

function createTestFiles() {
function createTestFiles(ruleName) {
const testsFolder = 'tests/' + ruleName;
const testFile = testsFolder + '/test.ts.lint';
const lintFile = testsFolder + '/tslint.json';
const testContent = '// Code that should be checked by rule';
const tslintContent = {
rules: {
[ruleName]: true
}
};

fs.mkdirSync(testsFolder);
if (!fs.existsSync(testsFolder)) {
fs.mkdirSync(testsFolder);
}

writeFile(testFile, testContent);
writeFile(lintFile, JSON.stringify(tslintContent, undefined, 4));
}

function addToConfig() {
const currentRuleset = readJSON('tslint.json');
return testFile;
}

currentRuleset.rules[ruleName] = true;
function camelCase(input) {
return input.toLowerCase().replace(/-(.)/g, (match, group1) => group1.toUpperCase());
}

writeFile('tslint.json', JSON.stringify(currentRuleset, undefined, 4));
function pascalCase(input) {
return input.charAt(0).toUpperCase() + input.substr(1);
}
33 changes: 14 additions & 19 deletions build-tasks/templates/rule.template.js
Original file line number Diff line number Diff line change
@@ -1,42 +1,37 @@
module.exports = ({ ruleName, walkerName }) =>
module.exports = ({ ruleName, walkerName, type, description, issueClass, issueType, severity, level, group }) =>
`import * as ts from 'typescript';
import * as Lint from 'tslint';

import {ExtendedMetadata} from './utils/ExtendedMetadata';
// use (and contribute to) AstUtils for common AST functions // TODO: delete comment
import {AstUtils} from './utils/AstUtils';
// use Utils instead of Underscore functions // TODO: delete comment
import {Utils} from './utils/Utils';
import { ExtendedMetadata } from './utils/ExtendedMetadata';
import { AstUtils } from './utils/AstUtils';
import { Utils } from './utils/Utils';

const FAILURE_STRING: string = 'Some error message: '; // TODO: Define an error message

export class Rule extends Lint.Rules.AbstractRule {

public static metadata: ExtendedMetadata = {
ruleName: '${ruleName}',
type: 'maintainability', // one of: 'functionality' | 'maintainability' | 'style' | 'typescript'
description: '... add a meaningful one line description',
type: '${type}',
description: '${description}',
options: null, // tslint:disable-line:no-null-keyword
optionsDescription: '',
reduckted marked this conversation as resolved.
Show resolved Hide resolved
optionExamples: [], // Remove this property if the rule has no options
optionExamples: [], // TODO: Remove this property if the rule has no options
typescriptOnly: false,
reduckted marked this conversation as resolved.
Show resolved Hide resolved
issueClass: 'Non-SDL', // one of: 'SDL' | 'Non-SDL' | 'Ignored'
issueType: 'Warning', // one of: 'Error' | 'Warning'
severity: 'Low', // one of: 'Critical' | 'Important' | 'Moderate' | 'Low'
level: 'Opportunity for Excellence', // one of 'Mandatory' | 'Opportunity for Excellence'
group: 'Clarity', // one of 'Ignored' | 'Security' | 'Correctness' | 'Clarity' | 'Whitespace' | 'Configurable' | 'Deprecated'
commonWeaknessEnumeration: '...' // if possible, please map your rule to a CWE (see cwe_descriptions.json and https://cwe.mitre.org)
issueClass: '${issueClass}',
issueType: '${issueType}',
severity: '${severity}',
level: '${level}',
group: '${group}',
commonWeaknessEnumeration: '...' // if possible, please map your rule to a CWE (see cwe_descriptions.json and https://cwe.mitre.org)
};

public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return this.applyWithWalker(new %WALKER_NAME%(sourceFile, this.getOptions()));
return this.applyWithWalker(new ${walkerName}(sourceFile, this.getOptions()));
}
}

class ${walkerName} extends Lint.RuleWalker {

protected visitNode(node: ts.Node): void {
console.log(ts.SyntaxKind[node.kind] + ' ' + node.getText());
super.visitNode(node);
}
}
Expand Down
Loading