Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: make utils/TSESLint export typed classes instead of just types #526

Merged
merged 2 commits into from May 16, 2019
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 5 additions & 2 deletions .eslintrc.json
Expand Up @@ -5,11 +5,14 @@
"es6": true,
"node": true
},
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"rules": {
"comma-dangle": ["error", "always-multiline"],
"curly": ["error", "all"],
"no-dupe-class-members": "off",
"no-mixed-operators": "error",
"no-console": "off",
"no-dupe-class-members": "off",
Expand Down
3 changes: 2 additions & 1 deletion packages/eslint-plugin-tslint/package.json
Expand Up @@ -27,14 +27,15 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@typescript-eslint/experimental-utils": "1.9.0",
"lodash.memoize": "^4.1.2"
},
"peerDependencies": {
"eslint": "^5.0.0",
"tslint": "^5.0.0"
},
"devDependencies": {
"@types/eslint": "^4.16.3",
"@types/json-schema": "^7.0.3",
"@types/lodash.memoize": "^4.1.4",
"@typescript-eslint/parser": "1.9.0"
}
Expand Down
159 changes: 4 additions & 155 deletions packages/eslint-plugin-tslint/src/index.ts
@@ -1,160 +1,9 @@
import { Rule } from 'eslint';
import memoize from 'lodash.memoize';
import { Configuration, RuleSeverity } from 'tslint';
import { Program } from 'typescript';
import { CustomLinter } from './custom-linter';
import { ParserServices } from '@typescript-eslint/typescript-estree';

//------------------------------------------------------------------------------
// Plugin Definition
//------------------------------------------------------------------------------

type RawRuleConfig =
| null
| undefined
| boolean
| any[]
| {
severity?: RuleSeverity | 'warn' | 'none' | 'default';
options?: any;
};

interface RawRulesConfig {
[key: string]: RawRuleConfig;
}
import configRule from './rules/config';

/**
* Construct a configFile for TSLint
* Expose a single rule called "config", which will be accessed in the user's eslint config files
* via "tslint/config"
*/
const tslintConfig = memoize(
(
lintFile: string,
tslintRules: RawRulesConfig,
tslintRulesDirectory: string[],
) => {
if (lintFile != null) {
return Configuration.loadConfigurationFromPath(lintFile);
}
return Configuration.parseConfigFile({
rules: tslintRules || {},
rulesDirectory: tslintRulesDirectory || [],
});
},
(lintFile: string | undefined, tslintRules = {}, tslintRulesDirectory = []) =>
`${lintFile}_${Object.keys(tslintRules).join(',')}_${
tslintRulesDirectory.length
}`,
);

export const rules = {
/**
* Expose a single rule called "config", which will be accessed in the user's eslint config files
* via "tslint/config"
*/
config: {
meta: {
docs: {
description:
'Wraps a TSLint configuration and lints the whole source using TSLint',
category: 'TSLint',
},
schema: [
{
type: 'object',
properties: {
rules: {
type: 'object',
/**
* No fixed schema properties for rules, as this would be a permanently moving target
*/
additionalProperties: true,
},
rulesDirectory: {
type: 'array',
items: {
type: 'string',
},
},
lintFile: {
type: 'string',
},
},
additionalProperties: false,
},
],
},
create(context: Rule.RuleContext) {
const fileName = context.getFilename();
const sourceCode = context.getSourceCode().text;
const parserServices: ParserServices | undefined = context.parserServices;

/**
* The user needs to have configured "project" in their parserOptions
* for @typescript-eslint/parser
*/
if (!parserServices || !parserServices.program) {
throw new Error(
`You must provide a value for the "parserOptions.project" property for @typescript-eslint/parser`,
);
}

/**
* The TSLint rules configuration passed in by the user
*/
const {
rules: tslintRules,
rulesDirectory: tslintRulesDirectory,
lintFile,
} = context.options[0];

const program: Program = parserServices.program;

/**
* Create an instance of TSLint
* Lint the source code using the configured TSLint instance, and the rules which have been
* passed via the ESLint rule options for this rule (using "tslint/config")
*/
const tslintOptions = {
formatter: 'json',
fix: false,
};
const tslint = new CustomLinter(tslintOptions, program);
const configuration = tslintConfig(
lintFile,
tslintRules,
tslintRulesDirectory,
);
tslint.lint(fileName, sourceCode, configuration);

const result = tslint.getResult();

/**
* Format the TSLint results for ESLint
*/
if (result.failures && result.failures.length) {
result.failures.forEach(failure => {
const start = failure.getStartPosition().getLineAndCharacter();
const end = failure.getEndPosition().getLineAndCharacter();
context.report({
message: `${failure.getFailure()} (tslint:${failure.getRuleName()})`,
loc: {
start: {
line: start.line + 1,
column: start.character,
},
end: {
line: end.line + 1,
column: end.character,
},
},
});
});
}

/**
* Return an empty object for the ESLint rule
*/
return {};
},
},
config: configRule,
};
176 changes: 176 additions & 0 deletions packages/eslint-plugin-tslint/src/rules/config.ts
@@ -0,0 +1,176 @@
import {
ESLintUtils,
ParserServices,
} from '@typescript-eslint/experimental-utils';
import memoize from 'lodash.memoize';
import { Configuration, RuleSeverity } from 'tslint';
import { CustomLinter } from '../custom-linter';

// note - cannot migrate this to an import statement because it will make TSC copy the package.json to the dist folder
const version = require('../../package.json').version;

const createRule = ESLintUtils.RuleCreator(
() =>
`https://github.com/typescript-eslint/typescript-eslint/blob/v${version}/packages/eslint-plugin-tslint/README.md`,
);
export type RawRulesConfig = Record<
string,
| null
| undefined
| boolean
| any[]
| {
severity?: RuleSeverity | 'warn' | 'none' | 'default';
options?: any;
}
>;

export type MessageIds = 'failure';
export type Options = [
{
rules?: RawRulesConfig;
rulesDirectory?: string[];
lintFile?: string;
}
];

/**
* Construct a configFile for TSLint
*/
const tslintConfig = memoize(
(
lintFile?: string,
tslintRules?: RawRulesConfig,
tslintRulesDirectory?: string[],
) => {
if (lintFile != null) {
return Configuration.loadConfigurationFromPath(lintFile);
}
return Configuration.parseConfigFile({
rules: tslintRules || {},
rulesDirectory: tslintRulesDirectory || [],
});
},
(lintFile: string | undefined, tslintRules = {}, tslintRulesDirectory = []) =>
`${lintFile}_${Object.keys(tslintRules).join(',')}_${
tslintRulesDirectory.length
}`,
);

export default createRule<Options, MessageIds>({
name: 'config',
meta: {
docs: {
description:
'Wraps a TSLint configuration and lints the whole source using TSLint',
category: 'TSLint' as any,
recommended: false,
},
type: 'problem',
messages: {
failure: '{{message}} (tslint:{{ruleName}})`',
},
schema: [
{
type: 'object',
properties: {
rules: {
type: 'object',
/**
* No fixed schema properties for rules, as this would be a permanently moving target
*/
additionalProperties: true,
},
rulesDirectory: {
type: 'array',
items: {
type: 'string',
},
},
lintFile: {
type: 'string',
},
},
additionalProperties: false,
},
],
},
defaultOptions: [] as any,
create(context) {
const fileName = context.getFilename();
const sourceCode = context.getSourceCode().text;
const parserServices: ParserServices | undefined = context.parserServices;

/**
* The user needs to have configured "project" in their parserOptions
* for @typescript-eslint/parser
*/
if (!parserServices || !parserServices.program) {
throw new Error(
`You must provide a value for the "parserOptions.project" property for @typescript-eslint/parser`,
);
}

/**
* The TSLint rules configuration passed in by the user
*/
const {
rules: tslintRules,
rulesDirectory: tslintRulesDirectory,
lintFile,
} = context.options[0];

const program = parserServices.program;

/**
* Create an instance of TSLint
* Lint the source code using the configured TSLint instance, and the rules which have been
* passed via the ESLint rule options for this rule (using "tslint/config")
*/
const tslintOptions = {
formatter: 'json',
fix: false,
};
const tslint = new CustomLinter(tslintOptions, program);
const configuration = tslintConfig(
lintFile,
tslintRules,
tslintRulesDirectory,
);
tslint.lint(fileName, sourceCode, configuration);

const result = tslint.getResult();

/**
* Format the TSLint results for ESLint
*/
if (result.failures && result.failures.length) {
result.failures.forEach(failure => {
const start = failure.getStartPosition().getLineAndCharacter();
const end = failure.getEndPosition().getLineAndCharacter();
context.report({
messageId: 'failure',
data: {
message: failure.getFailure(),
ruleName: failure.getRuleName(),
},
loc: {
start: {
line: start.line + 1,
column: start.character,
},
end: {
line: end.line + 1,
column: end.character,
},
},
});
});
}

/**
* Return an empty object for the ESLint rule
*/
return {};
},
});