Skip to content

Commit 09b790c

Browse files
authored
Merge pull request #13 from nhannah/add-required-first-option
add requiredFirst option
2 parents 697cb77 + 081d8e7 commit 09b790c

File tree

10 files changed

+612
-43
lines changed

10 files changed

+612
-43
lines changed

docs/rules/interface.md

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ interface U {
3636
10: T;
3737
}
3838

39+
// Non-required first order by default.
40+
interface U {
41+
b?: T;
42+
a: T;
43+
c: T;
44+
}
45+
3946
interface U {
4047
a: T;
4148
['c']: T;
@@ -73,6 +80,13 @@ interface U {
7380
2: T;
7481
}
7582

83+
// Non-required first order by default.
84+
interface U {
85+
a: T;
86+
b?: T;
87+
c: T;
88+
}
89+
7690
// This rule checks computed properties which have a simple name as well.
7791
interface U {
7892
a: T;
@@ -88,7 +102,7 @@ interface U {
88102
"typescript-sort-keys/interface": [
89103
"error",
90104
"asc",
91-
{ "caseSensitive": true, "natural": false }
105+
{ "caseSensitive": true, "natural": false, "requiredFirst": false }
92106
]
93107
}
94108
```
@@ -98,10 +112,11 @@ The 1st option is `"asc"` or `"desc"`.
98112
- `"asc"` (default) - enforce properties to be in ascending order.
99113
- `"desc"` - enforce properties to be in descending order.
100114

101-
The 2nd option is an object which has 2 properties.
115+
The 2nd option is an object which has 3 properties.
102116

103117
- `caseSensitive` - if `true`, enforce properties to be in case-sensitive order. Default is `true`.
104118
- `natural` - if `true`, enforce properties to be in natural order. Default is `false`. Natural Order compares strings containing combination of letters and numbers in the way a human being would sort. It basically sorts numerically, instead of sorting alphabetically. So the number 10 comes after the number 3 in Natural Sorting.
119+
- `requiredFirst` - if `true`, enforce optional properties to come after required ones.
105120

106121
Example for a list:
107122

@@ -144,6 +159,13 @@ interface U {
144159
a: T;
145160
}
146161

162+
// Non-required first order by default.
163+
interface U {
164+
a: T;
165+
b?: T;
166+
c: T;
167+
}
168+
147169
// Non-natural order by default.
148170
interface U {
149171
10: T;
@@ -175,6 +197,13 @@ interface U {
175197
C: T;
176198
}
177199

200+
// Non-required first order by default.
201+
interface U {
202+
c: T;
203+
b?: T;
204+
a: T;
205+
}
206+
178207
// Non-natural order by default.
179208
interface U {
180209
2: T;
@@ -249,6 +278,34 @@ interface U {
249278
}
250279
```
251280

281+
### required
282+
283+
Examples of **incorrect** code for the `{ requiredFirst: true }` option:
284+
285+
```ts
286+
/* eslint typescript-sort-keys/interface: ["error", "asc", { requiredFirst: true }] */
287+
288+
interface U {
289+
d: T;
290+
c?: T;
291+
b?: T;
292+
a: T;
293+
}
294+
```
295+
296+
Examples of **correct** code for the `{ requiredFirst: true }` option:
297+
298+
```ts
299+
/* eslint typescript-sort-keys/interface: ["error", "asc", { requiredFirst: true }] */
300+
301+
interface U {
302+
a: T;
303+
d: T;
304+
b?: T;
305+
c?: T;
306+
}
307+
```
308+
252309
## When Not To Use It
253310

254311
If you don't want to notify about properties' order, then it's safe to disable this rule.

lib/plugin.js

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
const assert = require('assert');
22

3-
const { getPropertyName } = require('./utils/ast');
3+
const { getPropertyName, getPropertyOptional } = require('./utils/ast');
44
const { compareFunctions } = require('./utils/compareFunctions');
55

66
function createNodeSwapper(context) {
@@ -131,6 +131,7 @@ module.exports = function createReporter(context, createReportObject) {
131131
const options = context.options[1];
132132
const insensitive = (options && options.caseSensitive) === false;
133133
const natural = Boolean(options && options.natural);
134+
const requiredFirst = (options && options.requiredFirst) === true;
134135
const computedOrder = [order, insensitive && 'I', natural && 'N']
135136
.filter(Boolean)
136137
.join('');
@@ -139,9 +140,21 @@ module.exports = function createReporter(context, createReportObject) {
139140
const swapNodes = createNodeSwapper(context);
140141

141142
return body => {
142-
const sortedBody = [...body].sort((a, b) => {
143-
return compareFn(getPropertyName(a), getPropertyName(b));
144-
});
143+
const required = [...body]
144+
.filter(node => !getPropertyOptional(node))
145+
.sort((a, b) => {
146+
return compareFn(getPropertyName(a), getPropertyName(b));
147+
});
148+
const optional = [...body]
149+
.filter(node => getPropertyOptional(node))
150+
.sort((a, b) => {
151+
return compareFn(getPropertyName(a), getPropertyName(b));
152+
});
153+
const sortedBody = requiredFirst
154+
? [...required, ...optional]
155+
: [...body].sort((a, b) => {
156+
return compareFn(getPropertyName(a), getPropertyName(b));
157+
});
145158

146159
const nodePositions = new Map(
147160
body.map(n => [
@@ -156,7 +169,15 @@ module.exports = function createReporter(context, createReportObject) {
156169
const prevNodeName = getPropertyName(prevNode);
157170
const currentNodeName = getPropertyName(currentNode);
158171

159-
if (compareFn(prevNodeName, currentNodeName) > 0) {
172+
if (
173+
(!requiredFirst && compareFn(prevNodeName, currentNodeName) > 0) ||
174+
(requiredFirst &&
175+
getPropertyOptional(prevNode) === getPropertyOptional(currentNode) &&
176+
compareFn(prevNodeName, currentNodeName) > 0) ||
177+
(requiredFirst &&
178+
getPropertyOptional(prevNode) !== getPropertyOptional(currentNode) &&
179+
getPropertyOptional(prevNode))
180+
) {
160181
const targetPosition = sortedBody.indexOf(currentNode);
161182
const replaceNode = body[targetPosition];
162183
const { data, ...rest } = createReportObject(currentNode, replaceNode);
@@ -179,6 +200,7 @@ module.exports = function createReporter(context, createReportObject) {
179200
order,
180201
insensitive: insensitive ? 'insensitive ' : '',
181202
natural: natural ? 'natural ' : '',
203+
requiredFirst: requiredFirst ? 'required first ' : '',
182204
...data,
183205
},
184206
fix: fixer => {

lib/rules/interface.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ module.exports = {
2828
natural: {
2929
type: 'boolean',
3030
},
31+
requiredFirst: {
32+
type: 'boolean',
33+
},
3134
},
3235
additionalProperties: false,
3336
},
@@ -38,7 +41,7 @@ module.exports = {
3841
const compareNodeListAndReport = createReporter(context, currentNode => ({
3942
loc: currentNode.key ? currentNode.key.loc : currentNode.loc,
4043
message:
41-
'Expected interface keys to be in {{ natural }}{{ insensitive }}{{ order }}ending order. ' +
44+
'Expected interface keys to be in {{ requiredFirst }}{{ natural }}{{ insensitive }}{{ order }}ending order. ' +
4245
`'{{ thisName }}' should be before '{{ prevName }}'.`,
4346
}));
4447
return {

lib/utils/ast.js

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ module.exports = {
1212
},
1313

1414
/**
15-
* Gets the property name of a given node.
15+
* Gets the property name and optional value of a given node.
1616
* The node can be a TSPropertySignature, or a TSMethodSignature.
1717
*
1818
* If the name is dynamic, this returns `null`.
@@ -39,10 +39,14 @@ module.exports = {
3939
* let a = {[tag`b`]: 1} // => null
4040
* let a = {[`${b}`]: 1} // => null
4141
*
42+
* @typedef {Object} NameAndOptional
43+
* @property {string|undefined} name - Name of node
44+
* @property {boolean|undefined} isOptional - Optional value of node
45+
*
4246
* @param {ASTNode} node - The node to get.
43-
* @returns {string|null} The property name if static. Otherwise, null.
47+
* @returns {NameAndOptional} - The property name and optional value if static.
4448
*/
45-
getStaticPropertyName(node) {
49+
getStaticPropertyNameAndOptional(node) {
4650
let prop;
4751

4852
switch (node && node.type) {
@@ -70,24 +74,47 @@ module.exports = {
7074

7175
switch (prop && prop.type) {
7276
case 'Literal':
73-
return String(prop.value);
77+
return {
78+
name: String(prop.value),
79+
isOptional: prop.optional,
80+
};
7481

7582
case 'TemplateLiteral':
7683
if (prop.expressions.length === 0 && prop.quasis.length === 1) {
77-
return prop.quasis[0].value.cooked;
84+
return {
85+
name: prop.quasis[0].value.cooked,
86+
isOptional: prop.optional,
87+
};
7888
}
7989
break;
8090

8191
case 'Identifier':
8292
if (!node.computed) {
83-
return prop.name;
93+
return {
94+
name: prop.name,
95+
isOptional: prop.optional,
96+
};
8497
}
8598
break;
8699

87100
// no default
88101
}
89102

90-
return null;
103+
return {};
104+
},
105+
106+
/**
107+
*
108+
* Gets if the property is optional given `Property` node.
109+
*
110+
* @param {ASTNode} node - The `Property` node to get.
111+
* @returns {boolean} - The optional value or false.
112+
*/
113+
getPropertyOptional(node) {
114+
const { isOptional } = module.exports.getStaticPropertyNameAndOptional(
115+
node,
116+
);
117+
return !!(isOptional || node.optional);
91118
},
92119

93120
/**
@@ -99,14 +126,12 @@ module.exports = {
99126
* - Otherwise, this returns null.
100127
*
101128
* @param {ASTNode} node - The `Property` node to get.
102-
* @returns {string|null} The property name or null.
129+
* @returns {string|null} - The property name or null.
103130
* @private
104131
*/
105132
getPropertyName(node) {
106-
return (
107-
module.exports.getStaticPropertyName(node) ||
108-
(node.key && node.key.name) ||
109-
null
110-
);
133+
const { name } = module.exports.getStaticPropertyNameAndOptional(node);
134+
135+
return name || (node.key && node.key.name) || null;
111136
},
112137
};

tests/autofix.spec.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,52 @@ describe('autofix', () => {
5555
assert.strictEqual(output, expected);
5656
});
5757
});
58+
59+
describe('autofix-required-first', () => {
60+
it('should properly format comments and indent level for required first option', () => {
61+
const { name: tmpDir } = tmp.dirSync({
62+
prefix: 'typescript-sort-keys-',
63+
unsafeCleanup: true,
64+
});
65+
66+
const testFilePath = Path.join(tmpDir, 'autofix-required-first.ts');
67+
68+
const input = fs.readFileSync('tests/fixtures/autofix.input.ts', 'utf8');
69+
70+
const expected = fs.readFileSync(
71+
'tests/fixtures/autofix-required-first.output.ts',
72+
'utf8',
73+
);
74+
75+
fs.writeFileSync(testFilePath, input);
76+
77+
const result = spawn.sync(
78+
'eslint',
79+
[
80+
'--ext',
81+
'.ts',
82+
'--rulesdir',
83+
'lib/rules',
84+
'--config',
85+
require.resolve('./fixtures/.eslintrcRequiredFirst'),
86+
testFilePath,
87+
'--fix',
88+
],
89+
{
90+
encoding: 'utf8',
91+
},
92+
);
93+
94+
if (result.status !== 0) {
95+
// eslint-disable-next-line no-console
96+
console.error(result.stdout);
97+
// eslint-disable-next-line no-console
98+
console.error(result.stderr);
99+
throw new Error(`Process exited with status ${result.status}`);
100+
}
101+
102+
const output = fs.readFileSync(testFilePath, 'utf8');
103+
104+
assert.strictEqual(output, expected);
105+
});
106+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
module.exports = {
2+
root: true,
3+
parser: '@typescript-eslint/parser',
4+
plugins: ['@typescript-eslint'],
5+
rules: {
6+
interface: ['error', 'asc', { caseSensitive: true, natural: true, requiredFirst: true }],
7+
'string-enum': 'error',
8+
},
9+
settings: {
10+
'import/parsers': {
11+
'@typescript-eslint/parser': ['.ts'],
12+
},
13+
},
14+
};

0 commit comments

Comments
 (0)