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 all commits
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
211 changes: 156 additions & 55 deletions build-tasks/create-rule.js
Original file line number Diff line number Diff line change
@@ -1,79 +1,180 @@
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;
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: 'typescriptOnly',
message: 'TypeScript only:',
type: 'confirm',
default: false
},
{
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'
}
];

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

function camelCase(input) {
return input.toLowerCase().replace(/-(.)/g, (match, group1) => group1.toUpperCase());
}
inquirer.prompt(questions).then(answers => {
const sourceFileName = createImplementationFile(answers);
const testFileNames = createTestFiles(answers);

function validateAguments() {
const USAGE_EXAMPLE = '\nUsage example:\nnpm run create-rule -- --rule-name=no-something-or-other\n';
console.log(`Rule '${answers.name}' created.`);
console.log(`Source file: ${sourceFileName}`);
console.log(`Test files: ${testFileNames.join(', ')}`);

if (!ruleName) {
console.log(red('--rule-name parameter is required.' + USAGE_EXAMPLE));
process.exit(1);
}
// Attempt to open the files in the current editor.
tryOpenFiles([...testFileNames, sourceFileName]);
});

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);
}
}

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,
typescriptOnly: answers.typescriptOnly,
issueClass: answers.issueClass,
issueType: answers.issueType,
severity: answers.severity,
level: answers.level,
group: answers.group
});

writeFile(sourceFileName, ruleSource);

return sourceFileName;
}

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

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

writeFile(tsTestFile, '// TypeScript code that should be checked by the rule.');
testFiles.push(tsTestFile);

if (!answers.typescriptOnly) {
const jsTestFile = testsFolder + '/test.js.lint';
writeFile(jsTestFile, '// JavaScript code that should be checked by the rule.');
testFiles.push(jsTestFile);
}

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

return testFiles;
}

function addToConfig() {
const currentRuleset = readJSON('tslint.json');
function camelCase(input) {
return input.toLowerCase().replace(/-(.)/g, (match, group1) => group1.toUpperCase());
}

currentRuleset.rules[ruleName] = true;
function pascalCase(input) {
return input.charAt(0).toUpperCase() + input.substr(1);
}

writeFile('tslint.json', JSON.stringify(currentRuleset, undefined, 4));
function tryOpenFiles(files) {
// Check if we're running in the VS Code terminal. If we
// are, then we can try to open the new files in VS Code.
// If we can't open the files, then it's not a big deal.
if (process.env.VSCODE_CWD) {
let exe;

// We need to check if we're running in normal VS Code or the Insiders version.
// The `TERM_PROGRAM_VERSION` environment variable will contain the version number
// of VS Code. For VS Code Insiders, that version will have the suffix "-insider".
const version = process.env.TERM_PROGRAM_VERSION || '';
if (version.endsWith('-insider')) {
exe = 'code-insiders';
console.log('Opening the new files in VS Code Insiders...');
} else {
exe = 'code';
console.log('Opening the new files in VS Code...');
}

try {
files.forEach(fileName => execSync(`${exe} "${fileName}"`));
} catch (ex) {
// Couldn't open VS Code.
console.log(ex);
}
}
}
36 changes: 16 additions & 20 deletions build-tasks/templates/rule.template.js
Original file line number Diff line number Diff line change
@@ -1,42 +1,38 @@
module.exports = ({ ruleName, walkerName }) =>
module.exports = ({ ruleName, walkerName, type, description, typescriptOnly, 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}',
// TODO: Fill in the options and options description, or leave them as they are if there are no options.
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
typescriptOnly: false,
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)
optionExamples: [], // TODO: Remove this property if the rule has no options
typescriptOnly: ${typescriptOnly},
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