-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
12 changed files
with
1,523 additions
and
58 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
# Commands and flags aliases probably want to deprecate their old names to provide more warnings to users (`sf-plugin/encourage-alias-deprecation`) | ||
|
||
⚠️ This rule _warns_ in the ✈️ `migration` config. | ||
|
||
🔧💡 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix) and manually fixable by [editor suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions). | ||
|
||
<!-- end auto-generated rule header --> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
# Checks core Messages usage for correct usage of named messages and message tokens (`sf-plugin/no-missing-messages`) | ||
|
||
💼 This rule is enabled in the following configs: `library`, ✈️ `migration`, ✅ `recommended`. | ||
|
||
<!-- end auto-generated rule header --> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
# Mark when an alias is unnecessary because its only an order permutation, not really a different name (`sf-plugin/no-unnecessary-aliases`) | ||
|
||
💼 This rule is enabled in the following configs: ✈️ `migration`, ✅ `recommended`. | ||
|
||
🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix). | ||
|
||
<!-- end auto-generated rule header --> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
# Boolean properties are false by default, so they should not be set to false (`sf-plugin/no-unnecessary-properties`) | ||
|
||
⚠️ This rule _warns_ in the following configs: ✈️ `migration`, ✅ `recommended`. | ||
|
||
🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix). | ||
|
||
<!-- end auto-generated rule header --> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
# basic | ||
|
||
lorem ipsum | ||
|
||
# basic2Placeholders | ||
|
||
lorem %s %s ipsum | ||
|
||
# array | ||
|
||
- foo | ||
|
||
- bar | ||
|
||
# arrayWith3Placeholders | ||
|
||
- foo %s foo | ||
|
||
- bar %s %s bar | ||
|
||
# nested.something | ||
|
||
lorem ipsum | ||
|
||
# nested.somethingWith2Placeholders | ||
|
||
lorem ipsum | ||
|
||
# eName | ||
|
||
lorem ipsum errata | ||
|
||
# eWith3Placeholders | ||
|
||
lorem %s %s %s ipsum | ||
|
||
# eWithActionsHaving4Placeholders | ||
|
||
lorem ipsum | ||
|
||
# eWithActionsHaving4Placeholders.actions | ||
|
||
- try turning it off and on action or %s or %s. | ||
|
||
- go get a new one from %s or %s. | ||
|
||
# iName | ||
|
||
lorem ipsum | ||
|
||
# iWithActions | ||
|
||
lorem ipsum | ||
|
||
# iWithActions.actions | ||
|
||
- it's probably fine. | ||
|
||
- go take a nap or drink a %s. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,165 @@ | ||
/* | ||
* Copyright (c) 2020, 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 | ||
*/ | ||
/* eslint-disable complexity */ | ||
|
||
import { ASTUtils, AST_NODE_TYPES, ESLintUtils, TSESTree } from '@typescript-eslint/utils'; | ||
import { Messages, SfError, StructuredMessage } from '@salesforce/core'; | ||
|
||
const methods = ['createError', 'createWarning', 'createInfo', 'getMessage', 'getMessages'] as const; | ||
|
||
export const noMissingMessages = ESLintUtils.RuleCreator.withoutDocs({ | ||
meta: { | ||
docs: { | ||
description: 'Checks core Messages usage for correct usage of named messages and message tokens', | ||
recommended: 'error', | ||
}, | ||
messages: { | ||
missing: 'the message "{{messageKey}}" does not exist in the messages file {{fileKey}}', | ||
placeholders: | ||
'the message "{{messageKey}}" in the messages file {{fileKey}} expects {{placeholderCount}} token(s) but received {{argumentCount}}', | ||
actionPlaceholders: | ||
'the actions for message "{{messageKey}}" in the messages file {{fileKey}} expects {{placeholderCount}} tokens(s) but received {{argumentCount}}', | ||
}, | ||
type: 'problem', | ||
schema: [], | ||
}, | ||
defaultOptions: [], | ||
create(context) { | ||
Messages.importMessagesDirectory(process.cwd()); | ||
|
||
const loadedMessages = new Map<string, Messages<string>>(); | ||
const loadedMessageBundles = new Map<string, string>(); | ||
return { | ||
// load any messages, by const name, that are loaded in the file | ||
VariableDeclarator(node): void { | ||
if ( | ||
node.init && | ||
node.id.type === AST_NODE_TYPES.Identifier && | ||
node.init.type === AST_NODE_TYPES.CallExpression && | ||
node.init.callee.type === AST_NODE_TYPES.MemberExpression && | ||
node.init.callee.object.type === AST_NODE_TYPES.Identifier && | ||
node.init.callee.object.name === 'Messages' && | ||
node.init.callee.property.type === AST_NODE_TYPES.Identifier && | ||
node.init.callee.property.name.startsWith('load') && | ||
node.init.arguments[0].type === AST_NODE_TYPES.Literal && | ||
typeof node.init.arguments[0].value === 'string' && | ||
node.init.arguments[1].type === AST_NODE_TYPES.Literal && | ||
typeof node.init.arguments[1].value === 'string' | ||
) { | ||
loadedMessages.set( | ||
node.id.name, | ||
Messages.loadMessages(node.init.arguments[0].value, node.init.arguments[1].value) | ||
); | ||
loadedMessageBundles.set(node.id.name, node.init.arguments[1].value); | ||
} | ||
}, | ||
CallExpression(node): void { | ||
if ( | ||
// we don't both if we never loaded any messages | ||
loadedMessages.size && | ||
node.callee.type === AST_NODE_TYPES.MemberExpression && | ||
node.callee.object.type === AST_NODE_TYPES.Identifier && | ||
loadedMessages.has(node.callee.object.name) && | ||
node.callee.property.type === AST_NODE_TYPES.Identifier && | ||
// the key needs to be a string so we can look it up | ||
node.arguments[0].type === AST_NODE_TYPES.Literal && | ||
typeof node.arguments[0].value === 'string' && | ||
isMessagesMethod(node.callee.property.name) | ||
) { | ||
const bundleConstant = node.callee.object.name; | ||
const messageKey = node.arguments[0].value; | ||
const fileKey = loadedMessageBundles.get(bundleConstant); | ||
const messageTokensCount = getTokensCount(node.arguments[1]); | ||
const actionTokensCount = getTokensCount(node.arguments[2]); | ||
let result: StructuredMessage | SfError | string | string[]; | ||
try { | ||
// execute some method on Messages so we can inspect the result | ||
// we are intentionally passing it no tokens so that we can see residual %s etc in the text | ||
result = loadedMessages.get(bundleConstant)[node.callee.property.name](messageKey); | ||
} catch (e) { | ||
// we never found the message at all, we can report and exit | ||
return context.report({ | ||
node: node.arguments[0], | ||
messageId: 'missing', | ||
data: { | ||
messageKey, | ||
fileKey, | ||
}, | ||
}); | ||
} | ||
const resolvedMessage = getMessage(result); | ||
const messagePlaceholderCount = getPlaceholderCount(resolvedMessage); | ||
if (messagePlaceholderCount !== messageTokensCount) { | ||
context.report({ | ||
// if there's not a second argument, we can report on the first | ||
node: node.arguments[1] ?? node.arguments[0], | ||
messageId: 'placeholders', | ||
data: { | ||
placeholderCount: messagePlaceholderCount, | ||
argumentCount: messageTokensCount, | ||
fileKey, | ||
messageKey, | ||
}, | ||
}); | ||
} | ||
// it's an SfError or a StructuredMessage, check the actions | ||
if (typeof result !== 'string' && !Array.isArray(result)) { | ||
const actionPlaceholderCount = getPlaceholderCount(result.actions ?? []); | ||
if (actionPlaceholderCount !== actionTokensCount) { | ||
context.report({ | ||
node: node.arguments[2] ?? node.arguments[0], | ||
messageId: 'actionPlaceholders', | ||
data: { | ||
placeholderCount: actionPlaceholderCount, | ||
argumentCount: actionTokensCount, | ||
fileKey, | ||
messageKey, | ||
}, | ||
}); | ||
} | ||
} | ||
} | ||
}, | ||
}; | ||
}, | ||
}); | ||
|
||
// util.format placeholders https://nodejs.org/api/util.html#utilformatformat-args | ||
const placeHolderersRegex = new RegExp(/(%s)|(%d)|(%i)|(%f)|(%j)|(%o)|(%O)|(%c)/g); | ||
|
||
const isMessagesMethod = (method: string): method is (typeof methods)[number] => | ||
methods.includes(method as (typeof methods)[number]); | ||
|
||
const getTokensCount = (node?: TSESTree.Node): number => { | ||
if (!node) { | ||
return 0; | ||
} | ||
if (ASTUtils.isNodeOfType(AST_NODE_TYPES.ArrayExpression)(node)) { | ||
return node.elements.length ?? 0; | ||
} | ||
return 0; | ||
}; | ||
|
||
const getMessage = (result: string | string[] | SfError | StructuredMessage): string | string[] => { | ||
if (typeof result === 'string') { | ||
return result; | ||
} | ||
if (Array.isArray(result)) { | ||
return result; | ||
} | ||
if ('message' in result) { | ||
return result.message; | ||
} | ||
}; | ||
|
||
const getPlaceholderCount = (message: string | string[]): number => { | ||
if (typeof message === 'string') { | ||
return (message.match(placeHolderersRegex) || []).length; | ||
} | ||
|
||
return message.reduce((count, m) => count + (m.match(placeHolderersRegex) || []).length, 0); | ||
}; |
Oops, something went wrong.