diff --git a/docs/rules/interface.md b/docs/rules/interface.md index afabb59..0e4f36a 100644 --- a/docs/rules/interface.md +++ b/docs/rules/interface.md @@ -36,6 +36,13 @@ interface U { 10: T; } +// Non-required first order by default. +interface U { + b?: T; + a: T; + c: T; +} + interface U { a: T; ['c']: T; @@ -73,6 +80,13 @@ interface U { 2: T; } +// Non-required first order by default. +interface U { + a: T; + b?: T; + c: T; +} + // This rule checks computed properties which have a simple name as well. interface U { a: T; @@ -88,7 +102,7 @@ interface U { "typescript-sort-keys/interface": [ "error", "asc", - { "caseSensitive": true, "natural": false } + { "caseSensitive": true, "natural": false, "requiredFirst": false } ] } ``` @@ -98,10 +112,11 @@ The 1st option is `"asc"` or `"desc"`. - `"asc"` (default) - enforce properties to be in ascending order. - `"desc"` - enforce properties to be in descending order. -The 2nd option is an object which has 2 properties. +The 2nd option is an object which has 3 properties. - `caseSensitive` - if `true`, enforce properties to be in case-sensitive order. Default is `true`. - `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. +- `requiredFirst` - if `true`, enforce optional properties to come after required ones. Example for a list: @@ -144,6 +159,13 @@ interface U { a: T; } +// Non-required first order by default. +interface U { + a: T; + b?: T; + c: T; +} + // Non-natural order by default. interface U { 10: T; @@ -175,6 +197,13 @@ interface U { C: T; } +// Non-required first order by default. +interface U { + c: T; + b?: T; + a: T; +} + // Non-natural order by default. interface U { 2: T; @@ -249,6 +278,34 @@ interface U { } ``` +### required + +Examples of **incorrect** code for the `{ requiredFirst: true }` option: + +```ts +/* eslint typescript-sort-keys/interface: ["error", "asc", { requiredFirst: true }] */ + +interface U { + d: T; + c?: T; + b?: T; + a: T; +} +``` + +Examples of **correct** code for the `{ requiredFirst: true }` option: + +```ts +/* eslint typescript-sort-keys/interface: ["error", "asc", { requiredFirst: true }] */ + +interface U { + a: T; + d: T; + b?: T; + c?: T; +} +``` + ## When Not To Use It If you don't want to notify about properties' order, then it's safe to disable this rule. diff --git a/lib/plugin.js b/lib/plugin.js index 2372c70..a250f30 100644 --- a/lib/plugin.js +++ b/lib/plugin.js @@ -1,6 +1,6 @@ const assert = require('assert'); -const { getPropertyName } = require('./utils/ast'); +const { getPropertyName, getPropertyOptional } = require('./utils/ast'); const { compareFunctions } = require('./utils/compareFunctions'); function createNodeSwapper(context) { @@ -131,6 +131,7 @@ module.exports = function createReporter(context, createReportObject) { const options = context.options[1]; const insensitive = (options && options.caseSensitive) === false; const natural = Boolean(options && options.natural); + const requiredFirst = (options && options.requiredFirst) === true; const computedOrder = [order, insensitive && 'I', natural && 'N'] .filter(Boolean) .join(''); @@ -139,9 +140,21 @@ module.exports = function createReporter(context, createReportObject) { const swapNodes = createNodeSwapper(context); return body => { - const sortedBody = [...body].sort((a, b) => { - return compareFn(getPropertyName(a), getPropertyName(b)); - }); + const required = [...body] + .filter(node => !getPropertyOptional(node)) + .sort((a, b) => { + return compareFn(getPropertyName(a), getPropertyName(b)); + }); + const optional = [...body] + .filter(node => getPropertyOptional(node)) + .sort((a, b) => { + return compareFn(getPropertyName(a), getPropertyName(b)); + }); + const sortedBody = requiredFirst + ? [...required, ...optional] + : [...body].sort((a, b) => { + return compareFn(getPropertyName(a), getPropertyName(b)); + }); const nodePositions = new Map( body.map(n => [ @@ -156,7 +169,15 @@ module.exports = function createReporter(context, createReportObject) { const prevNodeName = getPropertyName(prevNode); const currentNodeName = getPropertyName(currentNode); - if (compareFn(prevNodeName, currentNodeName) > 0) { + if ( + (!requiredFirst && compareFn(prevNodeName, currentNodeName) > 0) || + (requiredFirst && + getPropertyOptional(prevNode) === getPropertyOptional(currentNode) && + compareFn(prevNodeName, currentNodeName) > 0) || + (requiredFirst && + getPropertyOptional(prevNode) !== getPropertyOptional(currentNode) && + getPropertyOptional(prevNode)) + ) { const targetPosition = sortedBody.indexOf(currentNode); const replaceNode = body[targetPosition]; const { data, ...rest } = createReportObject(currentNode, replaceNode); @@ -179,6 +200,7 @@ module.exports = function createReporter(context, createReportObject) { order, insensitive: insensitive ? 'insensitive ' : '', natural: natural ? 'natural ' : '', + requiredFirst: requiredFirst ? 'required first ' : '', ...data, }, fix: fixer => { diff --git a/lib/rules/interface.js b/lib/rules/interface.js index cc0e250..5386778 100644 --- a/lib/rules/interface.js +++ b/lib/rules/interface.js @@ -28,6 +28,9 @@ module.exports = { natural: { type: 'boolean', }, + requiredFirst: { + type: 'boolean', + }, }, additionalProperties: false, }, @@ -38,7 +41,7 @@ module.exports = { const compareNodeListAndReport = createReporter(context, currentNode => ({ loc: currentNode.key ? currentNode.key.loc : currentNode.loc, message: - 'Expected interface keys to be in {{ natural }}{{ insensitive }}{{ order }}ending order. ' + + 'Expected interface keys to be in {{ requiredFirst }}{{ natural }}{{ insensitive }}{{ order }}ending order. ' + `'{{ thisName }}' should be before '{{ prevName }}'.`, })); return { diff --git a/lib/utils/ast.js b/lib/utils/ast.js index d17882a..d5ad8c6 100644 --- a/lib/utils/ast.js +++ b/lib/utils/ast.js @@ -12,7 +12,7 @@ module.exports = { }, /** - * Gets the property name of a given node. + * Gets the property name and optional value of a given node. * The node can be a TSPropertySignature, or a TSMethodSignature. * * If the name is dynamic, this returns `null`. @@ -39,10 +39,14 @@ module.exports = { * let a = {[tag`b`]: 1} // => null * let a = {[`${b}`]: 1} // => null * + * @typedef {Object} NameAndOptional + * @property {string|undefined} name - Name of node + * @property {boolean|undefined} isOptional - Optional value of node + * * @param {ASTNode} node - The node to get. - * @returns {string|null} The property name if static. Otherwise, null. + * @returns {NameAndOptional} - The property name and optional value if static. */ - getStaticPropertyName(node) { + getStaticPropertyNameAndOptional(node) { let prop; switch (node && node.type) { @@ -70,24 +74,47 @@ module.exports = { switch (prop && prop.type) { case 'Literal': - return String(prop.value); + return { + name: String(prop.value), + isOptional: prop.optional, + }; case 'TemplateLiteral': if (prop.expressions.length === 0 && prop.quasis.length === 1) { - return prop.quasis[0].value.cooked; + return { + name: prop.quasis[0].value.cooked, + isOptional: prop.optional, + }; } break; case 'Identifier': if (!node.computed) { - return prop.name; + return { + name: prop.name, + isOptional: prop.optional, + }; } break; // no default } - return null; + return {}; + }, + + /** + * + * Gets if the property is optional given `Property` node. + * + * @param {ASTNode} node - The `Property` node to get. + * @returns {boolean} - The optional value or false. + */ + getPropertyOptional(node) { + const { isOptional } = module.exports.getStaticPropertyNameAndOptional( + node, + ); + return !!(isOptional || node.optional); }, /** @@ -99,14 +126,12 @@ module.exports = { * - Otherwise, this returns null. * * @param {ASTNode} node - The `Property` node to get. - * @returns {string|null} The property name or null. + * @returns {string|null} - The property name or null. * @private */ getPropertyName(node) { - return ( - module.exports.getStaticPropertyName(node) || - (node.key && node.key.name) || - null - ); + const { name } = module.exports.getStaticPropertyNameAndOptional(node); + + return name || (node.key && node.key.name) || null; }, }; diff --git a/tests/autofix.spec.js b/tests/autofix.spec.js index 10ba9c1..c15e557 100644 --- a/tests/autofix.spec.js +++ b/tests/autofix.spec.js @@ -55,3 +55,52 @@ describe('autofix', () => { assert.strictEqual(output, expected); }); }); + +describe('autofix-required-first', () => { + it('should properly format comments and indent level for required first option', () => { + const { name: tmpDir } = tmp.dirSync({ + prefix: 'typescript-sort-keys-', + unsafeCleanup: true, + }); + + const testFilePath = Path.join(tmpDir, 'autofix-required-first.ts'); + + const input = fs.readFileSync('tests/fixtures/autofix.input.ts', 'utf8'); + + const expected = fs.readFileSync( + 'tests/fixtures/autofix-required-first.output.ts', + 'utf8', + ); + + fs.writeFileSync(testFilePath, input); + + const result = spawn.sync( + 'eslint', + [ + '--ext', + '.ts', + '--rulesdir', + 'lib/rules', + '--config', + require.resolve('./fixtures/.eslintrcRequiredFirst'), + testFilePath, + '--fix', + ], + { + encoding: 'utf8', + }, + ); + + if (result.status !== 0) { + // eslint-disable-next-line no-console + console.error(result.stdout); + // eslint-disable-next-line no-console + console.error(result.stderr); + throw new Error(`Process exited with status ${result.status}`); + } + + const output = fs.readFileSync(testFilePath, 'utf8'); + + assert.strictEqual(output, expected); + }); +}); diff --git a/tests/fixtures/.eslintrcRequiredFirst.js b/tests/fixtures/.eslintrcRequiredFirst.js new file mode 100644 index 0000000..0494595 --- /dev/null +++ b/tests/fixtures/.eslintrcRequiredFirst.js @@ -0,0 +1,14 @@ +module.exports = { + root: true, + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint'], + rules: { + interface: ['error', 'asc', { caseSensitive: true, natural: true, requiredFirst: true }], + 'string-enum': 'error', + }, + settings: { + 'import/parsers': { + '@typescript-eslint/parser': ['.ts'], + }, + }, +}; \ No newline at end of file diff --git a/tests/fixtures/autofix-required-first.output.ts b/tests/fixtures/autofix-required-first.output.ts new file mode 100644 index 0000000..81bb299 --- /dev/null +++ b/tests/fixtures/autofix-required-first.output.ts @@ -0,0 +1,112 @@ +// So typescript treats this as a module +export {}; + +class GraphQLExtension {_: T} + +interface GraphQLResponse {} + +namespace Koa { + export interface Context {} +} + +const inlineArrow: (props: {bar: boolean, foo: boolean; baz?: boolean;}) => null = ({...props}) => null; + +const inlineArrow2: (props: {baz: boolean, foo: boolean; bar?: boolean;}) => null = ({...props}) => null; + +const inlineWeird: (props: {bar: boolean,baz: boolean, + foo?: boolean;}) => null = ({...props}) => null; + +function inlineGeneric({...props}: T | {bar: boolean; foo: boolean; baz?: boolean}) { + return null +} + +enum InlineEnum {a="T", b="T", c="T", d="T", e="T"} + +enum InlineEnum2 {Bar = 'BAR',Baz = 'BAZ', Foo = 'FOO' } + +enum InlineEnum3 {C="T", b_="T", c="T"} + +enum WeirdEnum { + Bar = 'BAR',Baz = 'BAZ', Foo = 'FOO'} + +interface InlineInterface {b:"T"; d:"T"; e: "T"; a?:"T", c?:"T";} + +class Class extends GraphQLExtension<{ + graphqlResponse: GraphQLResponse; + context?: Koa.Context; +}> { + public method(o: { + graphqlResponse: GraphQLResponse; + context?: Koa.Context; + }): void | { context?: Koa.Context, graphqlResponse?: GraphQLResponse; } { + // + } +} + +interface Interface { + // %bar + bar: boolean; + /** + * %foo + */ + foo: boolean; + /* %baz */ + baz?: boolean; +} + +type Type1 = Partial<{ + /** + * %bar + */ + bar: boolean; + /* %baz */ baz: boolean; + + // %foo + foo?: boolean; +}> & { + /** + * %bar + */ + bar: boolean; + +// %baz + baz: boolean; + /* %foo */ + foo?: boolean; +} & { + [K in keyof TKey]: boolean; + }; + +enum StringEnum { + /** + * %bar + */ + Bar = 'BAR', + + // %baz + Baz = 'BAZ', + + /* %foo */ + Foo = 'FOO' +} + +type Type2 = { + /** + * %bar + */ + bar: boolean; + +// %baz + baz: boolean; + /* %foo */ + foo?: boolean; +} + +interface ClockConstructor { + new (hour: number, minute: number): ClockInterface; + new (hour: number): ClockInterface; +} + +interface ClockInterface { + tick(): void; +} diff --git a/tests/fixtures/autofix.input.ts b/tests/fixtures/autofix.input.ts index db61344..f11df65 100644 --- a/tests/fixtures/autofix.input.ts +++ b/tests/fixtures/autofix.input.ts @@ -9,14 +9,14 @@ namespace Koa { export interface Context {} } -const inlineArrow: (props: {foo: boolean; baz: boolean; bar: boolean}) => null = ({...props}) => null; +const inlineArrow: (props: {foo: boolean; baz?: boolean; bar: boolean}) => null = ({...props}) => null; -const inlineArrow2: (props: {foo: boolean; bar: boolean; baz: boolean}) => null = ({...props}) => null; +const inlineArrow2: (props: {foo: boolean; bar?: boolean; baz: boolean}) => null = ({...props}) => null; -const inlineWeird: (props: {foo: boolean;baz: boolean, +const inlineWeird: (props: {foo?: boolean;baz: boolean, bar: boolean}) => null = ({...props}) => null; -function inlineGeneric({...props}: T | {foo: boolean; bar: boolean; baz: boolean}) { +function inlineGeneric({...props}: T | {foo: boolean; bar: boolean; baz?: boolean}) { return null } @@ -29,15 +29,15 @@ enum InlineEnum3 {b_="T", c="T", C="T"} enum WeirdEnum { Foo = 'FOO',Baz = 'BAZ', Bar = 'BAR',} -interface InlineInterface {e: "T"; c:"T"; d:"T"; b:"T"; a:"T"} +interface InlineInterface {e: "T"; c?:"T"; d:"T"; b:"T"; a?:"T"} class Class extends GraphQLExtension<{ graphqlResponse: GraphQLResponse; - context: Koa.Context; + context?: Koa.Context; }> { public method(o: { graphqlResponse: GraphQLResponse; - context: Koa.Context; + context?: Koa.Context; }): void | { graphqlResponse?: GraphQLResponse; context?: Koa.Context } { // } @@ -49,14 +49,14 @@ interface Interface { */ foo: boolean; /* %baz */ - baz: boolean; + baz?: boolean; // %bar bar: boolean; } type Type1 = Partial<{ // %foo - foo: boolean; + foo?: boolean; /* %baz */ baz: boolean; /** @@ -64,7 +64,7 @@ type Type1 = Partial<{ */ bar: boolean; }> & {/* %foo */ - foo: boolean; + foo?: boolean; // %baz baz: boolean; @@ -90,7 +90,7 @@ enum StringEnum { } type Type2 = {/* %foo */ - foo: boolean; + foo?: boolean; // %baz baz: boolean; diff --git a/tests/fixtures/autofix.output.ts b/tests/fixtures/autofix.output.ts index 7313d57..37b38cd 100644 --- a/tests/fixtures/autofix.output.ts +++ b/tests/fixtures/autofix.output.ts @@ -9,14 +9,14 @@ namespace Koa { export interface Context {} } -const inlineArrow: (props: {bar: boolean, baz: boolean; foo: boolean;}) => null = ({...props}) => null; +const inlineArrow: (props: {bar: boolean, baz?: boolean; foo: boolean;}) => null = ({...props}) => null; -const inlineArrow2: (props: {bar: boolean; baz: boolean, foo: boolean;}) => null = ({...props}) => null; +const inlineArrow2: (props: {bar?: boolean; baz: boolean, foo: boolean;}) => null = ({...props}) => null; const inlineWeird: (props: {bar: boolean,baz: boolean, - foo: boolean;}) => null = ({...props}) => null; + foo?: boolean;}) => null = ({...props}) => null; -function inlineGeneric({...props}: T | {bar: boolean; baz: boolean, foo: boolean;}) { +function inlineGeneric({...props}: T | {bar: boolean; baz?: boolean, foo: boolean;}) { return null } @@ -29,14 +29,14 @@ enum InlineEnum3 {C="T", b_="T", c="T"} enum WeirdEnum { Bar = 'BAR',Baz = 'BAZ', Foo = 'FOO'} -interface InlineInterface {a:"T", b:"T"; c:"T"; d:"T"; e: "T";} +interface InlineInterface {a?:"T", b:"T"; c?:"T"; d:"T"; e: "T";} class Class extends GraphQLExtension<{ - context: Koa.Context; + context?: Koa.Context; graphqlResponse: GraphQLResponse; }> { public method(o: { - context: Koa.Context; + context?: Koa.Context; graphqlResponse: GraphQLResponse; }): void | { context?: Koa.Context, graphqlResponse?: GraphQLResponse; } { // @@ -47,7 +47,7 @@ interface Interface { // %bar bar: boolean; /* %baz */ - baz: boolean; + baz?: boolean; /** * %foo */ @@ -62,7 +62,7 @@ type Type1 = Partial<{ /* %baz */ baz: boolean; // %foo - foo: boolean; + foo?: boolean; }> & { /** * %bar @@ -72,7 +72,7 @@ type Type1 = Partial<{ // %baz baz: boolean; /* %foo */ - foo: boolean; + foo?: boolean; } & { [K in keyof TKey]: boolean; }; @@ -99,7 +99,7 @@ type Type2 = { // %baz baz: boolean; /* %foo */ - foo: boolean; + foo?: boolean; } interface ClockConstructor { diff --git a/tests/lib/rules/interface.spec.js b/tests/lib/rules/interface.spec.js index 0637387..dfbb47b 100644 --- a/tests/lib/rules/interface.spec.js +++ b/tests/lib/rules/interface.spec.js @@ -54,6 +54,36 @@ ruleTester.run('interface', rule, { { code: "interface U {1:T; 2:T; '11':T; A:T;}", options: ['asc', { natural: true, caseSensitive: false }] }, { code: "interface U {'#':T; 'Z':T; À:T; è:T;}", options: ['asc', { natural: true, caseSensitive: false }] }, + // asc, natural, insensitive, required + { code: 'interface U {_:T; b:T; a?:T;} // asc, natural, insensitive, required', options: ['asc', { natural: true, caseSensitive: false, requiredFirst: true }] }, + { code: 'interface U {a:T; c:T; b?:T;}', options: ['asc', { natural: true, caseSensitive: false, requiredFirst: true }] }, + { code: 'interface U {b:T; b_:T; a?:T;}', options: ['asc', { natural: true, caseSensitive: false, requiredFirst: true }] }, + { code: 'interface U {C:T; c:T; b_?:T;}', options: ['asc', { natural: true, caseSensitive: false, requiredFirst: true }] }, + { code: 'interface U {c:T; C:T; b_?:T;}', options: ['asc', { natural: true, caseSensitive: false, requiredFirst: true }] }, + { code: 'interface U {$:T; _:T; A?:T; a?:T;}', options: ['asc', { natural: true, caseSensitive: false, requiredFirst: true }] }, + { code: "interface U {1:T; '11':T; A:T; 2?:T;}", options: ['asc', { natural: true, caseSensitive: false, requiredFirst: true }] }, + { code: "interface U {'Z':T; À:T; è:T; '#'?:T;}", options: ['asc', { natural: true, caseSensitive: false, requiredFirst: true }] }, + + // asc, required + { code: 'interface U {_:T; b:T; a?:T;} // asc, natural, insensitive, required', options: ['asc', { requiredFirst: true }] }, + { code: 'interface U {a:T; c:T; b?:T;}', options: ['asc', { requiredFirst: true }] }, + { code: 'interface U {b:T; b_:T; a?:T;}', options: ['asc', { requiredFirst: true }] }, + { code: 'interface U {C:T; c:T; b_?:T;}', options: ['asc', { requiredFirst: true }] }, + { code: 'interface U {1:T; 11:T; 9:T; 111?:T;}', options: ['asc', { requiredFirst: true }] }, + { code: 'interface U {$:T; _:T; A?:T; a?:T;}', options: ['asc', { requiredFirst: true }] }, + { code: "interface U {10:T; '11':T; 1?:T; 12?:T; 2?:T;}", options: ['asc', { requiredFirst: true }] }, + { code: "interface U {'Z':T; À:T; è:T; '#'?:T;}", options: ['asc', { requiredFirst: true }] }, + + // asc, natural, insensitive, not-required + { code: 'interface U {_:T; a?:T; b:T;} // asc, natural, insensitive, not-required', options: ['asc', { natural: true, caseSensitive: false, requiredFirst: false }] }, + { code: 'interface U {a:T; b?:T; c:T;}', options: ['asc', { natural: true, caseSensitive: false, requiredFirst: false }] }, + { code: 'interface U {a?:T; b:T; b_:T;}', options: ['asc', { natural: true, caseSensitive: false, requiredFirst: false }] }, + { code: 'interface U {b_?:T; C:T; c:T;}', options: ['asc', { natural: true, caseSensitive: false, requiredFirst: false }] }, + { code: 'interface U {b_?:T; c:T; C:T;}', options: ['asc', { natural: true, caseSensitive: false, requiredFirst: false }] }, + { code: 'interface U {$:T; _:T; A?:T; a?:T;}', options: ['asc', { natural: true, caseSensitive: false, requiredFirst: false }] }, + { code: "interface U {1:T; 2?:T; '11':T; A:T;}", options: ['asc', { natural: true, caseSensitive: false, requiredFirst: false }] }, + { code: "interface U {'#'?:T; 'Z':T; À:T; è:T;}", options: ['asc', { natural: true, caseSensitive: false, requiredFirst: false }] }, + // desc { code: 'interface U {b:T; a:T; _:T;} // desc', options: ['desc'] }, { code: 'interface U {c:T; b:T; a:T;}', options: ['desc'] }, @@ -92,6 +122,36 @@ ruleTester.run('interface', rule, { { code: "interface U {A:T; '11':T; 2:T; 1:T;}", options: ['desc', { natural: true, caseSensitive: false }] }, { code: "interface U {è:T; À:T; 'Z':T; '#':T;}", options: ['desc', { natural: true, caseSensitive: false }] }, + // desc, natural, insensitive, required + { code: 'interface U {b:T; _:T; a?:T;} // desc, natural, insensitive, required', options: ['desc', { natural: true, caseSensitive: false, requiredFirst: true }] }, + { code: 'interface U {c:T; a:T; b?:T;}', options: ['desc', { natural: true, caseSensitive: false, requiredFirst: true }] }, + { code: 'interface U {b_:T; b:T; a?:T;}', options: ['desc', { natural: true, caseSensitive: false, requiredFirst: true }] }, + { code: 'interface U {c:T; C:T; b_?:T;}', options: ['desc', { natural: true, caseSensitive: false, requiredFirst: true }] }, + { code: 'interface U {C:T; c:T; b_?:T;}', options: ['desc', { natural: true, caseSensitive: false, requiredFirst: true }] }, + { code: 'interface U {_:T; $:T; a?:T; A?:T;}', options: ['desc', { natural: true, caseSensitive: false, requiredFirst: true }] }, + { code: "interface U { A:T; '11':T; 1:T; 2?:T;}", options: ['desc', { natural: true, caseSensitive: false, requiredFirst: true }] }, + { code: "interface U {è:T; 'Z':T; À?:T; '#'?:T;}", options: ['desc', { natural: true, caseSensitive: false, requiredFirst: true }] }, + + // desc, required + { code: 'interface U {b:T; _:T; a?:T;} // desc, natural, insensitive, required', options: ['desc', { requiredFirst: true }] }, + { code: 'interface U {c:T; a:T; b?:T;}', options: ['desc', { requiredFirst: true }] }, + { code: 'interface U {b_:T; b:T; a?:T;}', options: ['desc', { requiredFirst: true }] }, + { code: 'interface U {c:T; C:T; b_?:T;}', options: ['desc', { requiredFirst: true }] }, + { code: 'interface U {9:T; 11:T; 1:T; 111?:T;}', options: ['desc', { requiredFirst: true }] }, + { code: 'interface U {_:T; $:T; a?:T; A?:T;}', options: ['desc', { requiredFirst: true }] }, + { code: "interface U {'11':T; 10:T; 2?:T; 12?:T; 1?:T;}", options: ['desc', { requiredFirst: true }] }, + { code: "interface U {è:T; À:T; 'Z':T; '#'?:T;}", options: ['desc', { requiredFirst: true }] }, + + // desc, natural, insensitive, not-required + { code: 'interface U {b:T; a?:T; _:T;} // desc, natural, insensitive, not-required', options: ['desc', { natural: true, caseSensitive: false, requiredFirst: false }] }, + { code: 'interface U {c:T; b?:T; a:T;}', options: ['desc', { natural: true, caseSensitive: false, requiredFirst: false }] }, + { code: 'interface U {b_:T; b:T; a?:T;}', options: ['desc', { natural: true, caseSensitive: false, requiredFirst: false }] }, + { code: 'interface U {c:T; C:T; b_?:T;}', options: ['desc', { natural: true, caseSensitive: false, requiredFirst: false }] }, + { code: 'interface U {C:T; c:T; b_?:T;}', options: ['desc', { natural: true, caseSensitive: false, requiredFirst: false }] }, + { code: 'interface U {a?:T; A?:T; _:T; $:T;}', options: ['desc', { natural: true, caseSensitive: false, requiredFirst: false }] }, + { code: "interface U {A:T; '11':T; 2?:T; 1:T;}", options: ['desc', { natural: true, caseSensitive: false, requiredFirst: false }] }, + { code: "interface U {è:T; À:T; 'Z':T; '#'?:T;}", options: ['desc', { natural: true, caseSensitive: false, requiredFirst: false }] }, + // index signatures { code: 'interface U { [nkey: number]: T; [skey: string]: T; $: T; A: T; _: T; a: T; }', options: ['asc'] }, { code: 'interface U { a: T; _: T; A: T; $: T; [skey: string]: T; [nkey: number]: T; }', options: ['desc'] }, @@ -330,6 +390,106 @@ ruleTester.run('interface', rule, { output: "interface U {'#':T; 'Z':T; À:T; è:T;}", }, + // asc, natural, insensitive, required + { + code: 'interface U {_:T; a?:T; b:T;} // asc, natural, insensitive, required', + options: ['asc', { natural: true, caseSensitive: false, requiredFirst: true }], + errors: ["Expected interface keys to be in required first natural insensitive ascending order. 'b' should be before 'a'."], + output: 'interface U {_:T; b:T; a?:T;} // asc, natural, insensitive, required', + }, + { + code: 'interface U {a:T; b?:T; c:T;}', + options: ['asc', { natural: true, caseSensitive: false, requiredFirst: true }], + errors: ["Expected interface keys to be in required first natural insensitive ascending order. 'c' should be before 'b'."], + output: 'interface U {a:T; c:T; b?:T;}', + }, + { + code: 'interface U {b:T; a?:T; b_:T;}', + options: ['asc', { natural: true, caseSensitive: false, requiredFirst: true }], + errors: ["Expected interface keys to be in required first natural insensitive ascending order. 'b_' should be before 'a'."], + output: 'interface U {b:T; b_:T; a?:T;}', + }, + { + code: 'interface U {C:T; b_?:T; c:T;}', + options: ['asc', { natural: true, caseSensitive: false, requiredFirst: true }], + errors: ["Expected interface keys to be in required first natural insensitive ascending order. 'c' should be before 'b_'."], + output: 'interface U {C:T; c:T; b_?:T;}', + }, + { + code: 'interface U {C:T; b_?:T; c:T;}', + options: ['asc', { natural: true, caseSensitive: false, requiredFirst: true }], + errors: ["Expected interface keys to be in required first natural insensitive ascending order. 'c' should be before 'b_'."], + output: 'interface U {C:T; c:T; b_?:T;}', + }, + { + code: 'interface U {$:T; A?:T; _:T; a?:T;}', + options: ['asc', { natural: true, caseSensitive: false, requiredFirst: true }], + errors: ["Expected interface keys to be in required first natural insensitive ascending order. '_' should be before 'A'."], + output: 'interface U {$:T; _:T; A?:T; a?:T;}', + }, + { + code: "interface U {1:T; '11':T; 2?:T; A:T;}", + options: ['asc', { natural: true, caseSensitive: false, requiredFirst: true }], + errors: ["Expected interface keys to be in required first natural insensitive ascending order. 'A' should be before '2'."], + output: "interface U {1:T; '11':T; A:T; 2?:T;}", + }, + { + code: "interface U {'Z':T; À:T; '#'?:T; è:T;}", + options: ['asc', { natural: true, caseSensitive: false, requiredFirst: true }], + errors: ["Expected interface keys to be in required first natural insensitive ascending order. 'è' should be before '#'."], + output: "interface U {'Z':T; À:T; è:T; '#'?:T;}", + }, + + // asc, natural, insensitive, not-required + { + code: 'interface U {_:T; b:T; a?:T;} // asc, natural, insensitive, not-required', + options: ['asc', { natural: true, caseSensitive: false, requiredFirst: false }], + errors: ["Expected interface keys to be in natural insensitive ascending order. 'a' should be before 'b'."], + output: 'interface U {_:T; a?:T; b:T;} // asc, natural, insensitive, not-required', + }, + { + code: 'interface U {b?:T; a:T; c:T;}', + options: ['asc', { natural: true, caseSensitive: false, requiredFirst: false }], + errors: ["Expected interface keys to be in natural insensitive ascending order. 'a' should be before 'b'."], + output: 'interface U {a:T; b?:T; c:T;}', + }, + { + code: 'interface U {b:T; a?:T; b_:T;}', + options: ['asc', { natural: true, caseSensitive: false, requiredFirst: false }], + errors: ["Expected interface keys to be in natural insensitive ascending order. 'a' should be before 'b'."], + output: 'interface U {a?:T; b:T; b_:T;}', + }, + { + code: 'interface U {C:T; c:T; b_?:T;}', + options: ['asc', { natural: true, caseSensitive: false, requiredFirst: false }], + errors: ["Expected interface keys to be in natural insensitive ascending order. 'b_' should be before 'c'."], + output: 'interface U {b_?:T; c:T; C:T;}', + }, + { + code: 'interface U {C:T; b_?:T; c:T;}', + options: ['asc', { natural: true, caseSensitive: false, requiredFirst: false }], + errors: ["Expected interface keys to be in natural insensitive ascending order. 'b_' should be before 'C'."], + output: 'interface U {b_?:T; C:T; c:T;}', + }, + { + code: 'interface U {$:T; A?:T; _:T; a?:T;}', + options: ['asc', { natural: true, caseSensitive: false, requiredFirst: false }], + errors: ["Expected interface keys to be in natural insensitive ascending order. '_' should be before 'A'."], + output: 'interface U {$:T; _:T; A?:T; a?:T;}', + }, + { + code: "interface U {1:T; '11':T; 2?:T; A:T;}", + options: ['asc', { natural: true, caseSensitive: false, requiredFirst: false }], + errors: ["Expected interface keys to be in natural insensitive ascending order. '2' should be before '11'."], + output: "interface U {1:T; 2?:T; '11':T; A:T;}", + }, + { + code: "interface U {'Z':T; À:T; '#'?:T; è:T;}", + options: ['asc', { natural: true, caseSensitive: false, requiredFirst: false }], + errors: ["Expected interface keys to be in natural insensitive ascending order. '#' should be before 'À'."], + output: "interface U {'#'?:T; À:T; 'Z':T; è:T;}", + }, + // desc { code: 'interface U {a:T; _:T; b:T;} // desc', @@ -520,6 +680,133 @@ ruleTester.run('interface', rule, { output: "interface U {è:T; À:T; 'Z':T; '#':T;}", }, + // desc, natural, insensitive, required + { + code: 'interface U {_:T; a?:T; b:T;} // desc, natural, insensitive, required', + options: ['desc', { natural: true, caseSensitive: false, requiredFirst: true }], + errors: ["Expected interface keys to be in required first natural insensitive descending order. 'b' should be before 'a'."], + output: 'interface U {b:T; a?:T; _:T;} // desc, natural, insensitive, required', + }, + { + code: 'interface U {b:T; a?:T; _:T;}', + options: ['desc', { natural: true, caseSensitive: false, requiredFirst: true }], + errors: ["Expected interface keys to be in required first natural insensitive descending order. '_' should be before 'a'."], + output: 'interface U {b:T; _:T; a?:T;}', + }, + { + code: 'interface U {b:T; b_:T; a?:T;}', + options: ['desc', { natural: true, caseSensitive: false, requiredFirst: true }], + errors: ["Expected interface keys to be in required first natural insensitive descending order. 'b_' should be before 'b'."], + output: 'interface U {b_:T; b:T; a?:T;}', + }, + { + code: 'interface U {c:T; b_?:T; C:T;}', + options: ['desc', { natural: true, caseSensitive: false, requiredFirst: true }], + errors: ["Expected interface keys to be in required first natural insensitive descending order. 'C' should be before 'b_'."], + output: 'interface U {c:T; C:T; b_?:T;}', + }, + { + code: 'interface U {b_?:T; C:T; c:T;}', + options: ['desc', { natural: true, caseSensitive: false, requiredFirst: true }], + errors: ["Expected interface keys to be in required first natural insensitive descending order. 'C' should be before 'b_'."], + output: 'interface U {C:T; b_?:T; c:T;}', + }, + { + code: 'interface U {_:T; a?:T; $:T; A?:T;}', + options: ['desc', { natural: true, caseSensitive: false, requiredFirst: true }], + errors: ["Expected interface keys to be in required first natural insensitive descending order. '$' should be before 'a'."], + output: 'interface U {_:T; $:T; a?:T; A?:T;}', + }, + { + code: "interface U {2?:T; A:T; 1:T; '11':T;}", + options: ['desc', { natural: true, caseSensitive: false, requiredFirst: true }], + errors: [ + "Expected interface keys to be in required first natural insensitive descending order. 'A' should be before '2'.", + "Expected interface keys to be in required first natural insensitive descending order. '11' should be before '1'.", + ], + output: "interface U {A:T; 2?:T; 1:T; '11':T;}", + }, + { + code: "interface U {è:T; 'Z':T; '#'?:T; À?:T;}", + options: ['desc', { natural: true, caseSensitive: false, requiredFirst: true }], + errors: ["Expected interface keys to be in required first natural insensitive descending order. 'À' should be before '#'."], + output: "interface U {è:T; 'Z':T; À?:T; '#'?:T;}", + }, + { + code: "interface U {À?:T; 'Z':T; '#'?:T; è:T;}", + options: ['desc', { natural: true, caseSensitive: false, requiredFirst: true }], + errors: [ + "Expected interface keys to be in required first natural insensitive descending order. 'Z' should be before 'À'.", + "Expected interface keys to be in required first natural insensitive descending order. 'è' should be before '#'.", + ], + output: "interface U {è:T; 'Z':T; '#'?:T; À?:T;}", + }, + + // desc, natural, insensitive, not-required + { + code: 'interface U {_:T; a?:T; b:T;} // desc, natural, insensitive, not-required', + options: ['desc', { natural: true, caseSensitive: false, requiredFirst: false }], + errors: [ + "Expected interface keys to be in natural insensitive descending order. 'a' should be before '_'.", + "Expected interface keys to be in natural insensitive descending order. 'b' should be before 'a'.", + ], + output: 'interface U {b:T; a?:T; _:T;} // desc, natural, insensitive, not-required', + }, + { + code: 'interface U {a?:T; b:T; _:T;}', + options: ['desc', { natural: true, caseSensitive: false, requiredFirst: false }], + errors: ["Expected interface keys to be in natural insensitive descending order. 'b' should be before 'a'."], + output: 'interface U {b:T; a?:T; _:T;}', + }, + { + code: 'interface U {b:T; b_:T; a?:T;}', + options: ['desc', { natural: true, caseSensitive: false, requiredFirst: false }], + errors: ["Expected interface keys to be in natural insensitive descending order. 'b_' should be before 'b'."], + output: 'interface U {b_:T; b:T; a?:T;}', + }, + { + code: 'interface U {c:T; b_?:T; C:T;}', + options: ['desc', { natural: true, caseSensitive: false, requiredFirst: false }], + errors: ["Expected interface keys to be in natural insensitive descending order. 'C' should be before 'b_'."], + output: 'interface U {c:T; C:T; b_?:T;}', + }, + { + code: 'interface U {b_?:T; C:T; c:T;}', + options: ['desc', { natural: true, caseSensitive: false, requiredFirst: false }], + errors: ["Expected interface keys to be in natural insensitive descending order. 'C' should be before 'b_'."], + output: 'interface U {C:T; b_?:T; c:T;}', + }, + { + code: 'interface U {_:T; a?:T; $:T; A?:T;}', + options: ['desc', { natural: true, caseSensitive: false, requiredFirst: false }], + errors: [ + "Expected interface keys to be in natural insensitive descending order. 'a' should be before '_'.", + "Expected interface keys to be in natural insensitive descending order. 'A' should be before '$'.", + ], + output: 'interface U {a?:T; _:T; $:T; A?:T;}', + }, + { + code: "interface U {2?:T; A:T; 1:T; '11':T;}", + options: ['desc', { natural: true, caseSensitive: false, requiredFirst: false }], + errors: [ + "Expected interface keys to be in natural insensitive descending order. 'A' should be before '2'.", + "Expected interface keys to be in natural insensitive descending order. '11' should be before '1'.", + ], + output: "interface U {A:T; 2?:T; 1:T; '11':T;}", + }, + { + code: "interface U {è:T; 'Z':T; '#'?:T; À?:T;}", + options: ['desc', { natural: true, caseSensitive: false, requiredFirst: false }], + errors: ["Expected interface keys to be in natural insensitive descending order. 'À' should be before '#'."], + output: "interface U {è:T; À?:T; '#'?:T; 'Z':T;}", + }, + { + code: "interface U {À?:T; 'Z':T; '#'?:T; è:T;}", + options: ['desc', { natural: true, caseSensitive: false, requiredFirst: false }], + errors: ["Expected interface keys to be in natural insensitive descending order. 'è' should be before '#'."], + output: "interface U {è:T; 'Z':T; '#'?:T; À?:T;}", + }, + // index signatures { code: 'interface U { A: T; [skey: string]: T; _: T; }',