-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: no-depends-on-boolean-flag
rule
#447
Changes from 14 commits
afde4d1
033815f
5c3f55e
3823959
c0866fd
c92bf43
59dda9a
aeebd0b
496d99a
b71159d
56b1bfd
a917de4
b1a47a3
528dd81
890b822
bbaff14
502e025
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
# Do not allow flags to depend on boolean flags (`sf-plugin/no-depends-on-boolean-flag`) | ||
|
||
⚠️ This rule _warns_ in the following configs: ✈️ `migration`, ✅ `recommended`. | ||
|
||
<!-- end auto-generated rule header --> | ||
|
||
Flags depending on other boolean flags via `dependsOn` can cause unexpected behaviors: | ||
|
||
```ts | ||
// src/commands/data/query.ts | ||
|
||
export class DataSoqlQueryCommand extends SfCommand<unknown> { | ||
public static readonly flags = { | ||
query: Flags.string({ | ||
char: 'q', | ||
summary: messages.getMessage('flags.query.summary'), | ||
}), | ||
bulk: Flags.boolean({ | ||
char: 'b', | ||
default: false, | ||
summary: messages.getMessage('flags.bulk.summary'), | ||
}), | ||
wait: Flags.duration({ | ||
unit: 'minutes', | ||
char: 'w', | ||
summary: messages.getMessage('flags.wait.summary'), | ||
dependsOn: ['bulk'], | ||
}) | ||
} | ||
} | ||
``` | ||
|
||
This code is supposed to only allow `--wait` to be used when `--bulk` was provided. | ||
|
||
However, because `--bulk` has a default value of `false`, oclif's flag parser will allow `--wait` even without passing in `--bulk` because the `wait.dependsOn` check only ensures that `bulk` has a value, so the following execution would be allowed: | ||
|
||
``` | ||
sf data query -q 'select name,id from account limit 1' --wait 10 | ||
``` | ||
|
||
But even if `--bulk` didn't have a default value, it could still allow a wrong combination if it had `allowNo: true`: | ||
|
||
```ts | ||
bulk: Flags.boolean({ | ||
char: 'b', | ||
default: false, | ||
summary: messages.getMessage('flags.bulk.summary'), | ||
allowNo: true // Support reversible boolean flag with `--no-` prefix (e.g. `--no-bulk`). | ||
}), | ||
``` | ||
|
||
The following example would still run because `--no-bulk` sets `bulk` value to `false`: | ||
|
||
``` | ||
sf data query -q 'select name,id from account limit 1' --wait 10 --no-bulk | ||
``` | ||
|
||
If the desired behavior is to only allow a flag when another boolean flag was provided you should use oclif's relationships feature to verify the boolean flag value is `true`: | ||
|
||
```ts | ||
bulk: Flags.boolean({ | ||
char: 'b', | ||
default: false, | ||
summary: messages.getMessage('flags.bulk.summary'), | ||
allowNo: true // Support reversible boolean flag with `--no-` prefix (e.g. `--no-bulk`). | ||
}), | ||
wait: Flags.duration({ | ||
unit: 'minutes', | ||
char: 'w', | ||
summary: messages.getMessage('flags.wait.summary'), | ||
relationships: [ | ||
{ | ||
type: 'some', | ||
// eslint-disable-next-line @typescript-eslint/require-await | ||
flags: [{ name: 'bulk', when: async (flags): Promise<boolean> => Promise.resolve(flags['bulk'] === true) }], | ||
}, | ||
] | ||
}) | ||
``` | ||
|
||
See: https://oclif.io/docs/flags |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -49,6 +49,7 @@ import { esmMessageImport } from './rules/esm-message-import'; | |
import { noDefaultDependsOnFlags } from './rules/no-default-depends-on-flags'; | ||
import { onlyExtendSfCommand } from './rules/only-extend-sfCommand'; | ||
import { spreadBaseFlags } from './rules/spread-base-flags'; | ||
import { noDependsOnBooleanFlags } from './rules/no-depends-on-boolean-flags'; | ||
|
||
const library = { | ||
plugins: ['sf-plugin'], | ||
|
@@ -90,6 +91,7 @@ const recommended = { | |
'sf-plugin/no-default-and-depends-on-flags': 'error', | ||
'sf-plugin/only-extend-SfCommand': 'warn', | ||
'sf-plugin/spread-base-flags': 'warn', | ||
'sf-plugin/no-depends-on-boolean-flag': 'warn', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. warn instead of error because there are cases where depending on a boolean flag works as expected:
there's no way |
||
}, | ||
}; | ||
|
||
|
@@ -124,6 +126,7 @@ export = { | |
'no-h-short-char': dashH, | ||
'no-default-and-depends-on-flags': noDefaultDependsOnFlags, | ||
'only-extend-SfCommand': onlyExtendSfCommand, | ||
'no-depends-on-boolean-flag': noDependsOnBooleanFlags, | ||
'spread-base-flags': spreadBaseFlags, | ||
'no-duplicate-short-characters': noDuplicateShortCharacters, | ||
'run-matches-class-type': runMatchesClassType, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
/* | ||
* Copyright (c) 2024, salesforce.com, inc. | ||
* All rights reserved. | ||
* Licensed under the BSD 3-Clause license. | ||
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause | ||
*/ | ||
import { AST_NODE_TYPES } from '@typescript-eslint/utils'; | ||
import { RuleCreator } from '@typescript-eslint/utils/eslint-utils'; | ||
import { ancestorsContainsSfCommand, isInCommandDirectory } from '../shared/commands'; | ||
import { isFlag } from '../shared/flags'; | ||
|
||
export const noDependsOnBooleanFlags = RuleCreator.withoutDocs({ | ||
meta: { | ||
docs: { | ||
description: 'Do not allow flags to depend on boolean flags', | ||
recommended: 'recommended', | ||
}, | ||
messages: { | ||
message: 'Cannot create a flag that `dependsOn` a boolean flag', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can the message say more, something like, "use the |
||
}, | ||
type: 'problem', | ||
schema: [], | ||
}, | ||
defaultOptions: [], | ||
create(context) { | ||
return isInCommandDirectory(context) | ||
? { | ||
Property(node): void { | ||
if ( | ||
isFlag(node) && | ||
ancestorsContainsSfCommand(context) && | ||
node.value?.type === AST_NODE_TYPES.CallExpression && | ||
node.value.arguments?.[0]?.type === AST_NODE_TYPES.ObjectExpression | ||
) { | ||
const dependsOnFlagsProp = node.value.arguments[0].properties.find( | ||
(p) => p.type === 'Property' && p.key.type === 'Identifier' && p.key.name === 'dependsOn' | ||
); | ||
|
||
if (dependsOnFlagsProp) { | ||
// @ts-ignore we know `dependsOn` exists/is an array | ||
const dependedOnFlags = dependsOnFlagsProp.value.elements.map((l) => l.value); | ||
|
||
for (const flag of dependedOnFlags) { | ||
if (node.parent.type === 'ObjectExpression') { | ||
const possibleBoolFlag = node.parent.properties.find( | ||
(f) => | ||
f.type === AST_NODE_TYPES.Property && | ||
f.key.type == AST_NODE_TYPES.Identifier && | ||
f.key.name === flag && | ||
f.value.type == AST_NODE_TYPES.CallExpression && | ||
f.value.callee.type == AST_NODE_TYPES.MemberExpression && | ||
f.value.callee.property.type == AST_NODE_TYPES.Identifier && | ||
f.value.callee.property.name === 'boolean' | ||
); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
if (possibleBoolFlag) { | ||
context.report({ | ||
node: node, | ||
messageId: 'message', | ||
}); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
}, | ||
} | ||
: {}; | ||
}, | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
/* | ||
* Copyright (c) 2024, salesforce.com, inc. | ||
* All rights reserved. | ||
* Licensed under the BSD 3-Clause license. | ||
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause | ||
*/ | ||
import path from 'path'; | ||
import { RuleTester } from '@typescript-eslint/rule-tester'; | ||
|
||
import { noDependsOnBooleanFlags } from '../../src/rules/no-depends-on-boolean-flags'; | ||
|
||
const ruleTester = new RuleTester({ | ||
parser: '@typescript-eslint/parser', | ||
}); | ||
|
||
ruleTester.run('noDependsOnBooleanFlags', noDependsOnBooleanFlags, { | ||
valid: [ | ||
{ | ||
name: 'dependsOn non-boolean flag', | ||
filename: path.normalize('src/commands/foo.ts'), | ||
code: ` | ||
import {SfCommand} from '@salesforce/sf-plugins-core'; | ||
export default class DataQuery extends SfCommand<void> { | ||
public static readonly flags = { | ||
query: Flags.string({ | ||
char: 'q', | ||
summary: messages.getMessage('flags.query.summary'), | ||
exactlyOne: ['query', 'file'], | ||
}), | ||
test: Flags.string({ | ||
summary: 'test flag', | ||
dependsOn: ['query'] | ||
}), | ||
} | ||
} | ||
` | ||
} | ||
], | ||
invalid: [ | ||
{ | ||
name: 'dependsOn boolean flag', | ||
filename: path.normalize('src/commands/foo.ts'), | ||
errors: [{ messageId: 'message' }], | ||
code: ` | ||
import {SfCommand} from '@salesforce/sf-plugins-core'; | ||
export default class DataQuery extends SfCommand<void> { | ||
public static readonly flags = { | ||
...orgFlags, | ||
bulk: Flags.boolean({ | ||
char: 'b', | ||
default: false, | ||
summary: messages.getMessage('flags.bulk.summary'), | ||
exclusive: ['use-tooling-api'], | ||
}), | ||
wait: Flags.duration({ | ||
unit: 'minutes', | ||
char: 'w', | ||
summary: messages.getMessage('flags.wait.summary'), | ||
dependsOn: ['bulk'], | ||
exclusive: ['async'], | ||
}), | ||
} | ||
} | ||
`, | ||
} | ||
], | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
opt-in way to disable the ts-dep-check, see: salesforcecli/github-workflows#120
this is a devDep so it's ok to ship TS