eslint-plugin-formatjs
Advanced tools
Comparing version 2.1.5 to 2.2.0
@@ -6,2 +6,18 @@ # Change Log | ||
# [2.2.0](https://github.com/formatjs/formatjs/compare/eslint-plugin-formatjs@2.1.5...eslint-plugin-formatjs@2.2.0) (2020-04-24) | ||
### Bug Fixes | ||
* **eslint-plugin-formatjs:** fix issues with spread element in ([68acfaf](https://github.com/formatjs/formatjs/commit/68acfafdf86907db26a59b447f31bed7d4724466)) | ||
### Features | ||
* **eslint-plugin-formatjs:** Add `literal` option to `enforce-description` & `enforce-default-message` ([f20a2d4](https://github.com/formatjs/formatjs/commit/f20a2d4546d2c8cbffec5fa05792649488bbfda1)) | ||
## [2.1.5](https://github.com/formatjs/formatjs/compare/eslint-plugin-formatjs@2.1.4...eslint-plugin-formatjs@2.1.5) (2020-04-20) | ||
@@ -8,0 +24,0 @@ |
@@ -6,8 +6,17 @@ "use strict"; | ||
const msgs = util_1.extractMessages(node, importedMacroVars); | ||
const { options: [type], } = context; | ||
for (const [{ message: { defaultMessage }, messageNode, },] of msgs) { | ||
if (!defaultMessage && !messageNode) { | ||
context.report({ | ||
node, | ||
message: '`defaultMessage` has to be specified in message descriptor', | ||
}); | ||
if (!defaultMessage) { | ||
if (type === 'literal' && messageNode) { | ||
context.report({ | ||
node, | ||
message: '`defaultMessage` must be a string literal (not function call or variable)', | ||
}); | ||
} | ||
else if (!messageNode) { | ||
context.report({ | ||
node, | ||
message: '`defaultMessage` has to be specified in message descriptor', | ||
}); | ||
} | ||
} | ||
@@ -26,2 +35,7 @@ } | ||
fixable: 'code', | ||
schema: [ | ||
{ | ||
enum: ['literal', 'anything'], | ||
}, | ||
], | ||
}, | ||
@@ -28,0 +42,0 @@ create(context) { |
import { Rule } from 'eslint'; | ||
declare const rule: Rule.RuleModule; | ||
export default rule; | ||
declare const _default: Rule.RuleModule; | ||
export default _default; | ||
//# sourceMappingURL=enforce-description.d.ts.map |
@@ -6,12 +6,21 @@ "use strict"; | ||
const msgs = util_1.extractMessages(node, importedMacroVars); | ||
const { options: [type], } = context; | ||
for (const [{ message: { description }, descriptionNode, },] of msgs) { | ||
if (!description && !descriptionNode) { | ||
context.report({ | ||
node, | ||
message: '`description` has to be specified in message descriptor', | ||
}); | ||
if (!description) { | ||
if (type === 'literal' && descriptionNode) { | ||
context.report({ | ||
node, | ||
message: '`description` has to be a string literal (not function call or variable)', | ||
}); | ||
} | ||
else if (!descriptionNode) { | ||
context.report({ | ||
node, | ||
message: '`description` has to be specified in message descriptor', | ||
}); | ||
} | ||
} | ||
} | ||
} | ||
const rule = { | ||
exports.default = { | ||
meta: { | ||
@@ -26,2 +35,7 @@ type: 'problem', | ||
fixable: 'code', | ||
schema: [ | ||
{ | ||
enum: ['literal', 'anything'], | ||
}, | ||
], | ||
}, | ||
@@ -42,3 +56,2 @@ create(context) { | ||
}; | ||
exports.default = rule; | ||
//# sourceMappingURL=enforce-description.js.map |
@@ -18,3 +18,9 @@ "use strict"; | ||
} | ||
if (values.properties.find(prop => prop.type === 'SpreadElement')) { | ||
return true; // True bc there's a spread element | ||
} | ||
return !!values.properties.find(prop => { | ||
if (prop.type !== 'Property') { | ||
return false; | ||
} | ||
switch (prop.key.type) { | ||
@@ -21,0 +27,0 @@ case 'Identifier': |
@@ -1,4 +0,3 @@ | ||
import { Expression, Property } from 'estree'; | ||
import { TSESTree } from '@typescript-eslint/typescript-estree'; | ||
import { Scope } from 'eslint'; | ||
import { Node } from 'estree-jsx'; | ||
export interface MessageDescriptor { | ||
@@ -11,6 +10,6 @@ id?: string; | ||
message: MessageDescriptor; | ||
messageNode?: Property['value']; | ||
descriptionNode?: Property['value']; | ||
messageNode?: TSESTree.Property['value'] | TSESTree.JSXAttribute['value']; | ||
descriptionNode?: TSESTree.Property['value'] | TSESTree.JSXAttribute['value']; | ||
} | ||
export declare function extractMessages(node: Node, importedMacroVars: Scope.Variable[], excludeMessageDeclCalls?: boolean): Array<[MessageDescriptorNodeInfo, Expression | undefined]>; | ||
export declare function extractMessages(node: TSESTree.Node, importedMacroVars: Scope.Variable[], excludeMessageDeclCalls?: boolean): Array<[MessageDescriptorNodeInfo, TSESTree.Expression | undefined]>; | ||
//# sourceMappingURL=util.d.ts.map |
124
dist/util.js
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const typescript_estree_1 = require("@typescript-eslint/typescript-estree"); | ||
function isStringLiteral(node) { | ||
return node.type === typescript_estree_1.AST_NODE_TYPES.Literal && typeof node.value === 'string'; | ||
} | ||
function findReferenceImport(id, importedVars) { | ||
@@ -7,12 +11,15 @@ return importedVars.find(v => !!v.references.find(ref => ref.identifier === id)); | ||
function isIntlFormatMessageCall(node) { | ||
return (node.type === 'CallExpression' && | ||
node.callee.type === 'MemberExpression' && | ||
node.callee.object.type === 'Identifier' && | ||
return (node.type === typescript_estree_1.AST_NODE_TYPES.CallExpression && | ||
node.callee.type === typescript_estree_1.AST_NODE_TYPES.MemberExpression && | ||
node.callee.object.type === typescript_estree_1.AST_NODE_TYPES.Identifier && | ||
node.callee.object.name === 'intl' && | ||
node.callee.property.type === 'Identifier' && | ||
node.callee.property.type === typescript_estree_1.AST_NODE_TYPES.Identifier && | ||
node.callee.property.name === 'formatMessage' && | ||
node.arguments.length >= 1 && | ||
node.arguments[0].type === 'ObjectExpression'); | ||
node.arguments[0].type === typescript_estree_1.AST_NODE_TYPES.ObjectExpression); | ||
} | ||
function isSingleMessageDescriptorDeclaration(id, importedVars) { | ||
if (id.type !== typescript_estree_1.AST_NODE_TYPES.Identifier) { | ||
return false; | ||
} | ||
const importedVar = findReferenceImport(id, importedVars); | ||
@@ -22,5 +29,8 @@ if (!importedVar) { | ||
} | ||
return importedVar.name === '_'; | ||
return importedVar.name === '_' || importedVar.name === 'defineMessage'; | ||
} | ||
function isMultipleMessageDescriptorDeclaration(id, importedVars) { | ||
if (id.type !== typescript_estree_1.AST_NODE_TYPES.Identifier) { | ||
return false; | ||
} | ||
const importedVar = findReferenceImport(id, importedVars); | ||
@@ -33,3 +43,3 @@ if (!importedVar) { | ||
function extractMessageDescriptor(node) { | ||
if (!node || !node.properties) { | ||
if (!node || node.type !== typescript_estree_1.AST_NODE_TYPES.ObjectExpression) { | ||
return; | ||
@@ -42,35 +52,39 @@ } | ||
}; | ||
result.message = node.properties.reduce((msg, prop) => { | ||
if (prop.key.type !== 'Identifier') { | ||
return msg; | ||
for (const prop of node.properties) { | ||
if (prop.type !== typescript_estree_1.AST_NODE_TYPES.Property || | ||
prop.key.type !== typescript_estree_1.AST_NODE_TYPES.Identifier) { | ||
continue; | ||
} | ||
const value = prop.value.type === 'Literal' && typeof prop.value.value === 'string' | ||
? prop.value.value | ||
: undefined; | ||
const value = isStringLiteral(prop.value) ? prop.value.value : undefined; | ||
switch (prop.key.name) { | ||
case 'defaultMessage': | ||
result.messageNode = prop.value; | ||
msg.defaultMessage = value; | ||
result.message.defaultMessage = value; | ||
break; | ||
case 'description': | ||
result.descriptionNode = prop.value; | ||
msg.description = value; | ||
result.message.description = value; | ||
break; | ||
case 'id': | ||
msg.id = value; | ||
result.message.id = value; | ||
break; | ||
} | ||
return msg; | ||
}, {}); | ||
} | ||
return result; | ||
} | ||
function extractMessageDescriptorFromJSXElement(node) { | ||
var _a, _b, _c, _d; | ||
if (!node || !node.attributes) { | ||
return; | ||
} | ||
let values = undefined; | ||
let messageNode; | ||
let descriptionNode; | ||
const message = node.attributes.reduce((msg, prop) => { | ||
if (prop.type !== 'JSXAttribute' || prop.name.type !== 'JSXIdentifier') { | ||
return msg; | ||
let values; | ||
const result = { | ||
message: {}, | ||
messageNode: undefined, | ||
descriptionNode: undefined, | ||
}; | ||
for (const prop of node.attributes) { | ||
if (prop.type !== typescript_estree_1.AST_NODE_TYPES.JSXAttribute || | ||
prop.name.type !== typescript_estree_1.AST_NODE_TYPES.JSXIdentifier) { | ||
continue; | ||
} | ||
@@ -80,39 +94,58 @@ const key = prop.name; | ||
case 'defaultMessage': | ||
messageNode = prop.value; | ||
msg.defaultMessage = prop.value.value; | ||
result.messageNode = prop.value; | ||
if (((_a = prop.value) === null || _a === void 0 ? void 0 : _a.type) === typescript_estree_1.AST_NODE_TYPES.Literal && | ||
typeof prop.value.value === 'string') { | ||
result.message.defaultMessage = prop.value.value; | ||
} | ||
break; | ||
case 'description': | ||
descriptionNode = prop.value; | ||
msg.description = prop.value.value; | ||
result.descriptionNode = prop.value; | ||
if (((_b = prop.value) === null || _b === void 0 ? void 0 : _b.type) === typescript_estree_1.AST_NODE_TYPES.Literal && | ||
typeof prop.value.value === 'string') { | ||
result.message.description = prop.value.value; | ||
} | ||
break; | ||
case 'id': | ||
msg.id = prop.value.value; | ||
if (((_c = prop.value) === null || _c === void 0 ? void 0 : _c.type) === typescript_estree_1.AST_NODE_TYPES.Literal && | ||
typeof prop.value.value === 'string') { | ||
result.message.id = prop.value.value; | ||
} | ||
break; | ||
case 'values': | ||
values = prop.value | ||
.expression; | ||
if (((_d = prop.value) === null || _d === void 0 ? void 0 : _d.type) === typescript_estree_1.AST_NODE_TYPES.JSXExpressionContainer && | ||
prop.value.expression.type === typescript_estree_1.AST_NODE_TYPES.ObjectExpression) { | ||
values = prop.value.expression; | ||
} | ||
break; | ||
} | ||
return msg; | ||
}, {}); | ||
if (!Object.keys(message).length) { | ||
} | ||
if (!Object.keys(result.message).length) { | ||
return; | ||
} | ||
return [{ messageNode, descriptionNode, message }, values]; | ||
return [result, values]; | ||
} | ||
function extractMessageDescriptors(node) { | ||
if (!node || !node.properties) { | ||
if (!node || | ||
node.type !== typescript_estree_1.AST_NODE_TYPES.ObjectExpression || | ||
!node.properties.length) { | ||
return []; | ||
} | ||
return node.properties.reduce((msgs, prop) => { | ||
const msgs = []; | ||
for (const prop of node.properties) { | ||
if (prop.type !== typescript_estree_1.AST_NODE_TYPES.Property) { | ||
continue; | ||
} | ||
const msg = prop.value; | ||
if (msg.type !== typescript_estree_1.AST_NODE_TYPES.ObjectExpression) { | ||
continue; | ||
} | ||
const nodeInfo = extractMessageDescriptor(msg); | ||
if (nodeInfo) { | ||
return [...msgs, nodeInfo]; | ||
msgs.push(nodeInfo); | ||
} | ||
return msgs; | ||
}, []); | ||
} | ||
return msgs; | ||
} | ||
function extractMessages(node, importedMacroVars, excludeMessageDeclCalls = false) { | ||
if (node.type === 'CallExpression') { | ||
if (node.type === typescript_estree_1.AST_NODE_TYPES.CallExpression) { | ||
const expr = node; | ||
@@ -130,8 +163,11 @@ const fnId = expr.callee; | ||
isMultipleMessageDescriptorDeclaration(fnId, importedMacroVars)) { | ||
return extractMessageDescriptors(expr.arguments[0]).map(msg => [msg, undefined]); | ||
return extractMessageDescriptors(expr.arguments[0]).map(msg => [ | ||
msg, | ||
undefined, | ||
]); | ||
} | ||
} | ||
else if (node.type === 'JSXOpeningElement' && | ||
else if (node.type === typescript_estree_1.AST_NODE_TYPES.JSXOpeningElement && | ||
node.name && | ||
node.name.type === 'JSXIdentifier' && | ||
node.name.type === typescript_estree_1.AST_NODE_TYPES.JSXIdentifier && | ||
node.name.name === 'FormattedMessage') { | ||
@@ -138,0 +174,0 @@ const msgDescriptorNodeInfo = extractMessageDescriptorFromJSXElement(node); |
{ | ||
"name": "eslint-plugin-formatjs", | ||
"version": "2.1.5", | ||
"version": "2.2.0", | ||
"description": "ESLint plugin for formatjs", | ||
@@ -38,3 +38,3 @@ "main": "dist/index.js", | ||
}, | ||
"gitHead": "39af19d8de4beb06e467ba7328f9f01b046248d3" | ||
"gitHead": "bb7dbf865d4bfd862a2c8f1fc7752c95e56335f4" | ||
} |
@@ -126,2 +126,15 @@ # eslint-plugin-formatjs | ||
#### Options | ||
```json | ||
{ | ||
"plugins": ["formatjs"], | ||
"rules": { | ||
"formatjs/enforce-description": ["error", "literal"] | ||
} | ||
} | ||
``` | ||
Setting `literal` forces `description` to always be a string literal instead of function calls or variables. This is helpful for extraction tools that expects `description` to always be a literal | ||
### `enforce-default-message` | ||
@@ -133,3 +146,3 @@ | ||
- Can be usefull in case we want to extract messages for translations from source code. This way can make sure people won't forget about defaultMessage | ||
- Can be useful in case we want to extract messages for translations from source code. This way can make sure people won't forget about defaultMessage | ||
@@ -152,2 +165,15 @@ ```tsx | ||
#### Options | ||
```json | ||
{ | ||
"plugins": ["formatjs"], | ||
"rules": { | ||
"formatjs/enforce-default-message": ["error", "literal"] | ||
} | ||
} | ||
``` | ||
Setting `literal` forces `defaultMessage` to always be a string literal instead of function calls or variables. This is helpful for extraction tools that expects `defaultMessage` to always be a literal | ||
### `enforce-placeholders` | ||
@@ -154,0 +180,0 @@ |
@@ -16,2 +16,3 @@ import {Rule, Scope} from 'eslint'; | ||
} from 'intl-messageformat-parser'; | ||
import {TSESTree} from '@typescript-eslint/typescript-estree'; | ||
@@ -86,3 +87,3 @@ class BlacklistElement extends Error { | ||
) { | ||
const msgs = extractMessages(node, importedMacroVars); | ||
const msgs = extractMessages(node as TSESTree.Node, importedMacroVars); | ||
if (!msgs.length) { | ||
@@ -109,3 +110,3 @@ return; | ||
context.report({ | ||
node: messageNode, | ||
node: messageNode as Node, | ||
message: e.message, | ||
@@ -112,0 +113,0 @@ }); |
import {Rule, Scope} from 'eslint'; | ||
import {ImportDeclaration, Node} from 'estree'; | ||
import {extractMessages} from '../util'; | ||
import {TSESTree} from '@typescript-eslint/typescript-estree'; | ||
@@ -10,3 +11,6 @@ function checkNode( | ||
) { | ||
const msgs = extractMessages(node, importedMacroVars); | ||
const msgs = extractMessages(node as TSESTree.Node, importedMacroVars); | ||
const { | ||
options: [type], | ||
} = context; | ||
for (const [ | ||
@@ -18,7 +22,15 @@ { | ||
] of msgs) { | ||
if (!defaultMessage && !messageNode) { | ||
context.report({ | ||
node, | ||
message: '`defaultMessage` has to be specified in message descriptor', | ||
}); | ||
if (!defaultMessage) { | ||
if (type === 'literal' && messageNode) { | ||
context.report({ | ||
node, | ||
message: | ||
'`defaultMessage` must be a string literal (not function call or variable)', | ||
}); | ||
} else if (!messageNode) { | ||
context.report({ | ||
node, | ||
message: '`defaultMessage` has to be specified in message descriptor', | ||
}); | ||
} | ||
} | ||
@@ -39,2 +51,7 @@ } | ||
fixable: 'code', | ||
schema: [ | ||
{ | ||
enum: ['literal', 'anything'], | ||
}, | ||
], | ||
}, | ||
@@ -41,0 +58,0 @@ create(context) { |
import {Rule, Scope} from 'eslint'; | ||
import {ImportDeclaration, Node} from 'estree'; | ||
import {extractMessages} from '../util'; | ||
import {TSESTree} from '@typescript-eslint/typescript-estree'; | ||
@@ -10,3 +11,6 @@ function checkNode( | ||
) { | ||
const msgs = extractMessages(node, importedMacroVars); | ||
const msgs = extractMessages(node as TSESTree.Node, importedMacroVars); | ||
const { | ||
options: [type], | ||
} = context; | ||
for (const [ | ||
@@ -18,7 +22,15 @@ { | ||
] of msgs) { | ||
if (!description && !descriptionNode) { | ||
context.report({ | ||
node, | ||
message: '`description` has to be specified in message descriptor', | ||
}); | ||
if (!description) { | ||
if (type === 'literal' && descriptionNode) { | ||
context.report({ | ||
node, | ||
message: | ||
'`description` has to be a string literal (not function call or variable)', | ||
}); | ||
} else if (!descriptionNode) { | ||
context.report({ | ||
node, | ||
message: '`description` has to be specified in message descriptor', | ||
}); | ||
} | ||
} | ||
@@ -28,3 +40,3 @@ } | ||
const rule: Rule.RuleModule = { | ||
export default { | ||
meta: { | ||
@@ -40,2 +52,7 @@ type: 'problem', | ||
fixable: 'code', | ||
schema: [ | ||
{ | ||
enum: ['literal', 'anything'], | ||
}, | ||
], | ||
}, | ||
@@ -56,4 +73,2 @@ create(context) { | ||
}, | ||
}; | ||
export default rule; | ||
} as Rule.RuleModule; |
import {Rule, Scope} from 'eslint'; | ||
import {ImportDeclaration, Node, Expression} from 'estree'; | ||
import {TSESTree} from '@typescript-eslint/typescript-estree'; | ||
import {extractMessages} from '../util'; | ||
@@ -12,2 +12,3 @@ import { | ||
} from 'intl-messageformat-parser'; | ||
import {ImportDeclaration, Node} from 'estree'; | ||
@@ -22,3 +23,6 @@ class PlaceholderEnforcement extends Error { | ||
function keyExistsInExpression(key: string, values: Expression | undefined) { | ||
function keyExistsInExpression( | ||
key: string, | ||
values: TSESTree.Expression | undefined | ||
) { | ||
if (!values) { | ||
@@ -30,3 +34,9 @@ return false; | ||
} | ||
if (values.properties.find(prop => prop.type === 'SpreadElement')) { | ||
return true; // True bc there's a spread element | ||
} | ||
return !!values.properties.find(prop => { | ||
if (prop.type !== 'Property') { | ||
return false; | ||
} | ||
switch (prop.key.type) { | ||
@@ -44,3 +54,3 @@ case 'Identifier': | ||
ast: MessageFormatElement[], | ||
values: Expression | undefined | ||
values: TSESTree.Expression | undefined | ||
) { | ||
@@ -67,3 +77,3 @@ for (const el of ast) { | ||
context: Rule.RuleContext, | ||
node: Node, | ||
node: TSESTree.Node, | ||
importedMacroVars: Scope.Variable[] | ||
@@ -87,3 +97,3 @@ ) { | ||
context.report({ | ||
node: messageNode, | ||
node: messageNode as Node, | ||
message: e.message, | ||
@@ -118,4 +128,5 @@ }); | ||
JSXOpeningElement: (node: Node) => | ||
checkNode(context, node, importedMacroVars), | ||
CallExpression: node => checkNode(context, node, importedMacroVars), | ||
checkNode(context, node as TSESTree.Node, importedMacroVars), | ||
CallExpression: node => | ||
checkNode(context, node as TSESTree.Node, importedMacroVars), | ||
}; | ||
@@ -122,0 +133,0 @@ }, |
import {Rule, Scope} from 'eslint'; | ||
import {ImportDeclaration, Node} from 'estree'; | ||
import {TSESTree} from '@typescript-eslint/typescript-estree'; | ||
import {extractMessages} from '../util'; | ||
@@ -55,3 +56,3 @@ import { | ||
context: Rule.RuleContext, | ||
node: Node, | ||
node: TSESTree.Node, | ||
importedMacroVars: Scope.Variable[] | ||
@@ -81,3 +82,3 @@ ) { | ||
context.report({ | ||
node: messageNode, | ||
node: messageNode as Node, | ||
message: e.message, | ||
@@ -124,4 +125,5 @@ }); | ||
JSXOpeningElement: (node: Node) => | ||
checkNode(context, node, importedMacroVars), | ||
CallExpression: node => checkNode(context, node, importedMacroVars), | ||
checkNode(context, node as TSESTree.Node, importedMacroVars), | ||
CallExpression: node => | ||
checkNode(context, node as TSESTree.Node, importedMacroVars), | ||
}; | ||
@@ -128,0 +130,0 @@ }, |
import {Rule, Scope} from 'eslint'; | ||
import {ImportDeclaration, Node} from 'estree'; | ||
import {TSESTree} from '@typescript-eslint/typescript-estree'; | ||
import { | ||
@@ -39,3 +40,3 @@ parse, | ||
context: Rule.RuleContext, | ||
node: Node, | ||
node: TSESTree.Node, | ||
importedMacroVars: Scope.Variable[] | ||
@@ -58,3 +59,3 @@ ) { | ||
context.report({ | ||
node: messageNode, | ||
node: messageNode as Node, | ||
message: e.message, | ||
@@ -88,4 +89,5 @@ }); | ||
JSXOpeningElement: (node: Node) => | ||
checkNode(context, node, importedMacroVars), | ||
CallExpression: node => checkNode(context, node, importedMacroVars), | ||
checkNode(context, node as TSESTree.Node, importedMacroVars), | ||
CallExpression: node => | ||
checkNode(context, node as TSESTree.Node, importedMacroVars), | ||
}; | ||
@@ -92,0 +94,0 @@ }, |
import {Rule, Scope} from 'eslint'; | ||
import {ImportDeclaration, Node} from 'estree'; | ||
import {TSESTree} from '@typescript-eslint/typescript-estree'; | ||
import {extractMessages} from '../util'; | ||
@@ -9,3 +10,3 @@ import * as emojiRegex from 'emoji-regex'; | ||
context: Rule.RuleContext, | ||
node: Node, | ||
node: TSESTree.Node, | ||
importedMacroVars: Scope.Variable[] | ||
@@ -26,3 +27,3 @@ ) { | ||
context.report({ | ||
node: messageNode, | ||
node: messageNode as Node, | ||
message: 'Emojis are not allowed', | ||
@@ -56,4 +57,5 @@ }); | ||
JSXOpeningElement: (node: Node) => | ||
checkNode(context, node, importedMacroVars), | ||
CallExpression: node => checkNode(context, node, importedMacroVars), | ||
checkNode(context, node as TSESTree.Node, importedMacroVars), | ||
CallExpression: node => | ||
checkNode(context, node as TSESTree.Node, importedMacroVars), | ||
}; | ||
@@ -60,0 +62,0 @@ }, |
import {Rule, Scope} from 'eslint'; | ||
import {ImportDeclaration, Node} from 'estree'; | ||
import {TSESTree} from '@typescript-eslint/typescript-estree'; | ||
import {extractMessages} from '../util'; | ||
@@ -31,3 +32,3 @@ import { | ||
context: Rule.RuleContext, | ||
node: Node, | ||
node: TSESTree.Node, | ||
importedMacroVars: Scope.Variable[] | ||
@@ -50,3 +51,3 @@ ) { | ||
context.report({ | ||
node: messageNode, | ||
node: messageNode as Node, | ||
message: e.message, | ||
@@ -80,4 +81,5 @@ }); | ||
JSXOpeningElement: (node: Node) => | ||
checkNode(context, node, importedMacroVars), | ||
CallExpression: node => checkNode(context, node, importedMacroVars), | ||
checkNode(context, node as TSESTree.Node, importedMacroVars), | ||
CallExpression: node => | ||
checkNode(context, node as TSESTree.Node, importedMacroVars), | ||
}; | ||
@@ -84,0 +86,0 @@ }, |
import {Rule, Scope} from 'eslint'; | ||
import {ImportDeclaration, Node} from 'estree'; | ||
import {extractMessages} from '../util'; | ||
import {TSESTree} from '@typescript-eslint/typescript-estree'; | ||
const MULTIPLE_SPACES = /\s{2,}/g; | ||
@@ -8,3 +9,3 @@ | ||
context: Rule.RuleContext, | ||
node: Node, | ||
node: TSESTree.Node, | ||
importedMacroVars: Scope.Variable[] | ||
@@ -26,3 +27,3 @@ ) { | ||
reportObject = { | ||
node: messageNode, | ||
node: messageNode as Node, | ||
message: 'Multiple consecutive whitespaces are not allowed', | ||
@@ -33,3 +34,3 @@ }; | ||
return fixer.replaceText( | ||
messageNode, | ||
messageNode as Node, | ||
messageNode.raw!.replace(MULTIPLE_SPACES, ' ') | ||
@@ -68,4 +69,5 @@ ); | ||
JSXOpeningElement: (node: Node) => | ||
checkNode(context, node, importedMacroVars), | ||
CallExpression: node => checkNode(context, node, importedMacroVars), | ||
checkNode(context, node as TSESTree.Node, importedMacroVars), | ||
CallExpression: node => | ||
checkNode(context, node as TSESTree.Node, importedMacroVars), | ||
}; | ||
@@ -72,0 +74,0 @@ }, |
import {Rule, Scope} from 'eslint'; | ||
import {TSESTree} from '@typescript-eslint/typescript-estree'; | ||
import {extractMessages} from '../util'; | ||
@@ -30,3 +31,3 @@ import { | ||
context: Rule.RuleContext, | ||
node: Node, | ||
node: TSESTree.Node, | ||
importedMacroVars: Scope.Variable[] | ||
@@ -49,3 +50,3 @@ ) { | ||
context.report({ | ||
node: messageNode, | ||
node: messageNode as Node, | ||
message: e.message, | ||
@@ -79,4 +80,5 @@ }); | ||
JSXOpeningElement: (node: Node) => | ||
checkNode(context, node, importedMacroVars), | ||
CallExpression: node => checkNode(context, node, importedMacroVars), | ||
checkNode(context, node as TSESTree.Node, importedMacroVars), | ||
CallExpression: node => | ||
checkNode(context, node as TSESTree.Node, importedMacroVars), | ||
}; | ||
@@ -83,0 +85,0 @@ }, |
@@ -13,2 +13,3 @@ import {Rule, Scope} from 'eslint'; | ||
} from 'intl-messageformat-parser'; | ||
import {TSESTree} from '@typescript-eslint/typescript-estree'; | ||
import {ImportDeclaration, Node} from 'estree'; | ||
@@ -35,3 +36,3 @@ | ||
context: Rule.RuleContext, | ||
node: Node, | ||
node: TSESTree.Node, | ||
importedMacroVars: Scope.Variable[] | ||
@@ -54,3 +55,3 @@ ) { | ||
context.report({ | ||
node: messageNode, | ||
node: messageNode as Node, | ||
message: e.message, | ||
@@ -84,4 +85,5 @@ }); | ||
JSXOpeningElement: (node: Node) => | ||
checkNode(context, node, importedMacroVars), | ||
CallExpression: node => checkNode(context, node, importedMacroVars), | ||
checkNode(context, node as TSESTree.Node, importedMacroVars), | ||
CallExpression: node => | ||
checkNode(context, node as TSESTree.CallExpression, importedMacroVars), | ||
}; | ||
@@ -88,0 +90,0 @@ }, |
191
src/util.ts
@@ -1,11 +0,3 @@ | ||
import { | ||
Identifier, | ||
ObjectExpression, | ||
Literal, | ||
CallExpression, | ||
Expression, | ||
Property, | ||
} from 'estree'; | ||
import {TSESTree, AST_NODE_TYPES} from '@typescript-eslint/typescript-estree'; | ||
import {Scope} from 'eslint'; | ||
import {Node, JSXOpeningElement, JSXExpressionContainer} from 'estree-jsx'; | ||
@@ -20,7 +12,14 @@ export interface MessageDescriptor { | ||
message: MessageDescriptor; | ||
messageNode?: Property['value']; | ||
descriptionNode?: Property['value']; | ||
messageNode?: TSESTree.Property['value'] | TSESTree.JSXAttribute['value']; | ||
descriptionNode?: TSESTree.Property['value'] | TSESTree.JSXAttribute['value']; | ||
} | ||
function findReferenceImport(id: Identifier, importedVars: Scope.Variable[]) { | ||
function isStringLiteral(node: TSESTree.Node): node is TSESTree.StringLiteral { | ||
return node.type === AST_NODE_TYPES.Literal && typeof node.value === 'string'; | ||
} | ||
function findReferenceImport( | ||
id: TSESTree.Identifier, | ||
importedVars: Scope.Variable[] | ||
) { | ||
return importedVars.find( | ||
@@ -31,12 +30,12 @@ v => !!v.references.find(ref => ref.identifier === id) | ||
function isIntlFormatMessageCall(node: Node) { | ||
function isIntlFormatMessageCall(node: TSESTree.Node) { | ||
return ( | ||
node.type === 'CallExpression' && | ||
node.callee.type === 'MemberExpression' && | ||
node.callee.object.type === 'Identifier' && | ||
node.type === AST_NODE_TYPES.CallExpression && | ||
node.callee.type === AST_NODE_TYPES.MemberExpression && | ||
node.callee.object.type === AST_NODE_TYPES.Identifier && | ||
node.callee.object.name === 'intl' && | ||
node.callee.property.type === 'Identifier' && | ||
node.callee.property.type === AST_NODE_TYPES.Identifier && | ||
node.callee.property.name === 'formatMessage' && | ||
node.arguments.length >= 1 && | ||
node.arguments[0].type === 'ObjectExpression' | ||
node.arguments[0].type === AST_NODE_TYPES.ObjectExpression | ||
); | ||
@@ -46,5 +45,8 @@ } | ||
function isSingleMessageDescriptorDeclaration( | ||
id: Identifier, | ||
id: TSESTree.LeftHandSideExpression, | ||
importedVars: Scope.Variable[] | ||
) { | ||
if (id.type !== AST_NODE_TYPES.Identifier) { | ||
return false; | ||
} | ||
const importedVar = findReferenceImport(id, importedVars); | ||
@@ -54,8 +56,11 @@ if (!importedVar) { | ||
} | ||
return importedVar.name === '_'; | ||
return importedVar.name === '_' || importedVar.name === 'defineMessage'; | ||
} | ||
function isMultipleMessageDescriptorDeclaration( | ||
id: Identifier, | ||
id: TSESTree.LeftHandSideExpression, | ||
importedVars: Scope.Variable[] | ||
) { | ||
if (id.type !== AST_NODE_TYPES.Identifier) { | ||
return false; | ||
} | ||
const importedVar = findReferenceImport(id, importedVars); | ||
@@ -69,5 +74,5 @@ if (!importedVar) { | ||
function extractMessageDescriptor( | ||
node?: ObjectExpression | ||
node?: TSESTree.Expression | ||
): MessageDescriptorNodeInfo | undefined { | ||
if (!node || !node.properties) { | ||
if (!node || node.type !== AST_NODE_TYPES.ObjectExpression) { | ||
return; | ||
@@ -80,24 +85,24 @@ } | ||
}; | ||
result.message = node.properties.reduce((msg: MessageDescriptor, prop) => { | ||
if (prop.key.type !== 'Identifier') { | ||
return msg; | ||
for (const prop of node.properties) { | ||
if ( | ||
prop.type !== AST_NODE_TYPES.Property || | ||
prop.key.type !== AST_NODE_TYPES.Identifier | ||
) { | ||
continue; | ||
} | ||
const value = | ||
prop.value.type === 'Literal' && typeof prop.value.value === 'string' | ||
? prop.value.value | ||
: undefined; | ||
const value = isStringLiteral(prop.value) ? prop.value.value : undefined; | ||
switch (prop.key.name) { | ||
case 'defaultMessage': | ||
result.messageNode = prop.value; | ||
msg.defaultMessage = value; | ||
result.message.defaultMessage = value; | ||
break; | ||
case 'description': | ||
result.descriptionNode = prop.value; | ||
msg.description = value; | ||
result.message.description = value; | ||
break; | ||
case 'id': | ||
msg.id = value; | ||
result.message.id = value; | ||
break; | ||
} | ||
return msg; | ||
}, {}); | ||
} | ||
return result; | ||
@@ -107,13 +112,21 @@ } | ||
function extractMessageDescriptorFromJSXElement( | ||
node?: JSXOpeningElement | ||
): [MessageDescriptorNodeInfo, ObjectExpression | undefined] | undefined { | ||
node?: TSESTree.JSXOpeningElement | ||
): | ||
| [MessageDescriptorNodeInfo, TSESTree.ObjectExpression | undefined] | ||
| undefined { | ||
if (!node || !node.attributes) { | ||
return; | ||
} | ||
let values: ObjectExpression | undefined = undefined; | ||
let messageNode: Literal | undefined; | ||
let descriptionNode: Literal | undefined; | ||
const message = node.attributes.reduce((msg: MessageDescriptor, prop) => { | ||
if (prop.type !== 'JSXAttribute' || prop.name.type !== 'JSXIdentifier') { | ||
return msg; | ||
let values: TSESTree.ObjectExpression | undefined; | ||
const result: MessageDescriptorNodeInfo = { | ||
message: {}, | ||
messageNode: undefined, | ||
descriptionNode: undefined, | ||
}; | ||
for (const prop of node.attributes) { | ||
if ( | ||
prop.type !== AST_NODE_TYPES.JSXAttribute || | ||
prop.name.type !== AST_NODE_TYPES.JSXIdentifier | ||
) { | ||
continue; | ||
} | ||
@@ -123,47 +136,76 @@ const key = prop.name; | ||
case 'defaultMessage': | ||
messageNode = prop.value as Literal; | ||
msg.defaultMessage = (prop.value as Literal).value as string; | ||
result.messageNode = prop.value; | ||
if ( | ||
prop.value?.type === AST_NODE_TYPES.Literal && | ||
typeof prop.value.value === 'string' | ||
) { | ||
result.message.defaultMessage = prop.value.value; | ||
} | ||
break; | ||
case 'description': | ||
descriptionNode = prop.value as Literal; | ||
msg.description = (prop.value as Literal).value as string; | ||
result.descriptionNode = prop.value; | ||
if ( | ||
prop.value?.type === AST_NODE_TYPES.Literal && | ||
typeof prop.value.value === 'string' | ||
) { | ||
result.message.description = prop.value.value; | ||
} | ||
break; | ||
case 'id': | ||
msg.id = (prop.value as Literal).value as string; | ||
if ( | ||
prop.value?.type === AST_NODE_TYPES.Literal && | ||
typeof prop.value.value === 'string' | ||
) { | ||
result.message.id = prop.value.value; | ||
} | ||
break; | ||
case 'values': | ||
values = (prop.value as JSXExpressionContainer) | ||
.expression as ObjectExpression; | ||
if ( | ||
prop.value?.type === AST_NODE_TYPES.JSXExpressionContainer && | ||
prop.value.expression.type === AST_NODE_TYPES.ObjectExpression | ||
) { | ||
values = prop.value.expression; | ||
} | ||
break; | ||
} | ||
return msg; | ||
}, {}); | ||
if (!Object.keys(message).length) { | ||
} | ||
if (!Object.keys(result.message).length) { | ||
return; | ||
} | ||
return [{messageNode, descriptionNode, message}, values]; | ||
return [result, values]; | ||
} | ||
function extractMessageDescriptors(node?: ObjectExpression) { | ||
if (!node || !node.properties) { | ||
function extractMessageDescriptors(node?: TSESTree.Expression) { | ||
if ( | ||
!node || | ||
node.type !== AST_NODE_TYPES.ObjectExpression || | ||
!node.properties.length | ||
) { | ||
return []; | ||
} | ||
return node.properties.reduce((msgs: MessageDescriptorNodeInfo[], prop) => { | ||
const msg = prop.value as ObjectExpression; | ||
const msgs = []; | ||
for (const prop of node.properties) { | ||
if (prop.type !== AST_NODE_TYPES.Property) { | ||
continue; | ||
} | ||
const msg = prop.value; | ||
if (msg.type !== AST_NODE_TYPES.ObjectExpression) { | ||
continue; | ||
} | ||
const nodeInfo = extractMessageDescriptor(msg); | ||
if (nodeInfo) { | ||
return [...msgs, nodeInfo]; | ||
msgs.push(nodeInfo); | ||
} | ||
return msgs; | ||
}, []); | ||
} | ||
return msgs; | ||
} | ||
export function extractMessages( | ||
node: Node, | ||
node: TSESTree.Node, | ||
importedMacroVars: Scope.Variable[], | ||
excludeMessageDeclCalls = false | ||
): Array<[MessageDescriptorNodeInfo, Expression | undefined]> { | ||
if (node.type === 'CallExpression') { | ||
const expr = node as CallExpression; | ||
const fnId = expr.callee as Identifier; | ||
): Array<[MessageDescriptorNodeInfo, TSESTree.Expression | undefined]> { | ||
if (node.type === AST_NODE_TYPES.CallExpression) { | ||
const expr = node; | ||
const fnId = expr.callee; | ||
if ( | ||
@@ -174,7 +216,5 @@ (!excludeMessageDeclCalls && | ||
) { | ||
const msgDescriptorNodeInfo = extractMessageDescriptor( | ||
expr.arguments[0] as ObjectExpression | ||
); | ||
const msgDescriptorNodeInfo = extractMessageDescriptor(expr.arguments[0]); | ||
if (msgDescriptorNodeInfo) { | ||
return [[msgDescriptorNodeInfo, expr.arguments[1] as Expression]]; | ||
return [[msgDescriptorNodeInfo, expr.arguments[1]]]; | ||
} | ||
@@ -185,10 +225,11 @@ } else if ( | ||
) { | ||
return extractMessageDescriptors( | ||
expr.arguments[0] as ObjectExpression | ||
).map(msg => [msg, undefined]); | ||
return extractMessageDescriptors(expr.arguments[0]).map(msg => [ | ||
msg, | ||
undefined, | ||
]); | ||
} | ||
} else if ( | ||
node.type === 'JSXOpeningElement' && | ||
node.type === AST_NODE_TYPES.JSXOpeningElement && | ||
node.name && | ||
node.name.type === 'JSXIdentifier' && | ||
node.name.type === AST_NODE_TYPES.JSXIdentifier && | ||
node.name.name === 'FormattedMessage' | ||
@@ -195,0 +236,0 @@ ) { |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
126306
2287
394