Skip to content

Commit

Permalink
feat: check Messages
Browse files Browse the repository at this point in the history
  • Loading branch information
mshanemc committed Jan 26, 2023
1 parent 5abaaeb commit e84b0ce
Show file tree
Hide file tree
Showing 12 changed files with 1,523 additions and 58 deletions.
17 changes: 11 additions & 6 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,21 @@ jobs:
matrix:
externalProjectGitUrl:
- https://github.com/salesforcecli/plugin-deploy-retrieve
- https://github.com/salesforcecli/plugin-env
- https://github.com/salesforcecli/plugin-settings
- https://github.com/salesforcecli/plugin-login
- https://github.com/salesforcecli/plugin-sobject
- https://github.com/salesforcecli/plugin-community
- https://github.com/salesforcecli/plugin-custom-metadata
- https://github.com/salesforcecli/plugin-data
- https://github.com/salesforcecli/plugin-dev
- https://github.com/salesforcecli/plugin-env
- https://github.com/salesforcecli/plugin-limits
- https://github.com/salesforcecli/plugin-login
- https://github.com/salesforcecli/plugin-packaging
- https://github.com/salesforcecli/plugin-schema
- https://github.com/salesforcecli/plugin-data
- https://github.com/salesforcecli/plugin-community
- https://github.com/salesforcecli/plugin-settings
- https://github.com/salesforcecli/plugin-signups
- https://github.com/salesforcecli/plugin-sobject
- https://github.com/salesforcecli/plugin-info
- https://github.com/salesforcecli/plugin-user

with:
packageName: 'eslint-plugin-sf-plugin'
externalProjectGitUrl: ${{ matrix.externalProjectGitUrl }}
Expand Down
71 changes: 38 additions & 33 deletions README.md

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions docs/rules/encourage-alias-deprecation.md
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 -->
5 changes: 5 additions & 0 deletions docs/rules/no-missing-messages.md
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 -->
7 changes: 7 additions & 0 deletions docs/rules/no-unnecessary-aliases.md
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 -->
7 changes: 7 additions & 0 deletions docs/rules/no-unnecessary-properties.md
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 -->
59 changes: 59 additions & 0 deletions messages/test.md
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.
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,5 +68,8 @@
"commitizen": {
"path": "cz-conventional-changelog"
}
},
"peerDependencies": {
"@salesforce/core": "^3.33.1"
}
}
}
12 changes: 12 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,19 @@ import { noUsernameProperties } from './rules/migration/no-username-properties';
import { noUnnecessaryProperties } from './rules/no-unnecessary-properties';
import { encourageAliasDeprecation } from './rules/migration/encourage-alias-deprecation';
import { noUnnecessaryAliases } from './rules/no-unnecessary-aliases';
import { noMissingMessages } from './rules/no-missing-messages';

const library = {
plugins: ['sf-plugin'],
rules: {
'sf-plugin/no-missing-messages': 'error',
},
};

const recommended = {
plugins: ['sf-plugin'],
rules: {
...library.rules,
'sf-plugin/command-example': 'warn',
'sf-plugin/flag-min-max-default': 'warn',
'sf-plugin/no-hardcoded-messages-flags': 'warn',
Expand All @@ -65,9 +74,11 @@ const recommended = {
'sf-plugin/no-unnecessary-aliases': 'error',
},
};

export = {
configs: {
recommended,
library,
migration: {
plugins: ['sf-plugin'],
rules: {
Expand Down Expand Up @@ -124,5 +135,6 @@ export = {
'no-unnecessary-properties': noUnnecessaryProperties,
'encourage-alias-deprecation': encourageAliasDeprecation,
'no-unnecessary-aliases': noUnnecessaryAliases,
'no-missing-messages': noMissingMessages,
},
};
165 changes: 165 additions & 0 deletions src/rules/no-missing-messages.ts
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);
};
Loading

0 comments on commit e84b0ce

Please sign in to comment.