Skip to content

Commit ae7ac0a

Browse files
committedJan 19, 2019
working draft
1 parent 10d6c4b commit ae7ac0a

13 files changed

+3268
-0
lines changed
 

‎.editorconfig

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
root = true
2+
3+
[*]
4+
charset = utf-8
5+
indent_style = space
6+
indent_size = 2
7+
end_of_line = lf
8+
insert_final_newline = true
9+
trim_trailing_whitespace = true
10+
quote_type = single

‎.eslintrc.js

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
module.exports = {
2+
parserOptions: {
3+
sourceType: 'script',
4+
},
5+
extends: [
6+
'airbnb-base',
7+
'prettier/standard',
8+
'plugin:prettier/recommended',
9+
'plugin:node/recommended',
10+
],
11+
rules: {
12+
'object-shorthand': 0,
13+
},
14+
};

‎.gitignore

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Logs
2+
logs
3+
*.log
4+
npm-debug.log*
5+
yarn-debug.log*
6+
yarn-error.log*
7+
8+
# Runtime data
9+
pids
10+
*.pid
11+
*.seed
12+
*.pid.lock
13+
14+
# Directory for instrumented libs generated by jscoverage/JSCover
15+
lib-cov
16+
17+
# Coverage directory used by tools like istanbul
18+
coverage
19+
20+
# nyc test coverage
21+
.nyc_output
22+
23+
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
24+
.grunt
25+
26+
# Bower dependency directory (https://bower.io/)
27+
bower_components
28+
29+
# node-waf configuration
30+
.lock-wscript
31+
32+
# Compiled binary addons (https://nodejs.org/api/addons.html)
33+
build/Release
34+
35+
# Dependency directories
36+
node_modules/
37+
jspm_packages/
38+
39+
# TypeScript v1 declaration files
40+
typings/
41+
42+
# Optional npm cache directory
43+
.npm
44+
45+
# Optional eslint cache
46+
.eslintcache
47+
48+
# Optional REPL history
49+
.node_repl_history
50+
51+
# Output of 'npm pack'
52+
*.tgz
53+
54+
# Yarn Integrity file
55+
.yarn-integrity
56+
57+
# dotenv environment variables file
58+
.env
59+
.env.test
60+
61+
# parcel-bundler cache (https://parceljs.org/)
62+
.cache
63+
64+
# next.js build output
65+
.next
66+
67+
# nuxt.js build output
68+
.nuxt
69+
70+
# vuepress build output
71+
.vuepress/dist
72+
73+
# Serverless directories
74+
.serverless/
75+
76+
# FuseBox cache
77+
.fusebox/
78+
79+
# DynamoDB Local files
80+
.dynamodb/

‎.prettierrc.js

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports = {
2+
trailingComma: 'all',
3+
};

‎README.md

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# eslint-plugin-typescript-sort-keys
2+
3+
Sort interface keys
4+
5+
## Installation
6+
7+
You'll first need to install [ESLint](http://eslint.org):
8+
9+
```
10+
$ npm i eslint --save-dev
11+
```
12+
13+
Next, install `eslint-plugin-typescript-sort-keys`:
14+
15+
```
16+
$ npm install eslint-plugin-typescript-sort-keys --save-dev
17+
```
18+
19+
**Note:** If you installed ESLint globally (using the `-g` flag) then you must also install `eslint-plugin-typescript-sort-keys` globally.
20+
21+
## Usage
22+
23+
Add `typescript-sort-keys` to the plugins section of your `.eslintrc` configuration file. You can omit the `eslint-plugin-` prefix:
24+
25+
```json
26+
{
27+
"plugins": ["typescript-sort-keys"]
28+
}
29+
```
30+
31+
Then configure the rules you want to use under the rules section.
32+
33+
```json
34+
{
35+
"rules": {
36+
"typescript-sort-keys/rule-name": 2
37+
}
38+
}
39+
```
40+
41+
## Supported Rules
42+
43+
- Fill in provided rules here

‎lib/index.js

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* @fileoverview Sort interface keys
3+
* @author infctr
4+
*/
5+
6+
'use strict';
7+
8+
//------------------------------------------------------------------------------
9+
// Requirements
10+
//------------------------------------------------------------------------------
11+
12+
const requireIndex = require('requireindex');
13+
const path = require('path');
14+
15+
//------------------------------------------------------------------------------
16+
// Plugin Definition
17+
//------------------------------------------------------------------------------
18+
19+
// import all rules in lib/rules
20+
module.exports.rules = requireIndex(path.join(__dirname, 'rules'));

‎lib/rules/interface.js

+149
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
'use strict';
2+
3+
const naturalCompare = require('natural-compare-lite');
4+
const astUtils = require('../utils/ast');
5+
6+
/**
7+
* Gets the property name of the given `Property` node.
8+
*
9+
* - If the property's key is an `Identifier` node, this returns the key's name
10+
* whether it's a computed property or not.
11+
* - If the property has a static name, this returns the static name.
12+
* - Otherwise, this returns null.
13+
*
14+
* @param {ASTNode} node - The `Property` node to get.
15+
* @returns {string|null} The property name or null.
16+
* @private
17+
*/
18+
function getPropertyName(node) {
19+
return astUtils.getStaticPropertyName(node) || node.key.name || null;
20+
}
21+
22+
/**
23+
* Functions which check that the given 2 names are in specific order.
24+
*
25+
* Postfix `I` is meant insensitive.
26+
* Postfix `N` is meant natural.
27+
*
28+
* @private
29+
*/
30+
const isValidOrders = {
31+
asc(a, b) {
32+
return a <= b;
33+
},
34+
ascI(a, b) {
35+
return a.toLowerCase() <= b.toLowerCase();
36+
},
37+
ascN(a, b) {
38+
return naturalCompare(a, b) <= 0;
39+
},
40+
ascIN(a, b) {
41+
return naturalCompare(a.toLowerCase(), b.toLowerCase()) <= 0;
42+
},
43+
desc(a, b) {
44+
return isValidOrders.asc(b, a);
45+
},
46+
descI(a, b) {
47+
return isValidOrders.ascI(b, a);
48+
},
49+
descN(a, b) {
50+
return isValidOrders.ascN(b, a);
51+
},
52+
descIN(a, b) {
53+
return isValidOrders.ascIN(b, a);
54+
},
55+
};
56+
57+
module.exports = {
58+
meta: {
59+
type: 'suggestion',
60+
61+
docs: {
62+
description: 'require interface keys to be sorted',
63+
category: 'Stylistic Issues',
64+
recommended: false,
65+
url: 'https://eslint.org/docs/rules/sort-keys', // TODO
66+
},
67+
// fixable: 'code',
68+
69+
schema: [
70+
{
71+
enum: ['asc', 'desc'],
72+
},
73+
{
74+
type: 'object',
75+
properties: {
76+
caseSensitive: {
77+
type: 'boolean',
78+
},
79+
natural: {
80+
type: 'boolean',
81+
},
82+
},
83+
additionalProperties: false,
84+
},
85+
],
86+
},
87+
create: function create(context) {
88+
// Parse options.
89+
const order = context.options[0] || 'asc';
90+
const options = context.options[1];
91+
const insensitive = (options && options.caseSensitive) === false;
92+
const natural = Boolean(options && options.natural);
93+
const computedOrder = [order, insensitive && 'I', natural && 'N']
94+
.filter(Boolean)
95+
.join('');
96+
const isValidOrder = isValidOrders[computedOrder];
97+
98+
// The stack to save the previous property's name for each object literals.
99+
let stack = null;
100+
101+
const visitor = node => {
102+
const { prevName } = stack;
103+
const thisName = getPropertyName(node);
104+
105+
stack.prevName = thisName || prevName;
106+
107+
if (!prevName || !thisName) {
108+
return;
109+
}
110+
111+
if (!isValidOrder(prevName, thisName)) {
112+
context.report({
113+
node,
114+
loc: node.key.loc,
115+
message:
116+
"Expected interface keys to be in {{natural}}{{insensitive}}{{order}}ending order. '{{thisName}}' should be before '{{prevName}}'.",
117+
data: {
118+
thisName,
119+
prevName,
120+
order,
121+
insensitive: insensitive ? 'insensitive ' : '',
122+
natural: natural ? 'natural ' : '',
123+
},
124+
});
125+
}
126+
};
127+
128+
return {
129+
TSInterfaceDeclaration() {
130+
stack = {
131+
upper: stack,
132+
prevName: null,
133+
};
134+
},
135+
136+
'TSInterfaceDeclaration:exit'() {
137+
stack = stack.upper;
138+
},
139+
140+
TSPropertySignature(node) {
141+
return visitor(node);
142+
},
143+
144+
TSMethodSignature(node) {
145+
return visitor(node);
146+
},
147+
};
148+
},
149+
};

‎lib/utils/ast.js

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
'use strict';
2+
3+
module.exports = {
4+
/**
5+
* Gets the property name of a given node.
6+
* The node can be a MemberExpression, a Property, or a MethodDefinition.
7+
*
8+
* If the name is dynamic, this returns `null`.
9+
*
10+
* For examples:
11+
*
12+
* a.b // => "b"
13+
* a["b"] // => "b"
14+
* a['b'] // => "b"
15+
* a[`b`] // => "b"
16+
* a[100] // => "100"
17+
* a[b] // => null
18+
* a["a" + "b"] // => null
19+
* a[tag`b`] // => null
20+
* a[`${b}`] // => null
21+
*
22+
* let a = {b: 1} // => "b"
23+
* let a = {["b"]: 1} // => "b"
24+
* let a = {['b']: 1} // => "b"
25+
* let a = {[`b`]: 1} // => "b"
26+
* let a = {[100]: 1} // => "100"
27+
* let a = {[b]: 1} // => null
28+
* let a = {["a" + "b"]: 1} // => null
29+
* let a = {[tag`b`]: 1} // => null
30+
* let a = {[`${b}`]: 1} // => null
31+
*
32+
* @param {ASTNode} node - The node to get.
33+
* @returns {string|null} The property name if static. Otherwise, null.
34+
*/
35+
getStaticPropertyName(node) {
36+
let prop;
37+
38+
switch (node && node.type) {
39+
// case 'Property':
40+
// case 'MethodDefinition':
41+
case 'TSPropertySignature':
42+
case 'TSMethodSignature':
43+
prop = node.key;
44+
break;
45+
46+
// case 'MemberExpression':
47+
// prop = node.property;
48+
// break;
49+
50+
// no default
51+
}
52+
53+
switch (prop && prop.type) {
54+
case 'Literal':
55+
return String(prop.value);
56+
57+
case 'TemplateLiteral':
58+
if (prop.expressions.length === 0 && prop.quasis.length === 1) {
59+
return prop.quasis[0].value.cooked;
60+
}
61+
break;
62+
63+
case 'Identifier':
64+
if (!node.computed) {
65+
return prop.name;
66+
}
67+
break;
68+
69+
// no default
70+
}
71+
72+
return null;
73+
},
74+
};

‎package.json

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
{
2+
"name": "eslint-plugin-typescript-sort-keys",
3+
"version": "0.0.0",
4+
"description": "Sort interface and string enum keys",
5+
"keywords": [
6+
"eslint",
7+
"eslintplugin",
8+
"eslint-plugin"
9+
],
10+
"author": "infctr",
11+
"main": "lib/index.js",
12+
"scripts": {
13+
"lint": "eslint lib/ tests/",
14+
"lint:fix": "eslint lib/ tests/ --fix",
15+
"docs": "eslint-docs",
16+
"docs:check": "eslint-docs check",
17+
"format": "prettier --write lib/**/*.js tests/**/*.js",
18+
"mocha": "mocha tests --recursive --reporter=dot",
19+
"pretest": "yarn lint",
20+
"test": "mocha tests --recursive --reporter=dot",
21+
"posttest": "yarn docs:check",
22+
"precommit": "npm test && lint-staged"
23+
},
24+
"dependencies": {
25+
"natural-compare-lite": "~1.4.0",
26+
"requireindex": "~1.2.0"
27+
},
28+
"devDependencies": {
29+
"eslint": "~5.12.1",
30+
"eslint-config-airbnb-base": "~13.1.0",
31+
"eslint-config-prettier": "~3.5.0",
32+
"eslint-docs": "~0.3.0",
33+
"eslint-plugin-import": "~2.14.0",
34+
"eslint-plugin-node": "~8.0.1",
35+
"eslint-plugin-prettier": "~3.0.1",
36+
"husky": "~1.3.1",
37+
"lint-staged": "~8.1.0",
38+
"mocha": "~5.2.0",
39+
"prettier": "~1.15.3",
40+
"typescript": "^3.2.4",
41+
"typescript-eslint-parser": "^22.0.0"
42+
},
43+
"engines": {
44+
"node": ">=8.0.0"
45+
},
46+
"files": [
47+
"lib"
48+
],
49+
"lint-staged": {
50+
"*.js": [
51+
"prettier --write",
52+
"git add"
53+
]
54+
},
55+
"license": "ISC"
56+
}

‎tests/lib/rules/.eslintrc.js

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module.exports = {
2+
rules: {
3+
'prettier/prettier': [
4+
2,
5+
{
6+
printWidth: 200,
7+
},
8+
],
9+
},
10+
};

‎tests/lib/rules/.prettierrc.js

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
module.exports = {
2+
printWidth: 200,
3+
trailingComma: 'all',
4+
};

‎tests/lib/rules/interface.js

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
'use strict';
2+
3+
const { RuleTester } = require('eslint');
4+
5+
const rule = require('../../../lib/rules/interface');
6+
7+
const ruleTester = new RuleTester({
8+
parser: 'typescript-eslint-parser',
9+
});
10+
11+
ruleTester.run('interface', rule, {
12+
valid: [
13+
// default (asc)
14+
{ code: 'interface I {_:T; a:T; b:T;}' },
15+
{ code: 'interface I {a:T; b:T; c:T;}' },
16+
{ code: 'interface I {a:T; b:T; b_:T;}' },
17+
{ code: 'interface I {C:T; b_:T; c:T;}' },
18+
{ code: 'interface I {$:T; A:T; _:T; a:T;}' },
19+
{ code: "interface I {1:T; '11':T; 2:T; A:T;}" },
20+
{ code: "interface I {'#':T; 'Z':T; À:T; è:T;}" },
21+
22+
// methods (asc)
23+
{ code: 'interface I {_:T; a():T; b:T;}' },
24+
{ code: 'interface I {a():T; b:T; c:T;}' },
25+
],
26+
invalid: [
27+
// default (asc)
28+
{
29+
code: 'interface I {a:T; _:T; b:T;}',
30+
errors: ["Expected interface keys to be in ascending order. '_' should be before 'a'."],
31+
},
32+
{
33+
code: 'interface I {a:T; c:T; b:T;}',
34+
errors: ["Expected interface keys to be in ascending order. 'b' should be before 'c'."],
35+
},
36+
{
37+
code: 'interface I {b_:T; a:T; b:T;}',
38+
errors: ["Expected interface keys to be in ascending order. 'a' should be before 'b_'."],
39+
},
40+
{
41+
code: 'interface I {b_:T; c:T; C:T;}',
42+
errors: ["Expected interface keys to be in ascending order. 'C' should be before 'c'."],
43+
},
44+
{
45+
code: 'interface I {$:T; _:T; A:T; a:T;}',
46+
errors: ["Expected interface keys to be in ascending order. 'A' should be before '_'."],
47+
},
48+
{
49+
code: "interface I {1:T; 2:T; A:T; '11':T;}",
50+
errors: ["Expected interface keys to be in ascending order. '11' should be before 'A'."],
51+
},
52+
{
53+
code: "interface I {'#':T; À:T; 'Z':T; è:T;}",
54+
errors: ["Expected interface keys to be in ascending order. 'Z' should be before 'À'."],
55+
},
56+
57+
// methods
58+
{
59+
code: "interface I {1:T; 2:T; A():T; '11':T;}",
60+
errors: ["Expected interface keys to be in ascending order. '11' should be before 'A'."],
61+
},
62+
{
63+
code: "interface I {'#'():T; À():T; 'Z':T; è:T;}",
64+
errors: ["Expected interface keys to be in ascending order. 'Z' should be before 'À'."],
65+
},
66+
],
67+
});

‎yarn.lock

+2,738
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)
Please sign in to comment.