UNPKG

@ubiquity-os/eslint-plugin-no-empty-strings

Version:

ESLint rule to disallow empty string literals in runtime/value contexts for TypeScript and JSX/TSX.

8 lines (7 loc) 8.57 kB
{ "version": 3, "sources": ["../src/index.ts", "../src/no-empty-strings.ts"], "sourcesContent": ["import emptyStringRule from \"./no-empty-strings\";\n\n/**\n * ESLint plugin export under the namespace \"ubiquity-os\"\n * Consumers will use rule key: \"ubiquity-os/no-empty-strings\"\n */\nconst plugin = {\n rules: {\n \"no-empty-strings\": emptyStringRule,\n },\n};\n\nexport default plugin;\n", "import type { TSESTree } from \"@typescript-eslint/types\";\nimport { ESLintUtils } from \"@typescript-eslint/utils\";\n\ntype Options = [\n {\n checkWhitespaceOnly?: boolean;\n checkNoSubstitutionTemplates?: boolean;\n ignoreZeroWidth?: boolean;\n allowContexts?: string[];\n },\n];\n\ntype MessageIds = \"emptyStringNotAllowed\";\n\nconst createRule = ESLintUtils.RuleCreator((name) => `https://github.com/ubiquity-os/no-empty-strings/docs/rules/${name}`);\n\nfunction isZeroWidthOnly(text: string): boolean {\n // Common zero-width characters: BOM/ZWNBSP (FEFF), ZWSP (200B), ZWNJ (200C), ZWJ (200D)\n // Also include left-to-right mark (200E) and right-to-left mark (200F)\n // Replace common zero-width characters individually to avoid joined-sequence lint warnings\n const stripped = text\n .replace(/\\uFEFF/gu, \"\")\n .replace(/\\u200B/gu, \"\")\n .replace(/\\u200C/gu, \"\")\n .replace(/\\u200D/gu, \"\")\n .replace(/\\u200E/gu, \"\")\n .replace(/\\u200F/gu, \"\");\n return stripped.length === 0;\n}\n\nfunction isWhitespaceOnly(text: string): boolean {\n return text.trim().length === 0;\n}\n\nfunction isTypePosition(node: TSESTree.Node | null): boolean {\n while (node) {\n if (\n node.type === \"TSTypeAnnotation\" ||\n node.type === \"TSTypeAliasDeclaration\" ||\n node.type === \"TSLiteralType\" ||\n node.type === \"TSTypeReference\" ||\n node.type === \"TSInterfaceDeclaration\" ||\n node.type === \"TSPropertySignature\"\n ) {\n return true;\n }\n node = node.parent ?? null;\n }\n return false;\n}\n\nfunction isImportOrExportSpecifier(node: TSESTree.Node | null): boolean {\n while (node) {\n if (\n node.type === \"ImportDeclaration\" ||\n node.type === \"ExportAllDeclaration\" ||\n node.type === \"ExportNamedDeclaration\" ||\n node.type === \"ImportExpression\"\n ) {\n return true;\n }\n node = node.parent ?? null;\n }\n return false;\n}\n\nfunction shouldSkipByAllowedContexts(node: TSESTree.Node, allowContexts: string[] | undefined): boolean {\n if (!allowContexts || allowContexts.length === 0) {\n return false;\n }\n let cursor: TSESTree.Node | null = node;\n while (cursor) {\n if (allowContexts.indexOf(cursor.type) !== -1) {\n return true;\n }\n cursor = cursor.parent ?? null;\n }\n return false;\n}\n\nconst rule = createRule<Options, MessageIds>({\n name: \"no-empty-strings\",\n meta: {\n type: \"problem\",\n docs: {\n description: \"Disallow empty string literals in runtime/value contexts\",\n },\n messages: {\n emptyStringNotAllowed: \"Empty string literal is not allowed.\",\n },\n schema: [\n {\n type: \"object\",\n properties: {\n checkWhitespaceOnly: { type: \"boolean\" },\n checkNoSubstitutionTemplates: { type: \"boolean\" },\n ignoreZeroWidth: { type: \"boolean\" },\n allowContexts: {\n type: \"array\",\n items: { type: \"string\" },\n },\n },\n additionalProperties: false,\n },\n ],\n },\n defaultOptions: [\n {\n checkWhitespaceOnly: false,\n checkNoSubstitutionTemplates: true,\n ignoreZeroWidth: false,\n allowContexts: [],\n },\n ],\n create: function create(context, [options]) {\n function reportIfEmpty(text: string, node: TSESTree.Node): void {\n if (shouldSkipByAllowedContexts(node, options.allowContexts)) {\n return;\n }\n if (isTypePosition(node) || isImportOrExportSpecifier(node)) {\n return;\n }\n\n const value = text;\n\n if (options.ignoreZeroWidth === true && isZeroWidthOnly(value)) {\n return;\n }\n\n if (value.length === 0) {\n context.report({ node, messageId: \"emptyStringNotAllowed\" });\n return;\n }\n\n if (options.checkWhitespaceOnly && isWhitespaceOnly(value)) {\n context.report({ node, messageId: \"emptyStringNotAllowed\" });\n }\n }\n\n return {\n Literal: function onLiteral(node: TSESTree.Literal) {\n if (typeof node.value !== \"string\") {\n return;\n }\n if (node.parent && node.parent.type === \"JSXAttribute\") {\n return;\n }\n reportIfEmpty(String(node.value), node);\n },\n\n TemplateLiteral: function onTemplateLiteral(node: TSESTree.TemplateLiteral) {\n // Ignore templates with expressions; they are dynamic values\n if (node.expressions && node.expressions.length > 0) {\n return;\n }\n // NoSubstitutionTemplateLiteral equivalent: exactly one quasi\n if (!options.checkNoSubstitutionTemplates) {\n return;\n }\n const quasis = node.quasis ?? [];\n if (quasis.length !== 1) {\n return;\n }\n const first = quasis[0];\n const cooked = first?.value?.cooked ?? \"\";\n reportIfEmpty(cooked, node);\n },\n\n JSXAttribute: function onJsxAttribute(node: TSESTree.JSXAttribute) {\n if (!node.value) {\n return;\n }\n if (node.value.type === \"Literal\" && typeof (node.value as TSESTree.Literal).value === \"string\") {\n reportIfEmpty(String((node.value as TSESTree.Literal).value), node.value);\n }\n },\n };\n },\n});\n\nexport default rule;\n"], "mappings": "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", "names": ["src_exports", "__export", "src_default", "__toCommonJS", "import_utils", "createRule", "name", "isZeroWidthOnly", "text", "isWhitespaceOnly", "isTypePosition", "node", "isImportOrExportSpecifier", "shouldSkipByAllowedContexts", "allowContexts", "cursor", "rule", "context", "options", "reportIfEmpty", "value", "quasis", "cooked", "no_empty_strings_default", "plugin", "no_empty_strings_default", "src_default"] }