diff --git a/package.json b/package.json index 8198bf9c98..32f96f71a4 100644 --- a/package.json +++ b/package.json @@ -118,7 +118,8 @@ "ts-pegjs": "0.3", "tslib": "^2.0.1", "typescript": "4", - "vue": "3" + "vue": "3", + "vue-eslint-parser": "^7.3.0" }, "eslintConfig": { "parser": "@typescript-eslint/parser", diff --git a/packages/eslint-plugin-formatjs/BUILD b/packages/eslint-plugin-formatjs/BUILD index 09545dee69..6b90661d99 100644 --- a/packages/eslint-plugin-formatjs/BUILD +++ b/packages/eslint-plugin-formatjs/BUILD @@ -48,22 +48,28 @@ ts_compile( deps = SRC_DEPS, ) -jest_test( - name = "unit", - srcs = SRCS + glob([ - "tests/**/*.ts", - "tests/**/*.tsx", - "tests/**/*.snap", - "tests/**/*.js", - "tests/**/*.json", - ]), +TESTS_BASE_SRCS = SRCS + glob( + [ + "tests/*.ts", + ], + exclude = ["tests/*.test.ts"], +) + +TEST_FILES = glob([ + "tests/*.test.ts", +]) + +[jest_test( + name = "unit-%s" % f[6:f.index(".test.ts")], + srcs = TESTS_BASE_SRCS + [f], deps = [ "@npm//eslint", "@npm//@typescript-eslint/parser", + "@npm//vue-eslint-parser", "//packages/intl-messageformat-parser:types", "//packages/ts-transformer:types", ] + SRC_DEPS, -) +) for f in TEST_FILES] generated_file_test( name = "tsconfig_json", diff --git a/packages/eslint-plugin-formatjs/rules/blacklist-elements.ts b/packages/eslint-plugin-formatjs/rules/blacklist-elements.ts index 33920c55f5..da1ac9b22d 100644 --- a/packages/eslint-plugin-formatjs/rules/blacklist-elements.ts +++ b/packages/eslint-plugin-formatjs/rules/blacklist-elements.ts @@ -136,6 +136,19 @@ const rule: Rule.RuleModule = { }, create(context) { let importedMacroVars: Scope.Variable[] = []; + const callExpressionVisitor = (node: Node) => + checkNode(context, node, importedMacroVars); + + if (context.parserServices.defineTemplateBodyVisitor) { + return context.parserServices.defineTemplateBodyVisitor( + { + CallExpression: callExpressionVisitor, + }, + { + CallExpression: callExpressionVisitor, + } + ); + } return { ImportDeclaration: node => { const moduleName = (node as ImportDeclaration).source.value; @@ -145,7 +158,7 @@ const rule: Rule.RuleModule = { }, JSXOpeningElement: (node: Node) => checkNode(context, node, importedMacroVars), - CallExpression: node => checkNode(context, node, importedMacroVars), + CallExpression: callExpressionVisitor, }; }, }; diff --git a/packages/eslint-plugin-formatjs/rules/enforce-default-message.ts b/packages/eslint-plugin-formatjs/rules/enforce-default-message.ts index 1d31915cab..f1fd5df5d3 100644 --- a/packages/eslint-plugin-formatjs/rules/enforce-default-message.ts +++ b/packages/eslint-plugin-formatjs/rules/enforce-default-message.ts @@ -53,6 +53,19 @@ const rule: Rule.RuleModule = { }, create(context) { let importedMacroVars: Scope.Variable[] = []; + const callExpressionVisitor = (node: Node) => + checkNode(context, node, importedMacroVars); + + if (context.parserServices.defineTemplateBodyVisitor) { + return context.parserServices.defineTemplateBodyVisitor( + { + CallExpression: callExpressionVisitor, + }, + { + CallExpression: callExpressionVisitor, + } + ); + } return { ImportDeclaration: node => { const moduleName = (node as ImportDeclaration).source.value; @@ -62,7 +75,7 @@ const rule: Rule.RuleModule = { }, JSXOpeningElement: (node: Node) => checkNode(context, node, importedMacroVars), - CallExpression: node => checkNode(context, node, importedMacroVars), + CallExpression: callExpressionVisitor, }; }, }; diff --git a/packages/eslint-plugin-formatjs/rules/enforce-description.ts b/packages/eslint-plugin-formatjs/rules/enforce-description.ts index db0c087123..644f69ec72 100644 --- a/packages/eslint-plugin-formatjs/rules/enforce-description.ts +++ b/packages/eslint-plugin-formatjs/rules/enforce-description.ts @@ -53,6 +53,19 @@ export default { }, create(context) { let importedMacroVars: Scope.Variable[] = []; + const callExpressionVisitor = (node: Node) => + checkNode(context, node, importedMacroVars); + + if (context.parserServices.defineTemplateBodyVisitor) { + return context.parserServices.defineTemplateBodyVisitor( + { + CallExpression: callExpressionVisitor, + }, + { + CallExpression: callExpressionVisitor, + } + ); + } return { ImportDeclaration: node => { const moduleName = (node as ImportDeclaration).source.value; @@ -62,7 +75,7 @@ export default { }, JSXOpeningElement: (node: Node) => checkNode(context, node, importedMacroVars), - CallExpression: node => checkNode(context, node, importedMacroVars), + CallExpression: callExpressionVisitor, }; }, } as Rule.RuleModule; diff --git a/packages/eslint-plugin-formatjs/rules/enforce-id.ts b/packages/eslint-plugin-formatjs/rules/enforce-id.ts index 7fc868b1ce..5b6595ff4e 100644 --- a/packages/eslint-plugin-formatjs/rules/enforce-id.ts +++ b/packages/eslint-plugin-formatjs/rules/enforce-id.ts @@ -112,6 +112,19 @@ export default { }, create(context) { let importedMacroVars: Scope.Variable[] = []; + const callExpressionVisitor = (node: Node) => + checkNode(context, node, importedMacroVars); + + if (context.parserServices.defineTemplateBodyVisitor) { + return context.parserServices.defineTemplateBodyVisitor( + { + CallExpression: callExpressionVisitor, + }, + { + CallExpression: callExpressionVisitor, + } + ); + } return { ImportDeclaration: node => { const moduleName = (node as ImportDeclaration).source.value; @@ -121,7 +134,7 @@ export default { }, JSXOpeningElement: (node: Node) => checkNode(context, node, importedMacroVars), - CallExpression: node => checkNode(context, node, importedMacroVars), + CallExpression: callExpressionVisitor, }; }, } as Rule.RuleModule; diff --git a/packages/eslint-plugin-formatjs/rules/enforce-placeholders.ts b/packages/eslint-plugin-formatjs/rules/enforce-placeholders.ts index 1f77c3c2bb..4b52d668ce 100644 --- a/packages/eslint-plugin-formatjs/rules/enforce-placeholders.ts +++ b/packages/eslint-plugin-formatjs/rules/enforce-placeholders.ts @@ -129,6 +129,19 @@ const rule: Rule.RuleModule = { }, create(context) { let importedMacroVars: Scope.Variable[] = []; + const callExpressionVisitor = (node: TSESTree.Node) => + checkNode(context, node, importedMacroVars); + + if (context.parserServices.defineTemplateBodyVisitor) { + return context.parserServices.defineTemplateBodyVisitor( + { + CallExpression: callExpressionVisitor, + }, + { + CallExpression: callExpressionVisitor, + } + ); + } return { ImportDeclaration: node => { const moduleName = (node as ImportDeclaration).source.value; @@ -138,8 +151,7 @@ const rule: Rule.RuleModule = { }, JSXOpeningElement: (node: Node) => checkNode(context, node as TSESTree.Node, importedMacroVars), - CallExpression: node => - checkNode(context, node as TSESTree.Node, importedMacroVars), + CallExpression: callExpressionVisitor, }; }, }; diff --git a/packages/eslint-plugin-formatjs/rules/enforce-plural-rules.ts b/packages/eslint-plugin-formatjs/rules/enforce-plural-rules.ts index 87ac217935..caed6f3b5a 100644 --- a/packages/eslint-plugin-formatjs/rules/enforce-plural-rules.ts +++ b/packages/eslint-plugin-formatjs/rules/enforce-plural-rules.ts @@ -111,6 +111,19 @@ const rule: Rule.RuleModule = { }, create(context) { let importedMacroVars: Scope.Variable[] = []; + const callExpressionVisitor = (node: TSESTree.Node) => + checkNode(context, node, importedMacroVars); + + if (context.parserServices.defineTemplateBodyVisitor) { + return context.parserServices.defineTemplateBodyVisitor( + { + CallExpression: callExpressionVisitor, + }, + { + CallExpression: callExpressionVisitor, + } + ); + } return { ImportDeclaration: node => { const moduleName = (node as ImportDeclaration).source.value; @@ -120,8 +133,7 @@ const rule: Rule.RuleModule = { }, JSXOpeningElement: (node: Node) => checkNode(context, node as TSESTree.Node, importedMacroVars), - CallExpression: node => - checkNode(context, node as TSESTree.Node, importedMacroVars), + CallExpression: callExpressionVisitor, }; }, }; diff --git a/packages/eslint-plugin-formatjs/rules/no-camel-case.ts b/packages/eslint-plugin-formatjs/rules/no-camel-case.ts index 21ecf99340..e3d398c1d4 100644 --- a/packages/eslint-plugin-formatjs/rules/no-camel-case.ts +++ b/packages/eslint-plugin-formatjs/rules/no-camel-case.ts @@ -75,6 +75,19 @@ const rule: Rule.RuleModule = { }, create(context) { let importedMacroVars: Scope.Variable[] = []; + const callExpressionVisitor = (node: TSESTree.Node) => + checkNode(context, node, importedMacroVars); + + if (context.parserServices.defineTemplateBodyVisitor) { + return context.parserServices.defineTemplateBodyVisitor( + { + CallExpression: callExpressionVisitor, + }, + { + CallExpression: callExpressionVisitor, + } + ); + } return { ImportDeclaration: node => { const moduleName = (node as ImportDeclaration).source.value; @@ -84,8 +97,7 @@ const rule: Rule.RuleModule = { }, JSXOpeningElement: (node: Node) => checkNode(context, node as TSESTree.Node, importedMacroVars), - CallExpression: node => - checkNode(context, node as TSESTree.Node, importedMacroVars), + CallExpression: callExpressionVisitor, }; }, }; diff --git a/packages/eslint-plugin-formatjs/rules/no-emoji.ts b/packages/eslint-plugin-formatjs/rules/no-emoji.ts index b659e9ba9e..f5e91278a1 100644 --- a/packages/eslint-plugin-formatjs/rules/no-emoji.ts +++ b/packages/eslint-plugin-formatjs/rules/no-emoji.ts @@ -43,6 +43,19 @@ const rule: Rule.RuleModule = { }, create(context) { let importedMacroVars: Scope.Variable[] = []; + const callExpressionVisitor = (node: TSESTree.Node) => + checkNode(context, node, importedMacroVars); + + if (context.parserServices.defineTemplateBodyVisitor) { + return context.parserServices.defineTemplateBodyVisitor( + { + CallExpression: callExpressionVisitor, + }, + { + CallExpression: callExpressionVisitor, + } + ); + } return { ImportDeclaration: node => { const moduleName = (node as ImportDeclaration).source.value; @@ -52,8 +65,7 @@ const rule: Rule.RuleModule = { }, JSXOpeningElement: (node: Node) => checkNode(context, node as TSESTree.Node, importedMacroVars), - CallExpression: node => - checkNode(context, node as TSESTree.Node, importedMacroVars), + CallExpression: callExpressionVisitor, }; }, }; diff --git a/packages/eslint-plugin-formatjs/rules/no-id.ts b/packages/eslint-plugin-formatjs/rules/no-id.ts index 346bfa4cd0..1fc26a8c68 100644 --- a/packages/eslint-plugin-formatjs/rules/no-id.ts +++ b/packages/eslint-plugin-formatjs/rules/no-id.ts @@ -41,6 +41,19 @@ export default { }, create(context) { let importedMacroVars: Scope.Variable[] = []; + const callExpressionVisitor = (node: Node) => + checkNode(context, node, importedMacroVars); + + if (context.parserServices.defineTemplateBodyVisitor) { + return context.parserServices.defineTemplateBodyVisitor( + { + CallExpression: callExpressionVisitor, + }, + { + CallExpression: callExpressionVisitor, + } + ); + } return { ImportDeclaration: node => { const moduleName = (node as ImportDeclaration).source.value; @@ -50,7 +63,7 @@ export default { }, JSXOpeningElement: (node: Node) => checkNode(context, node, importedMacroVars), - CallExpression: node => checkNode(context, node, importedMacroVars), + CallExpression: callExpressionVisitor, }; }, } as Rule.RuleModule; diff --git a/packages/eslint-plugin-formatjs/rules/no-multiple-plurals.ts b/packages/eslint-plugin-formatjs/rules/no-multiple-plurals.ts index 127f4d5a8d..3979fa3d3e 100644 --- a/packages/eslint-plugin-formatjs/rules/no-multiple-plurals.ts +++ b/packages/eslint-plugin-formatjs/rules/no-multiple-plurals.ts @@ -67,6 +67,19 @@ const rule: Rule.RuleModule = { }, create(context) { let importedMacroVars: Scope.Variable[] = []; + const callExpressionVisitor = (node: TSESTree.Node) => + checkNode(context, node, importedMacroVars); + + if (context.parserServices.defineTemplateBodyVisitor) { + return context.parserServices.defineTemplateBodyVisitor( + { + CallExpression: callExpressionVisitor, + }, + { + CallExpression: callExpressionVisitor, + } + ); + } return { ImportDeclaration: node => { const moduleName = (node as ImportDeclaration).source.value; @@ -76,8 +89,7 @@ const rule: Rule.RuleModule = { }, JSXOpeningElement: (node: Node) => checkNode(context, node as TSESTree.Node, importedMacroVars), - CallExpression: node => - checkNode(context, node as TSESTree.Node, importedMacroVars), + CallExpression: callExpressionVisitor, }; }, }; diff --git a/packages/eslint-plugin-formatjs/rules/no-multiple-whitespaces.ts b/packages/eslint-plugin-formatjs/rules/no-multiple-whitespaces.ts index a15c947321..3541c6c8d5 100644 --- a/packages/eslint-plugin-formatjs/rules/no-multiple-whitespaces.ts +++ b/packages/eslint-plugin-formatjs/rules/no-multiple-whitespaces.ts @@ -51,6 +51,19 @@ const rule: Rule.RuleModule = { }, create(context) { let importedMacroVars: Scope.Variable[] = []; + const callExpressionVisitor = (node: TSESTree.Node) => + checkNode(context, node, importedMacroVars); + + if (context.parserServices.defineTemplateBodyVisitor) { + return context.parserServices.defineTemplateBodyVisitor( + { + CallExpression: callExpressionVisitor, + }, + { + CallExpression: callExpressionVisitor, + } + ); + } return { ImportDeclaration: node => { const moduleName = (node as ImportDeclaration).source.value; @@ -60,8 +73,7 @@ const rule: Rule.RuleModule = { }, JSXOpeningElement: (node: Node) => checkNode(context, node as TSESTree.Node, importedMacroVars), - CallExpression: node => - checkNode(context, node as TSESTree.Node, importedMacroVars), + CallExpression: callExpressionVisitor, }; }, }; diff --git a/packages/eslint-plugin-formatjs/rules/no-offset.ts b/packages/eslint-plugin-formatjs/rules/no-offset.ts index 2fd585999d..8048fd9bea 100644 --- a/packages/eslint-plugin-formatjs/rules/no-offset.ts +++ b/packages/eslint-plugin-formatjs/rules/no-offset.ts @@ -66,6 +66,19 @@ const rule: Rule.RuleModule = { }, create(context) { let importedMacroVars: Scope.Variable[] = []; + const callExpressionVisitor = (node: TSESTree.Node) => + checkNode(context, node, importedMacroVars); + + if (context.parserServices.defineTemplateBodyVisitor) { + return context.parserServices.defineTemplateBodyVisitor( + { + CallExpression: callExpressionVisitor, + }, + { + CallExpression: callExpressionVisitor, + } + ); + } return { ImportDeclaration: node => { const moduleName = (node as ImportDeclaration).source.value; @@ -75,8 +88,7 @@ const rule: Rule.RuleModule = { }, JSXOpeningElement: (node: Node) => checkNode(context, node as TSESTree.Node, importedMacroVars), - CallExpression: node => - checkNode(context, node as TSESTree.Node, importedMacroVars), + CallExpression: callExpressionVisitor, }; }, }; diff --git a/packages/eslint-plugin-formatjs/tests/blacklist-elements.test.ts b/packages/eslint-plugin-formatjs/tests/blacklist-elements.test.ts index 65b4deafc0..40403cdd12 100644 --- a/packages/eslint-plugin-formatjs/tests/blacklist-elements.test.ts +++ b/packages/eslint-plugin-formatjs/tests/blacklist-elements.test.ts @@ -1,6 +1,7 @@ import blacklistElements from '../rules/blacklist-elements'; -import {ruleTester} from './util'; -import {dynamicMessage, noMatch, spreadJsx, emptyFnCall} from './fixtures'; +import {dynamicMessage, emptyFnCall, noMatch, spreadJsx} from './fixtures'; +import {ruleTester, vueRuleTester} from './util'; + ruleTester.run('blacklist-elements', blacklistElements, { valid: [ { @@ -31,3 +32,48 @@ ruleTester.run('blacklist-elements', blacklistElements, { }, ], }); + +vueRuleTester.run('vue/blacklist-elements', blacklistElements, { + valid: [ + { + code: ``, + options: [['selectordinal']], + }, + ``, + ``, + ``, + ], + invalid: [ + { + code: ` + `, + options: [['selectordinal']], + errors: [ + { + message: 'selectordinal element is blacklisted', + }, + ], + }, + { + code: ` + `, + options: [['selectordinal']], + errors: [ + { + message: 'selectordinal element is blacklisted', + }, + ], + }, + ], +}); diff --git a/packages/eslint-plugin-formatjs/tests/enforce-default-message.test.ts b/packages/eslint-plugin-formatjs/tests/enforce-default-message.test.ts index 68f19a0d7b..3290993643 100644 --- a/packages/eslint-plugin-formatjs/tests/enforce-default-message.test.ts +++ b/packages/eslint-plugin-formatjs/tests/enforce-default-message.test.ts @@ -1,6 +1,6 @@ import enforceDefaultMessage from '../rules/enforce-default-message'; import {noMatch, spreadJsx, emptyFnCall, dynamicMessage} from './fixtures'; -import {ruleTester} from './util'; +import {ruleTester, vueRuleTester} from './util'; ruleTester.run('enforce-default-message', enforceDefaultMessage, { valid: [ @@ -187,3 +187,50 @@ const a = `, }, ], }); + +vueRuleTester.run('vue/enforce-default-message', enforceDefaultMessage, { + valid: [ + ``, + ``, + ``, + ], + invalid: [ + { + code: ` + `, + errors: [ + { + message: '`defaultMessage` has to be specified in message descriptor', + }, + ], + }, + { + code: ` + `, + errors: [ + { + message: + '`defaultMessage` must be a string literal (not function call or variable)', + }, + ], + options: ['literal'], + }, + ], +}); diff --git a/packages/eslint-plugin-formatjs/tests/util.ts b/packages/eslint-plugin-formatjs/tests/util.ts index 00183d2099..41008f804d 100644 --- a/packages/eslint-plugin-formatjs/tests/util.ts +++ b/packages/eslint-plugin-formatjs/tests/util.ts @@ -11,3 +11,16 @@ export const ruleTester = new RuleTester({ }, }, }); + +export const vueRuleTester = new RuleTester({ + parser: require.resolve('vue-eslint-parser'), + parserOptions: { + ecmaVersion: 6, + sourceType: 'module', + ecmaFeatures: { + globalReturn: false, + impliedStrict: false, + jsx: false, + }, + }, +}); diff --git a/packages/eslint-plugin-formatjs/util.ts b/packages/eslint-plugin-formatjs/util.ts index 24e1af1814..67d4c25afb 100644 --- a/packages/eslint-plugin-formatjs/util.ts +++ b/packages/eslint-plugin-formatjs/util.ts @@ -60,6 +60,14 @@ function isIntlFormatMessageCall(node: TSESTree.Node) { ); } +function is$formatMessageCall(node: TSESTree.Node) { + return ( + node.type === 'CallExpression' && + node.callee.type === 'Identifier' && + node.callee.name === '$formatMessage' + ); +} + function isSingleMessageDescriptorDeclaration( id: TSESTree.LeftHandSideExpression, importedVars: Scope.Variable[] @@ -248,7 +256,8 @@ export function extractMessages( if ( (!excludeMessageDeclCalls && isSingleMessageDescriptorDeclaration(fnId, importedMacroVars)) || - isIntlFormatMessageCall(node) + isIntlFormatMessageCall(node) || + is$formatMessageCall(node) ) { const msgDescriptorNodeInfo = extractMessageDescriptor(expr.arguments[0]); if (msgDescriptorNodeInfo) { diff --git a/yarn.lock b/yarn.lock index f03d185d43..3a1e9e225f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4025,7 +4025,7 @@ acorn-globals@^6.0.0: acorn "^7.1.1" acorn-walk "^7.1.1" -acorn-jsx@^5.0.1, acorn-jsx@^5.3.1: +acorn-jsx@^5.0.1, acorn-jsx@^5.2.0, acorn-jsx@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b" integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng== @@ -7938,6 +7938,15 @@ eslint@^7.4.0: text-table "^0.2.0" v8-compile-cache "^2.0.3" +espree@^6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-6.2.1.tgz#77fc72e1fd744a2052c20f38a5b575832e82734a" + integrity sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw== + dependencies: + acorn "^7.1.1" + acorn-jsx "^5.2.0" + eslint-visitor-keys "^1.1.0" + espree@^7.3.0, espree@^7.3.1: version "7.3.1" resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6" @@ -7957,7 +7966,7 @@ esprima@^4.0.0, esprima@^4.0.1, esprima@~4.0.0: resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== -esquery@^1.2.0: +esquery@^1.0.1, esquery@^1.2.0: version "1.3.1" resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.3.1.tgz#b78b5828aa8e214e29fb74c4d5b752e1c033da57" integrity sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ== @@ -18416,6 +18425,18 @@ void-elements@^2.0.0: resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" integrity sha1-wGavtYK7HLQSjWDqkjkulNXp2+w= +vue-eslint-parser@^7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-7.3.0.tgz#894085839d99d81296fa081d19643733f23d7559" + integrity sha512-n5PJKZbyspD0+8LnaZgpEvNCrjQx1DyDHw8JdWwoxhhC+yRip4TAvSDpXGf9SWX6b0umeB5aR61gwUo6NVvFxw== + dependencies: + debug "^4.1.1" + eslint-scope "^5.0.0" + eslint-visitor-keys "^1.1.0" + espree "^6.2.1" + esquery "^1.0.1" + lodash "^4.17.15" + vue@3: version "3.0.5" resolved "https://registry.yarnpkg.com/vue/-/vue-3.0.5.tgz#de1b82eba24abfe71e0970fc9b8d4b2babdc3fe1"