@magicspace/tslint-rules
Advanced tools
Comparing version 0.1.8 to 0.1.9
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.BASE_TYPES = [ | ||
"boolean", | ||
"number", | ||
"string", | ||
"array", | ||
"tuple", | ||
"null", | ||
"undefined", | ||
"never", | ||
"void" | ||
]; | ||
//# sourceMappingURL=@lang.js.map |
@@ -6,7 +6,13 @@ "use strict"; | ||
const typescript_1 = require("typescript"); | ||
const _lang_1 = require("../@lang"); | ||
const failure_manager_1 = require("../utils/failure-manager"); | ||
const ERROR_MESSAGE_EXPLICIT_RETURN_TYPE_REQUIRED = 'This function requires explicit return type.'; | ||
let typeChecker; | ||
class Rule extends tslint_1.Rules.AbstractRule { | ||
constructor(options) { | ||
super(options); | ||
this.parseOptions = options.ruleArguments[0] || { complexTypeFixer: false }; | ||
} | ||
apply(sourceFile) { | ||
return this.applyWithWalker(new ExplicitReturnTypeWalker(sourceFile, Rule.metadata.ruleName, undefined)); | ||
return this.applyWithWalker(new ExplicitReturnTypeWalker(sourceFile, Rule.metadata.ruleName, this.parseOptions)); | ||
} | ||
@@ -18,3 +24,7 @@ } | ||
optionsDescription: '', | ||
options: undefined, | ||
options: { | ||
properties: { | ||
complexTypeFixer: { type: 'boolean' } | ||
} | ||
}, | ||
type: 'maintainability', | ||
@@ -26,5 +36,11 @@ hasFix: true, | ||
class ExplicitReturnTypeWalker extends tslint_1.AbstractWalker { | ||
constructor() { | ||
super(...arguments); | ||
constructor(sourceFile, ruleName, options) { | ||
super(sourceFile, ruleName, options); | ||
this.failureManager = new failure_manager_1.FailureManager(this); | ||
this.typeChecker = typeChecker || (typeChecker = typescript_1.createProgram([this.sourceFile.fileName], { | ||
noEmitOnError: true, | ||
noImplicitAny: true, | ||
target: typescript_1.ScriptTarget.Latest, | ||
module: typescript_1.ModuleKind.CommonJS, | ||
}).getTypeChecker()); | ||
} | ||
@@ -42,2 +58,3 @@ walk(sourceFile) { | ||
message: ERROR_MESSAGE_EXPLICIT_RETURN_TYPE_REQUIRED, | ||
fixer: this.buildFixer(node), | ||
}); | ||
@@ -51,2 +68,30 @@ } | ||
} | ||
getReturnType(node) { | ||
let type = this.typeChecker.typeToString(this.typeChecker.getTypeAtLocation(node).getCallSignatures()[0].getReturnType()); | ||
if (!this.options.complexTypeFixer && !_lang_1.BASE_TYPES.some(v => v === type)) { | ||
console.log(type); | ||
console.log(type); | ||
console.log(type); | ||
console.log(type); | ||
console.log(type); | ||
console.log(type); | ||
return undefined; | ||
} | ||
try { | ||
return type; | ||
} | ||
catch (e) { | ||
return undefined; | ||
} | ||
} | ||
buildFixer(node) { | ||
function typeFactory(type) { | ||
return `: ${type} `; | ||
} | ||
let body = node.body; | ||
let returnType = this.getReturnType(node); | ||
return body && returnType | ||
? new tslint_1.Replacement(node.getChildren().find(v => v.getText() === ")").getEnd(), 0, typeFactory(returnType)) | ||
: undefined; | ||
} | ||
checkReturnType(node) { | ||
@@ -99,2 +144,20 @@ if (node.type) { | ||
} | ||
function Z() { | ||
let E; | ||
(function (E) { | ||
E[E["a"] = 0] = "a"; | ||
E[E["b"] = 1] = "b"; | ||
})(E || (E = {})); | ||
return E; | ||
} | ||
let foo = () => { | ||
return 1; | ||
}; | ||
let a = () => { }; | ||
// let b = () => { | ||
// return { | ||
// name: "string" | ||
// } | ||
// } | ||
let b = () => { }; | ||
//# sourceMappingURL=explicitReturnTypeRule.js.map |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const Path = require("path"); | ||
const resolve = require("resolve"); | ||
const tslint_1 = require("tslint"); | ||
const tsutils_1 = require("tsutils"); | ||
const TypeScript = require("typescript"); | ||
const path_1 = require("../utils/path"); | ||
const ERROR_MESSAGE_UNEXPECTED_EMPTY_LINE = 'Unexpected empty line within the same import group.'; | ||
const ERROR_MESSAGE_EXPECTING_EMPTY_LINE = 'Expecting an empty line between different import groups.'; | ||
const ERROR_MESSAGE_WRONG_MODULE_GROUP_ORDER = 'Import groups must be sorted according to given order.'; | ||
const ERROR_MESSAGE_NOT_GROUPED = 'Imports must be grouped.'; | ||
const resolveWithCache = (() => { | ||
let cache = new Map(); | ||
return (id, basedir) => { | ||
let key = `${id}\n${basedir}`; | ||
if (cache.has(key)) { | ||
return cache.get(key); | ||
} | ||
let value; | ||
try { | ||
value = resolve.sync(id, { basedir }); | ||
} | ||
catch (error) { } | ||
cache.set(key, value); | ||
return value; | ||
}; | ||
})(); | ||
const BUILT_IN_MODULE_GROUP_TESTER_DICT = { | ||
'$node-core'(path) { | ||
try { | ||
return require.resolve(path) === path; | ||
} | ||
catch (error) { | ||
return false; | ||
} | ||
}, | ||
'$node-modules'(modulePath, sourceFilePath) { | ||
let basedir = Path.dirname(sourceFilePath); | ||
let resolvedPath = resolveWithCache(modulePath, basedir); | ||
if (!resolvedPath) { | ||
return false; | ||
} | ||
let relativePath = Path.relative(basedir, resolvedPath); | ||
return /[\\/]node_modules[\\/]/.test(relativePath); | ||
}, | ||
}; | ||
class ModuleGroup { | ||
constructor({ name, test: testConfig }) { | ||
this.name = name; | ||
this.tester = this.buildTester(testConfig); | ||
} | ||
test(modulePath, sourceFilePath) { | ||
return this.tester(modulePath, sourceFilePath); | ||
} | ||
buildTester(config) { | ||
if (config.startsWith('$')) { | ||
return (BUILT_IN_MODULE_GROUP_TESTER_DICT[config] || (() => false)); | ||
} | ||
else { | ||
let regex = new RegExp(config); | ||
return (path) => regex.test(path); | ||
} | ||
} | ||
} | ||
class Rule extends tslint_1.Rules.AbstractRule { | ||
constructor(options) { | ||
super(options); | ||
let { groups: groupConfigItems, ordered } = options | ||
.ruleArguments[0]; | ||
this.parsedOptions = { | ||
groups: groupConfigItems.map(item => new ModuleGroup(item)), | ||
ordered: !!ordered, | ||
}; | ||
} | ||
apply(sourceFile) { | ||
return this.applyWithWalker(new ImportGroupWalker(sourceFile, Rule.metadata.ruleName, this.parsedOptions)); | ||
} | ||
} | ||
Rule.metadata = { | ||
ruleName: 'import-groups', | ||
description: 'Validate that module imports are grouped as expected.', | ||
optionsDescription: '', | ||
options: { | ||
properties: { | ||
groups: { | ||
items: { | ||
properties: { | ||
name: { | ||
type: 'string', | ||
}, | ||
test: { | ||
type: 'string', | ||
}, | ||
}, | ||
type: 'object', | ||
}, | ||
type: 'array', | ||
}, | ||
ordered: { | ||
type: 'boolean', | ||
}, | ||
}, | ||
type: 'object', | ||
}, | ||
optionExamples: [ | ||
[ | ||
true, | ||
{ | ||
groups: [ | ||
{ name: 'node-core', test: '$node-core' }, | ||
{ name: 'node-modules', test: '$node-modules' }, | ||
], | ||
ordered: true, | ||
}, | ||
], | ||
], | ||
type: 'maintainability', | ||
hasFix: true, | ||
typescriptOnly: false, | ||
}; | ||
exports.Rule = Rule; | ||
class ImportGroupWalker extends tslint_1.AbstractWalker { | ||
constructor() { | ||
super(...arguments); | ||
this.moduleImportInfos = []; | ||
this.pendingStatements = []; | ||
} | ||
walk(sourceFile) { | ||
let pendingCache = []; | ||
let checkWithAppendModuleImport = (expression) => { | ||
this.pendingStatements.push(...pendingCache); | ||
pendingCache = []; | ||
if (tsutils_1.isTextualLiteral(expression)) { | ||
this.appendModuleImport(expression, sourceFile); | ||
} | ||
}; | ||
for (let statement of sourceFile.statements) { | ||
if (tsutils_1.isImportDeclaration(statement)) { | ||
if (1 /* ImportDeclaration */) { | ||
checkWithAppendModuleImport(statement.moduleSpecifier); | ||
} | ||
} | ||
else if (tsutils_1.isImportEqualsDeclaration(statement)) { | ||
if (2 /* ImportEquals */ && | ||
statement.moduleReference.kind === | ||
TypeScript.SyntaxKind.ExternalModuleReference && | ||
statement.moduleReference.expression !== undefined) { | ||
checkWithAppendModuleImport(statement.moduleReference.expression); | ||
} | ||
} | ||
else { | ||
pendingCache.push(statement); | ||
} | ||
} | ||
this.validate(); | ||
} | ||
appendModuleImport(expression, sourceFile) { | ||
let node = expression; | ||
while (node.parent.kind !== TypeScript.SyntaxKind.SourceFile) { | ||
node = node.parent; | ||
} | ||
let modulePath = path_1.removeQuotes(expression.getText()); | ||
let sourceFilePath = sourceFile.fileName; | ||
let groups = this.options.groups; | ||
let index = groups.findIndex(group => group.test(modulePath, sourceFilePath)); | ||
this.moduleImportInfos.push({ | ||
node, | ||
// 如果没有找到匹配的分组, 则归到 "其他" 一组, groupIndex 为 groups.length. | ||
groupIndex: index < 0 ? groups.length : index, | ||
startLine: sourceFile.getLineAndCharacterOfPosition(node.getStart()).line, | ||
endLine: sourceFile.getLineAndCharacterOfPosition(node.getEnd()).line, | ||
}); | ||
} | ||
validate() { | ||
let infos = this.moduleImportInfos; | ||
let pendingStatements = this.pendingStatements; | ||
if (!infos.length) { | ||
return; | ||
} | ||
let { ordered } = this.options; | ||
let failureItems = []; | ||
let [lastInfo, ...restInfos] = infos; | ||
let fixerEnabled = !pendingStatements.length; | ||
let appearedGroupIndexSet = new Set([lastInfo.groupIndex]); | ||
for (let expression of pendingStatements) { | ||
failureItems.push({ | ||
node: expression, | ||
message: 'Unexpected code between import statements.', | ||
}); | ||
} | ||
for (let info of restInfos) { | ||
let checkOrdering = ordered; | ||
if (info.groupIndex === lastInfo.groupIndex) { | ||
// 只在分组第一项检查分组顺序. | ||
checkOrdering = false; | ||
// 如果当前分组和上一份组 groupIndex 相同, 则校验是否多了空行. | ||
if (info.startLine - lastInfo.endLine > 1) { | ||
failureItems.push({ | ||
node: info.node, | ||
message: ERROR_MESSAGE_UNEXPECTED_EMPTY_LINE, | ||
}); | ||
} | ||
} | ||
else { | ||
// 检验该组是否已经出现过. | ||
if (appearedGroupIndexSet.has(info.groupIndex)) { | ||
checkOrdering = false; | ||
failureItems.push({ | ||
node: info.node, | ||
message: ERROR_MESSAGE_NOT_GROUPED, | ||
}); | ||
} | ||
// 如果未出现过则校验是否少了空行. | ||
else if (info.startLine - lastInfo.endLine < 2) { | ||
failureItems.push({ | ||
node: info.node, | ||
message: ERROR_MESSAGE_EXPECTING_EMPTY_LINE, | ||
}); | ||
} | ||
} | ||
if (checkOrdering) { | ||
// 在要求分组顺序的情况下, 如果当前分组的 groupIndex 小于上一个分组的, | ||
// 则说明顺序错误. | ||
if (info.groupIndex < lastInfo.groupIndex) { | ||
failureItems.push({ | ||
node: info.node, | ||
message: ERROR_MESSAGE_WRONG_MODULE_GROUP_ORDER, | ||
}); | ||
} | ||
} | ||
appearedGroupIndexSet.add(info.groupIndex); | ||
lastInfo = info; | ||
} | ||
if (failureItems.length) { | ||
let fixer = fixerEnabled ? this.buildFixer(infos) : undefined; | ||
for (let { node, message } of failureItems) { | ||
this.addFailureAtNode(node, message, fixer); | ||
} | ||
} | ||
} | ||
buildFixer(infos) { | ||
let { ordered } = this.options; | ||
let startNode = infos[0].node; | ||
let endNode = infos[infos.length - 1].node; | ||
let infoGroups = groupModuleImportInfos(infos, ordered); | ||
let text = infoGroups | ||
.map(group => group.map(info => info.node.getText()).join('\n')) | ||
.join('\n\n'); | ||
return new tslint_1.Replacement(startNode.getStart(), endNode.getEnd(), text); | ||
} | ||
} | ||
function groupModuleImportInfos(infos, ordered) { | ||
// 这里利用了 Map 和 Set 枚举顺序和键加入顺序一致的特性. 如果不需要按顺序分 | ||
// 组, 则遵照分组出现顺序. | ||
let infoGroupMap = new Map(); | ||
for (let info of infos) { | ||
let infoGroup = infoGroupMap.get(info.groupIndex); | ||
if (infoGroup) { | ||
infoGroup.push(info); | ||
} | ||
else { | ||
infoGroup = [info]; | ||
infoGroupMap.set(info.groupIndex, infoGroup); | ||
} | ||
} | ||
if (ordered) { | ||
return Array.from(infoGroupMap.entries()) | ||
.sort(([indexX], [indexY]) => indexX - indexY) | ||
.map(([, infoGroup]) => infoGroup); | ||
} | ||
else { | ||
return Array.from(infoGroupMap.values()); | ||
} | ||
} | ||
// import * as Path from 'path'; | ||
// import resolve = require('resolve'); | ||
// import { | ||
// AbstractWalker, | ||
// IOptions, | ||
// IRuleMetadata, | ||
// Replacement, | ||
// RuleFailure, | ||
// Rules, | ||
// } from 'tslint'; | ||
// import { | ||
// ImportKind, | ||
// isImportDeclaration, | ||
// isImportEqualsDeclaration, | ||
// isTextualLiteral, | ||
// } from 'tsutils'; | ||
// import * as TypeScript from 'typescript'; | ||
// import {Dict} from '../@lang'; | ||
// import {removeQuotes} from '../utils/path'; | ||
// const ERROR_MESSAGE_UNEXPECTED_EMPTY_LINE = | ||
// 'Unexpected empty line within the same import group.'; | ||
// const ERROR_MESSAGE_EXPECTING_EMPTY_LINE = | ||
// 'Expecting an empty line between different import groups.'; | ||
// const ERROR_MESSAGE_WRONG_MODULE_GROUP_ORDER = | ||
// 'Import groups must be sorted according to given order.'; | ||
// const ERROR_MESSAGE_NOT_GROUPED = 'Imports must be grouped.'; | ||
// const resolveWithCache = ((): (( | ||
// id: string, | ||
// basedir: string, | ||
// ) => string | undefined) => { | ||
// let cache = new Map<string, string | undefined>(); | ||
// return (id: string, basedir: string): string | undefined => { | ||
// let key = `${id}\n${basedir}`; | ||
// if (cache.has(key)) { | ||
// return cache.get(key); | ||
// } | ||
// let value: string | undefined; | ||
// try { | ||
// value = resolve.sync(id, {basedir}); | ||
// } catch (error) {} | ||
// cache.set(key, value); | ||
// return value; | ||
// }; | ||
// })(); | ||
// const BUILT_IN_MODULE_GROUP_TESTER_DICT: Dict<ModuleGroupTester> = { | ||
// '$node-core'(path) { | ||
// try { | ||
// return require.resolve(path) === path; | ||
// } catch (error) { | ||
// return false; | ||
// } | ||
// }, | ||
// '$node-modules'(modulePath, sourceFilePath) { | ||
// let basedir = Path.dirname(sourceFilePath); | ||
// let resolvedPath = resolveWithCache(modulePath, basedir); | ||
// if (!resolvedPath) { | ||
// return false; | ||
// } | ||
// let relativePath = Path.relative(basedir, resolvedPath); | ||
// return /[\\/]node_modules[\\/]/.test(relativePath); | ||
// }, | ||
// }; | ||
// interface ModuleGroupConfigItem { | ||
// name: string; | ||
// test: string; | ||
// } | ||
// interface RawOptions { | ||
// groups: ModuleGroupConfigItem[]; | ||
// ordered?: boolean; | ||
// } | ||
// interface ParsedOptions { | ||
// groups: ModuleGroup[]; | ||
// ordered: boolean; | ||
// } | ||
// type ModuleGroupTester = ( | ||
// modulePath: string, | ||
// sourceFilePath: string, | ||
// ) => boolean; | ||
// class ModuleGroup { | ||
// readonly name: string; | ||
// private tester: ModuleGroupTester; | ||
// constructor({name, test: testConfig}: ModuleGroupConfigItem) { | ||
// this.name = name; | ||
// this.tester = this.buildTester(testConfig); | ||
// } | ||
// test(modulePath: string, sourceFilePath: string): boolean { | ||
// return this.tester(modulePath, sourceFilePath); | ||
// } | ||
// private buildTester(config: string): ModuleGroupTester { | ||
// if (config.startsWith('$')) { | ||
// return ( | ||
// BUILT_IN_MODULE_GROUP_TESTER_DICT[config] || ((): boolean => false) | ||
// ); | ||
// } else { | ||
// let regex = new RegExp(config); | ||
// return (path): boolean => regex.test(path); | ||
// } | ||
// } | ||
// } | ||
// export class Rule extends Rules.AbstractRule { | ||
// private parsedOptions: ParsedOptions; | ||
// constructor(options: IOptions) { | ||
// super(options); | ||
// let {groups: groupConfigItems, ordered} = options | ||
// .ruleArguments[0] as RawOptions; | ||
// this.parsedOptions = { | ||
// groups: groupConfigItems.map(item => new ModuleGroup(item)), | ||
// ordered: !!ordered, | ||
// }; | ||
// } | ||
// apply(sourceFile: TypeScript.SourceFile): RuleFailure[] { | ||
// return this.applyWithWalker( | ||
// new ImportGroupWalker( | ||
// sourceFile, | ||
// Rule.metadata.ruleName, | ||
// this.parsedOptions, | ||
// ), | ||
// ); | ||
// } | ||
// static metadata: IRuleMetadata = { | ||
// ruleName: 'import-groups', | ||
// description: 'Validate that module imports are grouped as expected.', | ||
// optionsDescription: '', | ||
// options: { | ||
// properties: { | ||
// groups: { | ||
// items: { | ||
// properties: { | ||
// name: { | ||
// type: 'string', | ||
// }, | ||
// test: { | ||
// type: 'string', | ||
// }, | ||
// }, | ||
// type: 'object', | ||
// }, | ||
// type: 'array', | ||
// }, | ||
// ordered: { | ||
// type: 'boolean', | ||
// }, | ||
// }, | ||
// type: 'object', | ||
// }, | ||
// optionExamples: [ | ||
// [ | ||
// true, | ||
// { | ||
// groups: [ | ||
// {name: 'node-core', test: '$node-core'}, | ||
// {name: 'node-modules', test: '$node-modules'}, | ||
// ], | ||
// ordered: true, | ||
// }, | ||
// ], | ||
// ], | ||
// type: 'maintainability', | ||
// hasFix: true, | ||
// typescriptOnly: false, | ||
// }; | ||
// } | ||
// interface ModuleImportInfo { | ||
// node: TypeScript.Node; | ||
// groupIndex: number; | ||
// /** 节点开始行. */ | ||
// startLine: number; | ||
// /** 节点结束行. */ | ||
// endLine: number; | ||
// } | ||
// class ImportGroupWalker extends AbstractWalker<ParsedOptions> { | ||
// private moduleImportInfos: ModuleImportInfo[] = []; | ||
// private pendingStatements: TypeScript.Statement[] = []; | ||
// walk(sourceFile: TypeScript.SourceFile): void { | ||
// let pendingCache: TypeScript.Statement[] = []; | ||
// let checkWithAppendModuleImport = ( | ||
// expression: TypeScript.Expression, | ||
// ): void => { | ||
// this.pendingStatements.push(...pendingCache); | ||
// pendingCache = []; | ||
// if (isTextualLiteral(expression)) { | ||
// this.appendModuleImport(expression, sourceFile); | ||
// } | ||
// }; | ||
// for (let statement of sourceFile.statements) { | ||
// if (isImportDeclaration(statement)) { | ||
// if (ImportKind.ImportDeclaration) { | ||
// checkWithAppendModuleImport(statement.moduleSpecifier); | ||
// } | ||
// } else if (isImportEqualsDeclaration(statement)) { | ||
// if ( | ||
// ImportKind.ImportEquals && | ||
// statement.moduleReference.kind === | ||
// TypeScript.SyntaxKind.ExternalModuleReference && | ||
// statement.moduleReference.expression !== undefined | ||
// ) { | ||
// checkWithAppendModuleImport(statement.moduleReference.expression); | ||
// } | ||
// } else { | ||
// pendingCache.push(statement); | ||
// } | ||
// } | ||
// this.validate(); | ||
// } | ||
// private appendModuleImport( | ||
// expression: TypeScript.LiteralExpression, | ||
// sourceFile: TypeScript.SourceFile, | ||
// ): void { | ||
// let node: TypeScript.Node = expression; | ||
// while (node.parent.kind !== TypeScript.SyntaxKind.SourceFile) { | ||
// node = node.parent; | ||
// } | ||
// let modulePath = removeQuotes(expression.getText()); | ||
// let sourceFilePath = sourceFile.fileName; | ||
// let groups = this.options.groups; | ||
// let index = groups.findIndex(group => | ||
// group.test(modulePath, sourceFilePath), | ||
// ); | ||
// this.moduleImportInfos.push({ | ||
// node, | ||
// // 如果没有找到匹配的分组, 则归到 "其他" 一组, groupIndex 为 groups.length. | ||
// groupIndex: index < 0 ? groups.length : index, | ||
// startLine: sourceFile.getLineAndCharacterOfPosition(node.getStart()).line, | ||
// endLine: sourceFile.getLineAndCharacterOfPosition(node.getEnd()).line, | ||
// }); | ||
// } | ||
// private validate(): void { | ||
// let infos = this.moduleImportInfos; | ||
// let pendingStatements = this.pendingStatements; | ||
// if (!infos.length) { | ||
// return; | ||
// } | ||
// let {ordered} = this.options; | ||
// interface FailureItem { | ||
// node: TypeScript.Node; | ||
// message: string; | ||
// } | ||
// let failureItems: FailureItem[] = []; | ||
// let [lastInfo, ...restInfos] = infos; | ||
// let fixerEnabled = !pendingStatements.length; | ||
// let appearedGroupIndexSet = new Set([lastInfo.groupIndex]); | ||
// for (let expression of pendingStatements) { | ||
// failureItems.push({ | ||
// node: expression, | ||
// message: 'Unexpected code between import statements.', | ||
// }); | ||
// } | ||
// for (let info of restInfos) { | ||
// let checkOrdering = ordered; | ||
// if (info.groupIndex === lastInfo.groupIndex) { | ||
// // 只在分组第一项检查分组顺序. | ||
// checkOrdering = false; | ||
// // 如果当前分组和上一份组 groupIndex 相同, 则校验是否多了空行. | ||
// if (info.startLine - lastInfo.endLine > 1) { | ||
// failureItems.push({ | ||
// node: info.node, | ||
// message: ERROR_MESSAGE_UNEXPECTED_EMPTY_LINE, | ||
// }); | ||
// } | ||
// } else { | ||
// // 检验该组是否已经出现过. | ||
// if (appearedGroupIndexSet.has(info.groupIndex)) { | ||
// checkOrdering = false; | ||
// failureItems.push({ | ||
// node: info.node, | ||
// message: ERROR_MESSAGE_NOT_GROUPED, | ||
// }); | ||
// } | ||
// // 如果未出现过则校验是否少了空行. | ||
// else if (info.startLine - lastInfo.endLine < 2) { | ||
// failureItems.push({ | ||
// node: info.node, | ||
// message: ERROR_MESSAGE_EXPECTING_EMPTY_LINE, | ||
// }); | ||
// } | ||
// } | ||
// if (checkOrdering) { | ||
// // 在要求分组顺序的情况下, 如果当前分组的 groupIndex 小于上一个分组的, | ||
// // 则说明顺序错误. | ||
// if (info.groupIndex < lastInfo.groupIndex) { | ||
// failureItems.push({ | ||
// node: info.node, | ||
// message: ERROR_MESSAGE_WRONG_MODULE_GROUP_ORDER, | ||
// }); | ||
// } | ||
// } | ||
// appearedGroupIndexSet.add(info.groupIndex); | ||
// lastInfo = info; | ||
// } | ||
// if (failureItems.length) { | ||
// let fixer = fixerEnabled ? this.buildFixer(infos) : undefined; | ||
// for (let {node, message} of failureItems) { | ||
// this.addFailureAtNode(node, message, fixer); | ||
// } | ||
// } | ||
// } | ||
// private buildFixer(infos: ModuleImportInfo[]): Replacement | undefined { | ||
// let {ordered} = this.options; | ||
// let startNode = infos[0].node; | ||
// let endNode = infos[infos.length - 1].node; | ||
// let infoGroups = groupModuleImportInfos(infos, ordered); | ||
// let text = infoGroups | ||
// .map(group => group.map(info => info.node.getText()).join('\n')) | ||
// .join('\n\n'); | ||
// return new Replacement(startNode.getStart(), endNode.getEnd(), text); | ||
// } | ||
// } | ||
// function groupModuleImportInfos( | ||
// infos: ModuleImportInfo[], | ||
// ordered: boolean, | ||
// ): ModuleImportInfo[][] { | ||
// // 这里利用了 Map 和 Set 枚举顺序和键加入顺序一致的特性. 如果不需要按顺序分 | ||
// // 组, 则遵照分组出现顺序. | ||
// let infoGroupMap = new Map<number, ModuleImportInfo[]>(); | ||
// for (let info of infos) { | ||
// let infoGroup = infoGroupMap.get(info.groupIndex); | ||
// if (infoGroup) { | ||
// infoGroup.push(info); | ||
// } else { | ||
// infoGroup = [info]; | ||
// infoGroupMap.set(info.groupIndex, infoGroup); | ||
// } | ||
// } | ||
// if (ordered) { | ||
// return Array.from(infoGroupMap.entries()) | ||
// .sort(([indexX], [indexY]) => indexX - indexY) | ||
// .map(([, infoGroup]) => infoGroup); | ||
// } else { | ||
// return Array.from(infoGroupMap.values()); | ||
// } | ||
// } | ||
//# sourceMappingURL=importGroupsRule.js.map |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const FS = require("fs"); | ||
const Path = require("path"); | ||
const _ = require("lodash"); | ||
const tslint_1 = require("tslint"); | ||
const tsutils_1 = require("tsutils"); | ||
const failure_manager_1 = require("../utils/failure-manager"); | ||
const path_1 = require("../utils/path"); | ||
const PARENT_DIRNAME = /^(?:\.{2})/; | ||
const RELATIVE_PATH = /^(?:\.{1,2}[\\/])+/; | ||
const ERROR_MESSAGE_IMPORT_OUT_OF_BASEURL = 'This import path must use baseUrl.'; | ||
const ERROR_MESSAGE_IMPORT_IN_BASEURL = 'This import path must be a relative path.'; | ||
class Rule extends tslint_1.Rules.AbstractRule { | ||
constructor(options) { | ||
super(options); | ||
this.parsedOptions = options.ruleArguments[0]; | ||
if (!this.parsedOptions || !this.parsedOptions.baseUrl) { | ||
throw new Error('Option baseUrl is required'); | ||
} | ||
} | ||
apply(sourceFile) { | ||
return this.applyWithWalker(new ImportPathBaseUrlWalker(sourceFile, Rule.metadata.ruleName, this.parsedOptions)); | ||
} | ||
} | ||
Rule.metadata = { | ||
ruleName: 'import-path-convention', | ||
description: 'Check import module from baseUrl', | ||
optionsDescription: '', | ||
options: { | ||
properties: { | ||
baseUrl: { | ||
type: 'string', | ||
}, | ||
baseUrlDirSearchName: { | ||
type: 'string', | ||
default: 'tsconfig', | ||
}, | ||
}, | ||
}, | ||
type: 'maintainability', | ||
hasFix: true, | ||
typescriptOnly: false, | ||
}; | ||
exports.Rule = Rule; | ||
class ImportPathBaseUrlWalker extends tslint_1.AbstractWalker { | ||
constructor(sourceFile, ruleName, options) { | ||
super(sourceFile, ruleName, options); | ||
this.importExpressions = []; | ||
this.failureManager = new failure_manager_1.FailureManager(this); | ||
this.sourceDirname = Path.dirname(sourceFile.fileName); | ||
} | ||
walk(sourceFile) { | ||
for (const expression of tsutils_1.findImports(sourceFile, 3 /* AllStaticImports */)) { | ||
this.importExpressions.push(expression); | ||
} | ||
this.validate(); | ||
} | ||
validate() { | ||
let importExpressions = this.importExpressions; | ||
for (let expression of importExpressions) { | ||
let text = path_1.removeQuotes(expression.getText()); | ||
if (!this.isModuleInbaseUrl(this.sourceDirname) && | ||
this.isModuleInbaseUrl(text) && | ||
RELATIVE_PATH.test(text)) { | ||
// sourceFile 不在 baseUrl 目录下, 且模块用的是相对路径 | ||
this.sourceFileOutOfBaseUrl(expression, text); | ||
} | ||
else if (this.isModuleInbaseUrl(this.sourceDirname) && | ||
this.isModuleInbaseUrl(text) && | ||
!RELATIVE_PATH.test(text)) { | ||
// sourceFile 在 baseUrl 目录下, 且模块没有用相对路径 | ||
this.sourceFileInBaseUrl(expression, text); | ||
} | ||
this.failureManager.throw(); | ||
} | ||
} | ||
sourceFileInBaseUrl(expression, text) { | ||
let importPath = Path.relative(this.sourceDirname, Path.join(this.getBaseUrl(), text)); | ||
if (!PARENT_DIRNAME.test(importPath)) { | ||
importPath = `./${importPath}`; | ||
} | ||
this.failureManager.append({ | ||
message: ERROR_MESSAGE_IMPORT_OUT_OF_BASEURL, | ||
node: expression.parent, | ||
fixer: new tslint_1.Replacement(expression.getStart(), expression.getWidth(), `'${importPath}'`), | ||
}); | ||
} | ||
sourceFileOutOfBaseUrl(expression, text) { | ||
let importPath = Path.relative(this.getBaseUrl(), Path.join(this.sourceDirname, text)); | ||
this.failureManager.append({ | ||
message: ERROR_MESSAGE_IMPORT_IN_BASEURL, | ||
node: expression.parent, | ||
fixer: new tslint_1.Replacement(expression.getStart(), expression.getWidth(), `'${importPath}'`), | ||
}); | ||
} | ||
getBaseUrl() { | ||
let rootPath = findProjectRootPath(this.sourceDirname, this.options.baseUrlDirSearchName); | ||
if (!rootPath) { | ||
throw new Error('can not find tslint.json'); | ||
} | ||
return Path.join(rootPath, this.options.baseUrl); | ||
} | ||
isModuleInbaseUrl(filePath) { | ||
if (RELATIVE_PATH.test(filePath) || Path.isAbsolute(filePath)) { | ||
return this.isFileInDirectory(filePath, this.getBaseUrl()); | ||
} | ||
filePath = Path.join(this.getBaseUrl(), filePath); | ||
let basename = path_1.removeModuleFileExtension(Path.basename(filePath)); | ||
let dirname = Path.dirname(filePath); | ||
let files = FS.readdirSync(dirname).map(file => path_1.removeModuleFileExtension(file)); | ||
if (_.includes(files, basename)) { | ||
return true; | ||
} | ||
return false; | ||
} | ||
isFileInDirectory(filePath, directoryPath) { | ||
let modulePath = filePath; | ||
if (!Path.isAbsolute(filePath)) { | ||
modulePath = Path.join(this.sourceDirname, filePath); | ||
} | ||
return !PARENT_DIRNAME.test(Path.relative(directoryPath, modulePath)); | ||
} | ||
} | ||
exports.ImportPathBaseUrlWalker = ImportPathBaseUrlWalker; | ||
let findProjectRootPath = (() => { | ||
let rootPathCache; | ||
return function inner(currentPath, baseUrlDirSearchName = 'tsconfig.json') { | ||
if (rootPathCache) { | ||
return rootPathCache; | ||
} | ||
try { | ||
let files = FS.readdirSync(currentPath); | ||
if (_.includes(files, baseUrlDirSearchName)) { | ||
rootPathCache = currentPath; | ||
return currentPath; | ||
} | ||
else { | ||
return inner(Path.join(currentPath, '..'), baseUrlDirSearchName); | ||
} | ||
} | ||
catch (e) { | ||
throw new Error(`can not find such 'baseUrlDirSearchName' that ${baseUrlDirSearchName}`); | ||
} | ||
}; | ||
})(); | ||
// import * as FS from 'fs'; | ||
// import * as Path from 'path'; | ||
// import * as _ from 'lodash'; | ||
// import { | ||
// AbstractWalker, | ||
// IOptions, | ||
// IRuleMetadata, | ||
// Replacement, | ||
// RuleFailure, | ||
// Rules, | ||
// } from 'tslint'; | ||
// import {ImportKind, findImports} from 'tsutils'; | ||
// import * as TypeScript from 'typescript'; | ||
// import {FailureManager} from '../utils/failure-manager'; | ||
// import {removeModuleFileExtension, removeQuotes} from '../utils/path'; | ||
// const PARENT_DIRNAME = /^(?:\.{2})/; | ||
// const RELATIVE_PATH = /^(?:\.{1,2}[\\/])+/; | ||
// const ERROR_MESSAGE_IMPORT_OUT_OF_BASEURL = | ||
// 'This import path must use baseUrl.'; | ||
// const ERROR_MESSAGE_IMPORT_IN_BASEURL = | ||
// 'This import path must be a relative path.'; | ||
// interface RuleOptions { | ||
// baseUrl: string; | ||
// baseUrlDirSearchName: string | undefined; | ||
// } | ||
// export class Rule extends Rules.AbstractRule { | ||
// private parsedOptions: RuleOptions; | ||
// constructor(options: IOptions) { | ||
// super(options); | ||
// this.parsedOptions = options.ruleArguments[0]; | ||
// if (!this.parsedOptions || !this.parsedOptions.baseUrl) { | ||
// throw new Error('Option baseUrl is required'); | ||
// } | ||
// } | ||
// apply(sourceFile: TypeScript.SourceFile): RuleFailure[] { | ||
// return this.applyWithWalker( | ||
// new ImportPathBaseUrlWalker( | ||
// sourceFile, | ||
// Rule.metadata.ruleName, | ||
// this.parsedOptions, | ||
// ), | ||
// ); | ||
// } | ||
// static metadata: IRuleMetadata = { | ||
// ruleName: 'import-path-base-url', | ||
// description: 'Check import module from baseUrl', | ||
// optionsDescription: '', | ||
// options: { | ||
// properties: { | ||
// baseUrl: { | ||
// type: 'string', | ||
// }, | ||
// baseUrlDirSearchName: { | ||
// type: 'string', | ||
// default: 'tsconfig', | ||
// }, | ||
// }, | ||
// }, | ||
// type: 'maintainability', | ||
// hasFix: true, | ||
// typescriptOnly: false, | ||
// }; | ||
// } | ||
// export class ImportPathBaseUrlWalker extends AbstractWalker<RuleOptions> { | ||
// private sourceDirname: string; | ||
// private importExpressions: TypeScript.Expression[] = []; | ||
// private failureManager = new FailureManager(this); | ||
// constructor( | ||
// sourceFile: TypeScript.SourceFile, | ||
// ruleName: string, | ||
// options: RuleOptions, | ||
// ) { | ||
// super(sourceFile, ruleName, options); | ||
// this.sourceDirname = Path.dirname(sourceFile.fileName); | ||
// } | ||
// walk(sourceFile: TypeScript.SourceFile): void { | ||
// for (const expression of findImports( | ||
// sourceFile, | ||
// ImportKind.AllStaticImports, | ||
// )) { | ||
// this.importExpressions.push(expression); | ||
// } | ||
// this.validate(); | ||
// } | ||
// private validate(): void { | ||
// let importExpressions = this.importExpressions; | ||
// for (let expression of importExpressions) { | ||
// let text = removeQuotes(expression.getText()); | ||
// if ( | ||
// !this.isModuleInbaseUrl(this.sourceDirname) && | ||
// this.isModuleInbaseUrl(text) && | ||
// RELATIVE_PATH.test(text) | ||
// ) { | ||
// // sourceFile 不在 baseUrl 目录下, 且模块用的是相对路径 | ||
// this.sourceFileOutOfBaseUrl(expression, text); | ||
// } else if ( | ||
// this.isModuleInbaseUrl(this.sourceDirname) && | ||
// this.isModuleInbaseUrl(text) && | ||
// !RELATIVE_PATH.test(text) | ||
// ) { | ||
// // sourceFile 在 baseUrl 目录下, 且模块没有用相对路径 | ||
// this.sourceFileInBaseUrl(expression, text); | ||
// } | ||
// this.failureManager.throw(); | ||
// } | ||
// } | ||
// private sourceFileInBaseUrl( | ||
// expression: TypeScript.Expression, | ||
// text: string, | ||
// ): void { | ||
// let importPath = Path.relative( | ||
// this.sourceDirname, | ||
// Path.join(this.getBaseUrl(), text), | ||
// ); | ||
// if (!PARENT_DIRNAME.test(importPath)) { | ||
// importPath = `./${importPath}`; | ||
// } | ||
// this.failureManager.append({ | ||
// message: ERROR_MESSAGE_IMPORT_IN_BASEURL, | ||
// node: expression.parent, | ||
// fixer: new Replacement( | ||
// expression.getStart(), | ||
// expression.getWidth(), | ||
// `'${importPath}'`, | ||
// ), | ||
// }); | ||
// } | ||
// private sourceFileOutOfBaseUrl( | ||
// expression: TypeScript.Expression, | ||
// text: string, | ||
// ): void { | ||
// let importPath = Path.relative( | ||
// this.getBaseUrl(), | ||
// Path.join(this.sourceDirname, text), | ||
// ); | ||
// this.failureManager.append({ | ||
// message: ERROR_MESSAGE_IMPORT_OUT_OF_BASEURL, | ||
// node: expression.parent, | ||
// fixer: new Replacement( | ||
// expression.getStart(), | ||
// expression.getWidth(), | ||
// `'${importPath}'`, | ||
// ), | ||
// }); | ||
// } | ||
// private getBaseUrl(): string { | ||
// let rootPath = findProjectRootPath( | ||
// this.sourceDirname, | ||
// this.options.baseUrlDirSearchName, | ||
// ); | ||
// if (!rootPath) { | ||
// throw new Error('can not find tslint.json'); | ||
// } | ||
// return Path.join(rootPath, this.options.baseUrl); | ||
// } | ||
// private isModuleInbaseUrl(filePath: string): boolean { | ||
// if (RELATIVE_PATH.test(filePath) || Path.isAbsolute(filePath)) { | ||
// return this.isFileInDirectory(filePath, this.getBaseUrl()); | ||
// } | ||
// filePath = Path.join(this.getBaseUrl(), filePath); | ||
// let basename = removeModuleFileExtension(Path.basename(filePath)); | ||
// let dirname = Path.dirname(filePath); | ||
// let files = FS.readdirSync(dirname).map(file => | ||
// removeModuleFileExtension(file), | ||
// ); | ||
// if (_.includes(files, basename)) { | ||
// return true; | ||
// } | ||
// return false; | ||
// } | ||
// private isFileInDirectory(filePath: string, directoryPath: string): boolean { | ||
// let modulePath = filePath; | ||
// if (!Path.isAbsolute(filePath)) { | ||
// modulePath = Path.join(this.sourceDirname, filePath); | ||
// } | ||
// return !PARENT_DIRNAME.test(Path.relative(directoryPath, modulePath)); | ||
// } | ||
// } | ||
// let findProjectRootPath = ((): (( | ||
// currentPath: string, | ||
// baseUrlDirSearchName: string | undefined, | ||
// ) => string | undefined) => { | ||
// let rootPathCache: string | undefined; | ||
// return function inner( | ||
// currentPath: string, | ||
// baseUrlDirSearchName: string | undefined = 'tsconfig.json', | ||
// ): string | undefined { | ||
// if (rootPathCache) { | ||
// return rootPathCache; | ||
// } | ||
// try { | ||
// let files = FS.readdirSync(currentPath); | ||
// if (_.includes(files, baseUrlDirSearchName)) { | ||
// rootPathCache = currentPath; | ||
// return currentPath; | ||
// } else { | ||
// return inner(Path.join(currentPath, '..'), baseUrlDirSearchName); | ||
// } | ||
// } catch (e) { | ||
// throw new Error( | ||
// `can not find such 'baseUrlDirSearchName' that ${baseUrlDirSearchName}`, | ||
// ); | ||
// } | ||
// }; | ||
// })(); | ||
//# sourceMappingURL=importPathBaseUrlRule.js.map |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const Path = require("path"); | ||
const tslint_1 = require("tslint"); | ||
const tsutils_1 = require("tsutils"); | ||
const path_1 = require("../utils/path"); | ||
const DIRECTORY_MODULE_PATH = /^\.{1,2}(?:[\\/]\.{2})*[\\/]?$/; | ||
const ERROR_MESSAGE_BANNED_PARENT_IMPORT = 'Importing from parent directory is not allowed.'; | ||
class Rule extends tslint_1.Rules.AbstractRule { | ||
apply(sourceFile) { | ||
return this.applyWithWalker(new NoParentImportRule(sourceFile, Rule.metadata.ruleName, undefined)); | ||
} | ||
} | ||
Rule.metadata = { | ||
ruleName: 'no-parent-import', | ||
description: '', | ||
optionsDescription: '', | ||
options: undefined, | ||
type: 'maintainability', | ||
hasFix: true, | ||
typescriptOnly: false, | ||
}; | ||
exports.Rule = Rule; | ||
class NoParentImportRule extends tslint_1.AbstractWalker { | ||
constructor() { | ||
super(...arguments); | ||
/** 装 import 语句的容器 */ | ||
this.importExpressions = []; | ||
} | ||
walk(sourceFile) { | ||
for (let expression of tsutils_1.findImports(sourceFile, 3 /* AllStaticImports */)) { | ||
this.importExpressions.push(expression); | ||
} | ||
this.validate(); | ||
} | ||
validate() { | ||
let importExpressions = this.importExpressions; | ||
let sourceDirName = Path.dirname(this.sourceFile.fileName); | ||
for (let expression of importExpressions) { | ||
let modulePath = path_1.removeQuotes(expression.getText()); | ||
modulePath = Path.isAbsolute(modulePath) | ||
? Path.relative(sourceDirName, modulePath) | ||
: (modulePath = Path.relative(sourceDirName, Path.join(sourceDirName, modulePath))); | ||
if (!DIRECTORY_MODULE_PATH.test(modulePath) && modulePath !== '') { | ||
continue; | ||
} | ||
this.addFailureAtNode(expression.parent, ERROR_MESSAGE_BANNED_PARENT_IMPORT); | ||
} | ||
} | ||
} | ||
// import * as Path from 'path'; | ||
// import {AbstractWalker, IRuleMetadata, RuleFailure, Rules} from 'tslint'; | ||
// import {ImportKind, findImports} from 'tsutils'; | ||
// import * as Typescript from 'typescript'; | ||
// import {removeQuotes} from '../utils/path'; | ||
// const DIRECTORY_MODULE_PATH = /^\.{1,2}(?:[\\/]\.{2})*[\\/]?$/; | ||
// const ERROR_MESSAGE_BANNED_PARENT_IMPORT = | ||
// 'Importing from parent directory is not allowed.'; | ||
// export class Rule extends Rules.AbstractRule { | ||
// apply(sourceFile: Typescript.SourceFile): RuleFailure[] { | ||
// return this.applyWithWalker( | ||
// new NoParentImportRule(sourceFile, Rule.metadata.ruleName, undefined), | ||
// ); | ||
// } | ||
// static metadata: IRuleMetadata = { | ||
// ruleName: 'no-parent-import', | ||
// description: '', | ||
// optionsDescription: '', | ||
// options: undefined, | ||
// type: 'maintainability', | ||
// hasFix: true, | ||
// typescriptOnly: false, | ||
// }; | ||
// } | ||
// class NoParentImportRule extends AbstractWalker<undefined> { | ||
// /** 装 import 语句的容器 */ | ||
// private importExpressions: Typescript.Expression[] = []; | ||
// walk(sourceFile: Typescript.SourceFile): void { | ||
// for (let expression of findImports( | ||
// sourceFile, | ||
// ImportKind.AllStaticImports, | ||
// )) { | ||
// this.importExpressions.push(expression); | ||
// } | ||
// this.validate(); | ||
// } | ||
// private validate(): void { | ||
// let importExpressions = this.importExpressions; | ||
// let sourceDirName = Path.dirname(this.sourceFile.fileName); | ||
// for (let expression of importExpressions) { | ||
// let modulePath: string = removeQuotes(expression.getText()); | ||
// modulePath = Path.isAbsolute(modulePath) | ||
// ? Path.relative(sourceDirName, modulePath) | ||
// : (modulePath = Path.relative( | ||
// sourceDirName, | ||
// Path.join(sourceDirName, modulePath), | ||
// )); | ||
// if (!DIRECTORY_MODULE_PATH.test(modulePath) && modulePath !== '') { | ||
// continue; | ||
// } | ||
// this.addFailureAtNode( | ||
// expression.parent, | ||
// ERROR_MESSAGE_BANNED_PARENT_IMPORT, | ||
// ); | ||
// } | ||
// } | ||
// } | ||
//# sourceMappingURL=noParentImportRule.js.map |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const FS = require("fs"); | ||
const Path = require("path"); | ||
const _ = require("lodash"); | ||
const tslint_1 = require("tslint"); | ||
const tsutils_1 = require("tsutils"); | ||
const failure_manager_1 = require("../utils/failure-manager"); | ||
const path_1 = require("../utils/path"); | ||
const ERROR_MESSAGE_BANNED_IMPORT = "This module can not be imported, because it contains internal module with prefix '@' under a parallel directory."; | ||
const ERROR_MESSAGE_BANNED_EXPORT = "This module can not be exported, because it contains internal module with prefix '@' under a parallel directory."; | ||
const ERROR_MESSAGE_MISSING_EXPORTS = 'Missing modules expected to be exported.'; | ||
const INDEX_FILE_REGEX = /(?:^|[\\/])index\.((?:js|ts)x?)$/; | ||
const BannedPattern = { | ||
import: /^(?!(?:\.{1,2}[\\/])+@(?!.*[\\/]@)).*[\\/]@/, | ||
export: /[\\/]@/, | ||
}; | ||
/** 根据不同的 tag 返回不同的 fixer */ | ||
const fixerBuilder = { | ||
removeNotExportFixer: node => new tslint_1.Replacement(node.getStart(), node.getWidth(), ''), | ||
autoExportModuleFixer: (sourceFile, exportNodesPath) => new tslint_1.Replacement(sourceFile.getStart(), sourceFile.getFullWidth(), `${[ | ||
sourceFile.getText().trimRight(), | ||
...exportNodesPath.map(value => `export * from '${path_1.removeModuleFileExtension(value)}';`), | ||
] | ||
.filter(text => !!text) | ||
.join('\n')}\n`), | ||
}; | ||
class Rule extends tslint_1.Rules.AbstractRule { | ||
apply(sourceFile) { | ||
return this.applyWithWalker(new ScopedModuleWalker(sourceFile, Rule.metadata.ruleName, undefined)); | ||
} | ||
} | ||
Rule.metadata = { | ||
ruleName: 'scoped-modules', | ||
description: '', | ||
optionsDescription: '', | ||
options: undefined, | ||
type: 'maintainability', | ||
hasFix: true, | ||
typescriptOnly: false, | ||
}; | ||
exports.Rule = Rule; | ||
class ScopedModuleWalker extends tslint_1.AbstractWalker { | ||
constructor() { | ||
super(...arguments); | ||
this.nodeInfos = []; | ||
this.failureManager = new failure_manager_1.FailureManager(this); | ||
} | ||
walk(sourceFile) { | ||
for (let statement of sourceFile.statements) { | ||
if (tsutils_1.isImportDeclaration(statement)) { | ||
this.nodeInfos.push({ | ||
node: statement.moduleSpecifier, | ||
type: 'import', | ||
}); | ||
} | ||
if (tsutils_1.isExportDeclaration(statement)) { | ||
this.nodeInfos.push({ | ||
node: statement.moduleSpecifier, | ||
type: 'export', | ||
}); | ||
} | ||
} | ||
this.validate(); | ||
} | ||
validateExportsAndImport(message, text, node, tag) { | ||
if (BannedPattern[tag].test(text)) { | ||
this.failureManager.append({ | ||
message, | ||
node, | ||
fixer: fixerBuilder.removeNotExportFixer(node), | ||
}); | ||
} | ||
} | ||
validateExports(text, node) { | ||
this.validateExportsAndImport(ERROR_MESSAGE_BANNED_EXPORT, text, node, 'export'); | ||
} | ||
validateImport(text, node) { | ||
this.validateExportsAndImport(ERROR_MESSAGE_BANNED_IMPORT, text, node, 'import'); | ||
} | ||
validateIndexFile(exportIds) { | ||
let fileName = this.sourceFile.fileName; | ||
if (!INDEX_FILE_REGEX.test(fileName)) { | ||
return; | ||
} | ||
let dirName = Path.dirname(fileName); | ||
let entryNames = FS.readdirSync(dirName); | ||
let expectedExportIds = entryNames | ||
.map((entryName) => { | ||
let entryFullPath = Path.join(dirName, entryName); | ||
let stats = FS.statSync(entryFullPath); | ||
if (stats.isFile()) { | ||
if (INDEX_FILE_REGEX.test(entryName)) { | ||
return undefined; | ||
} | ||
let entryModuleId = `./${path_1.removeModuleFileExtension(entryName)}`; | ||
if (BannedPattern.export.test(entryModuleId)) { | ||
return undefined; | ||
} | ||
return entryModuleId; | ||
} | ||
else if (stats.isDirectory()) { | ||
let entryNamesInFolder = FS.readdirSync(entryFullPath); | ||
let hasIndexFile = entryNamesInFolder.some(entryNameInFolder => INDEX_FILE_REGEX.test(entryNameInFolder)); | ||
if (!hasIndexFile) { | ||
return undefined; | ||
} | ||
return `./${entryName}`; | ||
} | ||
else { | ||
return undefined; | ||
} | ||
}) | ||
.filter((entryName) => !!entryName); | ||
let missingExportIds = _.difference(expectedExportIds, exportIds); | ||
if (missingExportIds.length) { | ||
this.failureManager.append({ | ||
node: undefined, | ||
message: ERROR_MESSAGE_MISSING_EXPORTS, | ||
fixer: fixerBuilder.autoExportModuleFixer(this.sourceFile, missingExportIds), | ||
}); | ||
} | ||
} | ||
validate() { | ||
let infos = this.nodeInfos; | ||
for (let info of infos) { | ||
if (info.type === 'export') { | ||
this.validateExports(path_1.removeQuotes(info.node.getText()), info.node.parent); | ||
} | ||
else if (info.type === 'import') { | ||
this.validateImport(path_1.removeQuotes(info.node.getText()), info.node.parent); | ||
} | ||
} | ||
let exportIds = infos | ||
.filter(info => info.type === 'export') | ||
.map(info => path_1.removeQuotes(info.node.getText())); | ||
this.validateIndexFile(exportIds); | ||
this.failureManager.throw(); | ||
} | ||
} | ||
// import * as FS from 'fs'; | ||
// import * as Path from 'path'; | ||
// import _ = require('lodash'); | ||
// import { | ||
// AbstractWalker, | ||
// IRuleMetadata, | ||
// Replacement, | ||
// RuleFailure, | ||
// Rules, | ||
// } from 'tslint'; | ||
// import {isExportDeclaration, isImportDeclaration} from 'tsutils'; | ||
// import { | ||
// ExportDeclaration, | ||
// ImportDeclaration, | ||
// SourceFile, | ||
// isStringLiteral, | ||
// } from 'typescript'; | ||
// import {FailureManager} from '../utils/failure-manager'; | ||
// import {removeModuleFileExtension, removeQuotes} from '../utils/path'; | ||
// const ERROR_MESSAGE_BANNED_IMPORT = | ||
// "This module can not be imported, because it contains internal module with prefix '@' under a parallel directory."; | ||
// const ERROR_MESSAGE_BANNED_EXPORT = | ||
// "This module can not be exported, because it contains internal module with prefix '@' under a parallel directory."; | ||
// const ERROR_MESSAGE_MISSING_EXPORTS = | ||
// 'Missing modules expected to be exported.'; | ||
// const INDEX_FILE_REGEX = /(?:^|[\\/])index\.(?:js|jsx|ts|tsx|d\.ts)$/i; | ||
// const EXPORTABLE_EXTENSION_REGEX = /\.(?:js|jsx|ts|tsx|d\.ts)$/i; | ||
// const BANNED_IMPORT_REGEX = /^(?!(?:\.{1,2}[\\/])+@(?!.*[\\/]@)).*[\\/]@/; | ||
// const BANNED_EXPORT_REGEX = /[\\/]@/; | ||
// export class Rule extends Rules.AbstractRule { | ||
// apply(sourceFile: SourceFile): RuleFailure[] { | ||
// return this.applyWithWalker( | ||
// new ScopedModuleWalker(sourceFile, Rule.metadata.ruleName, undefined), | ||
// ); | ||
// } | ||
// static metadata: IRuleMetadata = { | ||
// ruleName: 'scoped-modules', | ||
// description: '', | ||
// optionsDescription: '', | ||
// options: undefined, | ||
// type: 'maintainability', | ||
// hasFix: true, | ||
// typescriptOnly: false, | ||
// }; | ||
// } | ||
// type ModuleStatement = ImportDeclaration | ExportDeclaration; | ||
// type ModuleStatementInfo = ImportStatementInfo | ExportStatementInfo; | ||
// type ModuleStatementType = ModuleStatementInfo['type']; | ||
// interface ImportStatementInfo { | ||
// type: 'import'; | ||
// statement: ModuleStatement; | ||
// specifier: string; | ||
// } | ||
// interface ExportStatementInfo { | ||
// type: 'export'; | ||
// statement: ModuleStatement; | ||
// specifier: string; | ||
// } | ||
// class ScopedModuleWalker extends AbstractWalker<undefined> { | ||
// private infos: ModuleStatementInfo[] = []; | ||
// private failureManager = new FailureManager(this); | ||
// walk(sourceFile: SourceFile): void { | ||
// for (let statement of sourceFile.statements) { | ||
// let type: ModuleStatementType; | ||
// if (isImportDeclaration(statement)) { | ||
// type = 'import'; | ||
// } else if (isExportDeclaration(statement)) { | ||
// type = 'export'; | ||
// } else { | ||
// continue; | ||
// } | ||
// let specifier = getModuleSpecifier(statement); | ||
// if (!specifier) { | ||
// continue; | ||
// } | ||
// this.infos.push({ | ||
// type, | ||
// statement, | ||
// specifier, | ||
// } as ModuleStatementInfo); | ||
// } | ||
// this.validate(); | ||
// } | ||
// private validateImportOrExport({ | ||
// type, | ||
// statement, | ||
// specifier, | ||
// }: ModuleStatementInfo): void { | ||
// let bannedPattern: RegExp; | ||
// let message: string; | ||
// if (type === 'import') { | ||
// bannedPattern = BANNED_IMPORT_REGEX; | ||
// message = ERROR_MESSAGE_BANNED_IMPORT; | ||
// } else { | ||
// bannedPattern = BANNED_EXPORT_REGEX; | ||
// message = ERROR_MESSAGE_BANNED_EXPORT; | ||
// } | ||
// if (bannedPattern.test(specifier)) { | ||
// this.failureManager.append({ | ||
// message, | ||
// node: statement, | ||
// fixer: buildBannedImportsAndExportsFixer(statement), | ||
// }); | ||
// } | ||
// } | ||
// private validateIndexFile(exportSpecifiers: string[]): void { | ||
// let fileName = this.sourceFile.fileName; | ||
// if (!INDEX_FILE_REGEX.test(fileName)) { | ||
// return; | ||
// } | ||
// let dirName = Path.dirname(fileName); | ||
// let fileNames = FS.readdirSync(dirName); | ||
// let expectedExportSpecifiers = fileNames | ||
// .map( | ||
// (fileName): string | undefined => { | ||
// let entryFullPath = Path.join(dirName, fileName); | ||
// let stats = FS.statSync(entryFullPath); | ||
// let specifier: string; | ||
// if (stats.isFile()) { | ||
// if ( | ||
// INDEX_FILE_REGEX.test(fileName) || | ||
// !EXPORTABLE_EXTENSION_REGEX.test(fileName) | ||
// ) { | ||
// return undefined; | ||
// } | ||
// specifier = `./${fileName.replace(EXPORTABLE_EXTENSION_REGEX, '')}`; | ||
// } else if (stats.isDirectory()) { | ||
// let entryNamesInFolder = FS.readdirSync(entryFullPath); | ||
// let hasIndexFile = entryNamesInFolder.some(entryNameInFolder => | ||
// INDEX_FILE_REGEX.test(entryNameInFolder), | ||
// ); | ||
// if (!hasIndexFile) { | ||
// return undefined; | ||
// } | ||
// specifier = `./${fileName}`; | ||
// } else { | ||
// return undefined; | ||
// } | ||
// if (BANNED_EXPORT_REGEX.test(specifier)) { | ||
// return undefined; | ||
// } | ||
// return specifier; | ||
// }, | ||
// ) | ||
// .filter((entryName): entryName is string => !!entryName); | ||
// let missingExportIds = _.difference( | ||
// expectedExportSpecifiers, | ||
// exportSpecifiers, | ||
// ); | ||
// if (missingExportIds.length) { | ||
// this.failureManager.append({ | ||
// node: undefined, | ||
// message: ERROR_MESSAGE_MISSING_EXPORTS, | ||
// fixer: buildAddMissingExportsFixer(this.sourceFile, missingExportIds), | ||
// }); | ||
// } | ||
// } | ||
// private validate(): void { | ||
// let infos = this.infos; | ||
// for (let info of infos) { | ||
// this.validateImportOrExport(info); | ||
// } | ||
// let exportSpecifiers = infos | ||
// .filter(info => info.type === 'export') | ||
// .map(info => info.specifier); | ||
// this.validateIndexFile(exportSpecifiers); | ||
// this.failureManager.throw(); | ||
// } | ||
// } | ||
// function getModuleSpecifier({ | ||
// moduleSpecifier, | ||
// }: ModuleStatement): string | undefined { | ||
// return moduleSpecifier && isStringLiteral(moduleSpecifier) | ||
// ? removeQuotes(moduleSpecifier.getText()) | ||
// : undefined; | ||
// } | ||
// function buildBannedImportsAndExportsFixer(node: ModuleStatement): Replacement { | ||
// return new Replacement(node.getFullStart(), node.getFullWidth(), ''); | ||
// } | ||
// function buildAddMissingExportsFixer( | ||
// sourceFile: SourceFile, | ||
// exportNodesPath: string[], | ||
// ): Replacement { | ||
// return new Replacement( | ||
// sourceFile.getStart(), | ||
// sourceFile.getEnd(), | ||
// `${[ | ||
// sourceFile.getText().trimRight(), | ||
// ...exportNodesPath.map( | ||
// value => `export * from '${removeModuleFileExtension(value)}';`, | ||
// ), | ||
// ] | ||
// .filter(text => !!text) | ||
// .join('\n')}\n`, | ||
// ); | ||
// } | ||
//# sourceMappingURL=scopedModulesRule.js.map |
{ | ||
"name": "@magicspace/tslint-rules", | ||
"version": "0.1.8", | ||
"version": "0.1.9", | ||
"description": "Custom TSLint rules for MagicSpace.", | ||
@@ -5,0 +5,0 @@ "repository": "https://github.com/makeflow/magicspace.git", |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
34855
1005
1
1