├── .npmrc ├── .gitignore ├── CHANGELOG.md ├── .prettierrc ├── SECURITY.md ├── tsconfig.json ├── src ├── index.ts └── rules │ ├── jsx-explicit-boolean.ts │ └── jsx-explicit-boolean.test.tsx ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── FUNDING.yml └── workflows │ ├── greetings.yml │ ├── npm-publish.yml │ └── codeql.yml ├── LICENSE ├── .eslintrc ├── README.md ├── package.json ├── CONTRIBUTING.md └── CODE_OF_CONDUCT.md /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | allow-same-version=true 3 | message=v%s -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | coverage/ 3 | lib 4 | node_modules 5 | npm-debug.log 6 | 7 | npm-shrinkwrap.json 8 | package-lock.json 9 | yarn-error.log 10 | yarn.lock 11 | 12 | .npmignore -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "semi": true, 5 | "singleQuote": false, 6 | "quoteProps": "as-needed", 7 | "jsxSingleQuote": false, 8 | "trailingComma": "es5", 9 | "bracketSpacing": true, 10 | "arrowParens": "always", 11 | "printWidth": 80 12 | } 13 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | 1.1.x | :white_check_mark: | 8 | 9 | ## Reporting a Vulnerability 10 | 11 | Create an issue with the tag "vulnerability" and we will review it with priority. 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "strict": false, 6 | "esModuleInterop": true, 7 | "skipLibCheck": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "moduleResolution": "node", 10 | "resolveJsonModule": true, 11 | "lib": ["ES2020"], 12 | "outDir": "dist", 13 | "rootDir": "src" 14 | }, 15 | "exclude": ["node_modules"] 16 | } 17 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | "jsx-explicit-boolean": require("./rules/jsx-explicit-boolean"), 4 | }, 5 | 6 | configs: { 7 | recommended: { 8 | plugins: ["safe-jsx"], 9 | parserOptions: { 10 | ecmaFeatures: { 11 | jsx: true, 12 | }, 13 | }, 14 | rules: { 15 | "safe-jsx/jsx-explicit-boolean": "error", 16 | }, 17 | }, 18 | 19 | strict: { 20 | plugins: ["safe-jsx"], 21 | parserOptions: { 22 | ecmaFeatures: { 23 | jsx: true, 24 | }, 25 | }, 26 | rules: { 27 | "safe-jsx/jsx-explicit-boolean": "error", 28 | }, 29 | }, 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [gstj] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/workflows/greetings.yml: -------------------------------------------------------------------------------- 1 | name: Greetings 2 | 3 | on: [pull_request_target, issues] 4 | 5 | jobs: 6 | greeting: 7 | runs-on: ubuntu-latest 8 | permissions: 9 | issues: write 10 | pull-requests: write 11 | steps: 12 | - uses: actions/first-interaction@v1 13 | with: 14 | repo-token: ${{ secrets.GITHUB_TOKEN }} 15 | issue-message: "👋 Hello! Thank you so much for creating your first issue in '@gstj/safe-jsx'! We appreciate your contribution and will review it as soon as possible. In the meantime, please make sure you have followed our guidelines and codes of conduct." 16 | pr-message: "🎉 Congratulations on making your first pull request in '@gstj/safe-jsx'! We appreciate your contribution and will review it as soon as possible. Thank you for helping us make this project better." 17 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages 3 | 4 | name: Node.js Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/setup-node@v4 16 | with: 17 | node-version: 16 18 | - run: yarn build 19 | - run: yarn test 20 | 21 | publish-npm: 22 | needs: build 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v3 26 | - uses: actions/setup-node@v4 27 | with: 28 | node-version: 16 29 | registry-url: https://registry.npmjs.org/ 30 | - run: yarn build 31 | - run: yarn publish 32 | env: 33 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Gabriel Taveira 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": ["airbnb-base", "plugin:prettier/recommended"], 4 | "ignorePatterns": ["lib/", "reports/"], 5 | "parser": "@typescript-eslint/parser", 6 | "plugins": ["@typescript-eslint", "prettier"], 7 | "settings": { 8 | "import/resolver": { 9 | "node": { 10 | "extensions": [".js", ".jsx", ".ts", ".tsx"] 11 | } 12 | } 13 | }, 14 | "rules": { 15 | "max-len": "off", 16 | "no-template-curly-in-string": "off", 17 | "global-require": "off", 18 | "import/extensions": [ 19 | "error", 20 | "ignorePackages", 21 | { 22 | "js": "never", 23 | "jsx": "never", 24 | "ts": "never", 25 | "tsx": "never" 26 | } 27 | ] 28 | }, 29 | "overrides": [ 30 | { 31 | "files": ["src/rules/*"], 32 | "extends": ["plugin:eslint-plugin/rules-recommended"], 33 | "rules": { 34 | "eslint-plugin/require-meta-docs-description": [ 35 | "error", 36 | { "pattern": "^(Enforce|Require|Disallow)" } 37 | ], 38 | "eslint-plugin/require-meta-type": "off" 39 | } 40 | }, 41 | { 42 | "files": ["__tests__/src/rules/*.{js,ts,jsx,tsx}"], 43 | "extends": ["plugin:eslint-plugin/tests-recommended"] 44 | }, 45 | { 46 | "files": ["**/*.test.{js,ts,jsx,tsx}"], 47 | "rules": { 48 | "import/no-extraneous-dependencies": "off" 49 | }, 50 | "env": { 51 | "jest": true 52 | } 53 | } 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🛡️ eslint-plugin-safe-jsx 2 | 3 | `eslint-plugin-safe-jsx` is an ESLint plugin that enforces explicit boolean conversion before using the && operator with JSX in React and React Native applications. This plugin improves the reliability of your code by helping prevent certain types of bugs that can break your app. 4 | 5 | ## 💡 Why Use eslint-plugin-safe-jsx? 6 | 7 | In JavaScript, certain "falsy" values such as `0`, `''`, and `null` can lead to unexpected behavior when used in a logical expression. This can be particularly problematic in React JSX code, where you might be expecting a boolean value. 8 | 9 | Consider the following example: 10 | 11 | ```jsx 12 | const myText = 0; 13 | myText && {myText}; 14 | ``` 15 | 16 | In this scenario, the code tries to render `0` outside the Text component, leading to a failure. The issue is even more critical when the variable value comes from a server or an external source. This ESLint rule helps prevent such scenarios from occurring. 17 | 18 | With `eslint-plugin-safe-jsx`, ESLint will alert you to these potential errors and can even auto-fix them, like so: 19 | 20 | ```jsx 21 | const myText = 0; 22 | Boolean(myText) && {myText}; 23 | ``` 24 | 25 | Now, `myText` is explicitly converted to a boolean before being used in the logical expression, preventing the `0` from being rendered. 26 | 27 | For more examples, check out our [test cases](./src/rules/jsx-explicit-boolean.test.tsx). 28 | 29 | ## 🚀 Installation 30 | 31 | You'll first need to install [ESLint](https://eslint.org/docs/latest/user-guide/getting-started): 32 | 33 | ```sh 34 | # npm 35 | npm install eslint --save-dev 36 | 37 | # yarn 38 | yarn add eslint --dev 39 | ``` 40 | 41 | Next, install `eslint-plugin-safe-jsx`: 42 | 43 | ```sh 44 | # npm 45 | npm install eslint-plugin-safe-jsx --save-dev 46 | 47 | # yarn 48 | yarn add eslint-plugin-safe-jsx --dev 49 | ``` 50 | 51 | **Note:** If you installed ESLint globally (using the `-g` flag in npm, or the `global` prefix in yarn) then you must also install `eslint-plugin-safe-jsx` globally. 52 | 53 | ## ⚙️ Usage 54 | 55 | Add `safe-jsx` to the plugins section of your `.eslintrc` configuration file. You can omit the `eslint-plugin-` prefix: 56 | 57 | ```json 58 | { 59 | "plugins": ["safe-jsx"] 60 | } 61 | ``` 62 | 63 | Then configure the rules you want to use under the rules section. 64 | 65 | ```json 66 | { 67 | "rules": { 68 | "safe-jsx/jsx-explicit-boolean": "error" // or "warn" 69 | } 70 | } 71 | ``` 72 | 73 | ## 📚 Supported Rules 74 | 75 | - `jsx-explicit-boolean`: Enforces explicit boolean conversion before using the && operator with JSX. 76 | 77 | ## 🤝 Contributing 78 | 79 | We welcome your contributions! For major changes, please open an issue first to discuss what you would like to change. Don't forget to update tests as appropriate. 80 | 81 | ## 📃 License 82 | 83 | [MIT](./LICENSE) 84 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-plugin-safe-jsx", 3 | "description": "An ESLint plugin that enforces explicit boolean conversion before using && operator with JSX in React and React Native.", 4 | "keywords": [ 5 | "jsx", 6 | "react", 7 | "react-native", 8 | "eslint", 9 | "eslint-plugin", 10 | "boolean", 11 | "conversion", 12 | "javascript", 13 | "coding standards", 14 | "linting", 15 | "code quality" 16 | ], 17 | "version": "1.1.1", 18 | "main": "lib/index.js", 19 | "scripts": { 20 | "prepack": "npmignore --auto --commentLines=autogenerated && npm run build", 21 | "build": "rimraf lib && tsc --project . --outDir lib", 22 | "lint:fix": "npm run lint -- --fix", 23 | "lint": "eslint --ext=js,mjs,cjs,ts,tsx .", 24 | "prepublish": "not-in-publish || npm run prepublishOnly", 25 | "prepublishOnly": "safe-publish-latest && npm run lint && npm run jest", 26 | "jest": "jest --coverage src/**/*.test.tsx", 27 | "posttest": "aud --production", 28 | "version": "auto-changelog && git add CHANGELOG.md", 29 | "postversion": "auto-changelog && git add CHANGELOG.md && git commit --no-edit --amend && git tag -f \"v$(node -e \"console.log(require('./package.json').version)\")\"" 30 | }, 31 | "author": "Gabriel Taveira", 32 | "license": "MIT", 33 | "devDependencies": { 34 | "@typescript-eslint/eslint-plugin": "^6.0.0", 35 | "@typescript-eslint/experimental-utils": "^5.59.5", 36 | "@typescript-eslint/parser": "^6.0.0", 37 | "aud": "^2.0.2", 38 | "auto-changelog": "^2.4.0", 39 | "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8", 40 | "eslint-config-airbnb-base": "^15.0.0", 41 | "eslint-config-prettier": "^8.8.0", 42 | "eslint-doc-generator": "^1.4.1", 43 | "eslint-plugin-eslint-plugin": "^4.3.0", 44 | "eslint-plugin-import": "^2.26.0", 45 | "eslint-plugin-prettier": "^4.2.1", 46 | "jest": "^29.5.0", 47 | "npmignore": "^0.3.0", 48 | "prettier": "^2.8.8", 49 | "rimraf": "^5.0.0", 50 | "safe-publish-latest": "^2.0.0", 51 | "semver": "^6.3.0", 52 | "ts-jest": "^29.1.0", 53 | "tsc": "^2.0.4", 54 | "typescript": "^5.0.4" 55 | }, 56 | "peerDependencies": { 57 | "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" 58 | }, 59 | "engines": { 60 | "node": ">=4.0" 61 | }, 62 | "jest": { 63 | "preset": "ts-jest", 64 | "testEnvironment": "node", 65 | "coverageReporters": [ 66 | "lcov", 67 | "json", 68 | "html" 69 | ], 70 | "coverageDirectory": "coverage", 71 | "roots": [ 72 | "src" 73 | ] 74 | }, 75 | "auto-changelog": { 76 | "output": "CHANGELOG.md", 77 | "template": "keepachangelog", 78 | "unreleased": false, 79 | "commitLimit": false, 80 | "backfillLimit": false, 81 | "hideCredit": true, 82 | "startingVersion": "6.6.2" 83 | }, 84 | "publishConfig": { 85 | "ignore": [ 86 | "!lib", 87 | ".github/workflows", 88 | "/src", 89 | "/reports", 90 | "CONTRIBUTING.md" 91 | ] 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "main" ] 20 | schedule: 21 | - cron: '38 1 * * 2' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Use only 'java' to analyze code written in Java, Kotlin or both 38 | # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both 39 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 40 | 41 | steps: 42 | - name: Checkout repository 43 | uses: actions/checkout@v3 44 | 45 | # Initializes the CodeQL tools for scanning. 46 | - name: Initialize CodeQL 47 | uses: github/codeql-action/init@v2 48 | with: 49 | languages: ${{ matrix.language }} 50 | # If you wish to specify custom queries, you can do so here or in a config file. 51 | # By default, queries listed here will override any specified in a config file. 52 | # Prefix the list here with "+" to use these queries and those in the config file. 53 | 54 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 55 | # queries: security-extended,security-and-quality 56 | 57 | 58 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). 59 | # If this step fails, then you should remove it and run the build manually (see below) 60 | - name: Autobuild 61 | uses: github/codeql-action/autobuild@v2 62 | 63 | # ℹ️ Command-line programs to run using the OS shell. 64 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 65 | 66 | # If the Autobuild fails above, remove it and uncomment the following three lines. 67 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 68 | 69 | # - run: | 70 | # echo "Run, Build Application using script" 71 | # ./location_of_script_within_repo/buildscript.sh 72 | 73 | - name: Perform CodeQL Analysis 74 | uses: github/codeql-action/analyze@v2 75 | with: 76 | category: "/language:${{matrix.language}}" 77 | -------------------------------------------------------------------------------- /src/rules/jsx-explicit-boolean.ts: -------------------------------------------------------------------------------- 1 | // Does not include sum or minus, for example, as they don't always evaluate to a boolean 2 | const binaryExpressionOperators = ["===", "!==", ">", "<", ">=", "<="]; 3 | 4 | function findVariable(context, nodeName) { 5 | let scope = context.getScope(); 6 | 7 | // Traverse the scope chain until we find the variable 8 | while (scope) { 9 | const variable = scope.variables.find((v) => v.name === nodeName); 10 | if (variable) return variable; 11 | 12 | scope = scope.upper; 13 | } 14 | 15 | return null; 16 | } 17 | 18 | function checkBooleanValidity(node, context) { 19 | const { type } = node; 20 | 21 | switch (type) { 22 | // Example: !a 23 | case "UnaryExpression": 24 | return node.operator === "!"; 25 | 26 | // Example: true or false 27 | case "Literal": 28 | return typeof node.value === "boolean"; 29 | 30 | // Example: a === b, a !== b, a > b, a < b, a >= b, a <= b 31 | case "BinaryExpression": 32 | return binaryExpressionOperators.includes(node.operator); 33 | 34 | // Example: Boolean(a) or new Boolean(a) 35 | case "CallExpression": 36 | case "NewExpression": 37 | return ( 38 | node.callee.type === "Identifier" && node.callee.name === "Boolean" 39 | ); 40 | 41 | // Example: a && b && c &&
, where all operands are boolean 42 | case "LogicalExpression": { 43 | const { operator, left, right } = node; 44 | 45 | if (operator !== "&&") return false; 46 | 47 | return ( 48 | checkBooleanValidity(left, context) && 49 | checkBooleanValidity(right, context) 50 | ); 51 | } 52 | 53 | // Example: a ? b : c, where both b and c are boolean 54 | case "ConditionalExpression": 55 | return ( 56 | checkBooleanValidity(node.test, context) && 57 | checkBooleanValidity(node.consequent, context) && 58 | checkBooleanValidity(node.alternate, context) 59 | ); 60 | 61 | case "Identifier": { 62 | const variable = findVariable(context, node.name); 63 | if (!variable) return false; 64 | 65 | const variableDef = variable.defs.find((def) => def.type === "Variable"); 66 | if (!variableDef) return false; 67 | 68 | return checkBooleanValidity(variableDef.node.init, context); 69 | } 70 | 71 | default: 72 | return false; 73 | } 74 | } 75 | 76 | module.exports = { 77 | meta: { 78 | type: "suggestion", 79 | fixable: "code", 80 | messages: { 81 | booleanConversion: 82 | "Please ensure a boolean conversion before using the && operator with JSX", 83 | }, 84 | schema: [], 85 | }, 86 | defaultOptions: [], 87 | create(context) { 88 | return { 89 | LogicalExpression(node) { 90 | // We're only interested in && operators 91 | if (node.operator !== "&&") return; 92 | 93 | // We're only interested in JSX elements on the right-hand side 94 | if (node.right.type !== "JSXElement") return; 95 | 96 | // Left-hand side part of the expression 97 | const { left } = node; 98 | 99 | // Check if it's a valid boolean usage, otherwise it must be fixed 100 | const isSafeBooleanUsage = checkBooleanValidity(left, context); 101 | if (isSafeBooleanUsage) return; 102 | 103 | // Report the error and fix it 104 | context.report({ 105 | node, 106 | messageId: "booleanConversion", 107 | fix(fixer) { 108 | return fixer.replaceTextRange( 109 | [left.range[0], left.range[1]], 110 | `Boolean(${context.getSourceCode().getText(left)})` 111 | ); 112 | }, 113 | }); 114 | }, 115 | }; 116 | }, 117 | } as const; 118 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to @gstj/safe-jsx 2 | 3 | 🎉 First off, thanks for taking the time to contribute! 🎉 4 | 5 | The following is a set of guidelines for contributing to @gstj/safe-jsx. These are just guidelines, not rules, use your best judgment and feel free to propose changes to this document in a pull request. 6 | 7 | ## Code of Conduct 8 | 9 | This project and everyone participating in it is governed by the [Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to [gstj@github.com](mailto:gstj@github.com). 10 | 11 | ## How Can I Contribute? 12 | 13 | ### Reporting Bugs 14 | 15 | - **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/gstj/safe-jsx/issues). 16 | 17 | - If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/gstj/safe-jsx/issues/new). Be sure to include a **title and clear description**, as much relevant information as possible, and a **code sample** or an **executable test case** demonstrating the expected behavior that is not occurring. 18 | 19 | ### Suggesting Enhancements 20 | 21 | If you find yourself wishing for a feature that doesn't exist in @gstj/safe-jsx, you are probably not alone. There are bound to be others out there with similar needs. Many of the features that @gstj/safe-jsx has today have been added because our users saw the need. 22 | 23 | Open an issue on our issues list on GitHub which describes the feature you would like to see, why you need it, and how it should work. 24 | 25 | ### Pull Requests 26 | 27 | - Fork the repo and create your branch from `master`. 28 | - If you've added code that should be tested, add tests. 29 | - Ensure the test suite passes. 30 | - Make sure your code lints. 31 | 32 | ## Commit Message Guidelines 33 | 34 | We have very precise rules over how our Git commit messages must be formatted. This format leads to **easier to read commit history**. 35 | 36 | - Wrap message lines to about 72 characters or so. 37 | - Each commit message consists of a **header**, a **body**, and a **footer**. 38 | - The header is mandatory and must conform to the [Commit Message Header](#commit-message-header) format. 39 | - The body is mandatory for all commits except for those of type "docs". When the body is required it must be at least 20 characters long. 40 | - The footer is optional. 41 | 42 | `(): ` 43 | 44 | ### Commit Message Header 45 | 46 | The commit message header is a single line that summarizing the change. It should be no longer than 50 characters to make sure it's shown in GitHub. 47 | 48 | `(): ` 49 | 50 | #### `` 51 | 52 | This describes the kind of change that this commit is providing. 53 | 54 | - **feat** (new feature for the user, not a new feature for build script) 55 | - **fix** (bug fix for the user, not a fix to a build script) 56 | - **docs** (changes to the documentation) 57 | - **style** (formatting, missing semi colons, etc; no production code change) 58 | - **refactor** (refactoring production code, eg. renaming a variable) 59 | - **test** (adding missing tests, refactoring tests; no production code change) 60 | - **chore** (updating grunt tasks etc; no production code change) 61 | 62 | #### `` 63 | 64 | The scope could be anything specifying the place of the commit change. 65 | 66 | For example `$location`, `$browser`, `$compile`,``, `*` etc. 67 | 68 | The `` can be empty (e.g. `revert: fix: 'alert()'`) when the change is global or does not relate to a single component. 69 | 70 | #### `` 71 | 72 | The `` is very important as it is the title of the commit message. As the name suggests, it should be a short summary about the improvements made in the commit. 73 | 74 | - use the imperative, present tense: "change" not "changed" nor "changes" 75 | - don't capitalize the first letter 76 | - no dot (.) at the end 77 | 78 | ### Commit Message Body 79 | 80 | Just as in the ``, use the imperative, present tense: "change" not "changed" nor "changes". The body should include the motivation for the change and contrast this with previous behavior. 81 | 82 | ### Commit Message Footer 83 | 84 | The footer is the place to reference any issues or pull requests related to this commit. 85 | 86 | For example: 87 | 88 | `Closes #204` 89 | 90 | ## Pull Request Process 91 | 92 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a build. 93 | 2. Update the README.md with details of changes to the interface, this includes new environment variables, exposed ports, useful file locations and container parameters. 94 | 3. Increase the version numbers in any examples files and the README.md to the new version that this Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). 95 | 4. You may merge the Pull Request in once you have the sign-off of other developer, or if you do not have permission to do that, you may request the reviewer to merge it for you. 96 | 97 | Thank you for considering contributing to @gstj/safe-jsx! 98 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | gabrielstaveira@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /src/rules/jsx-explicit-boolean.test.tsx: -------------------------------------------------------------------------------- 1 | import { TSESLint } from "@typescript-eslint/experimental-utils"; 2 | 3 | const parser = require.resolve("@typescript-eslint/parser"); 4 | 5 | const parserOptions = { 6 | parserOptions: { 7 | project: "../../tsconfig.json", 8 | sourceType: "module", 9 | }, 10 | ecmaFeatures: { 11 | experimentalObjectRestSpread: true, 12 | jsx: true, 13 | }, 14 | }; 15 | 16 | const ruleTester = new TSESLint.RuleTester({ 17 | parser, 18 | parserOptions, 19 | }); 20 | 21 | ruleTester.run("jsx-explicit-boolean", require("./jsx-explicit-boolean"), { 22 | valid: [ 23 | // Nested expressions 24 | { code: "const a = true; const b = true; (a && b) &&
;" }, 25 | 26 | // Conditional expressions 27 | { code: "const a = true; const b = false; a ? b : false &&
;" }, 28 | 29 | // Scope of variables 30 | { code: "const a = true; const Component = () => a &&
;" }, 31 | 32 | // Rest 33 | { code: "const a = true; a &&
;" }, 34 | { code: "const a = true; const b = a; b &&
;" }, 35 | { code: "const a = false; a &&
;" }, 36 | { code: "const a = true; Boolean(a) &&
;" }, 37 | { code: "const a = false; Boolean(a) &&
;" }, 38 | { code: "const a = 1; Boolean(a) &&
;" }, 39 | { 40 | code: "const Component = ({ a }) => {Boolean(a) && {a}};", 41 | }, 42 | { 43 | code: "const Component = ({ a }) => {new Boolean(a) && {a}};", 44 | }, 45 | { 46 | code: "const Component = ({ a }) => {!!a && {a}};", 47 | }, 48 | { 49 | code: "const Component = ({ a }) => {!a && {a}};", 50 | }, 51 | { 52 | code: "const index = 1; {index === 0 && };", 53 | }, 54 | { 55 | code: "const a = 1; b = '0'; {!!a && !!b && };", 56 | }, 57 | { 58 | code: "const a = 1; b = '0'; c = '0'; {!!a && !!b && !!c && };", 59 | }, 60 | { 61 | code: "{!!step.subtitle && index === activeStep - 1 && }", 62 | }, 63 | ], 64 | invalid: [ 65 | // Nested conditional expressions 66 | { 67 | code: "const a = true; const b = '0'; (a ? b : false) &&
;", 68 | errors: [{ messageId: "booleanConversion" }], 69 | output: 70 | "const a = true; const b = '0'; (Boolean(a ? b : false)) &&
;", 71 | }, 72 | 73 | // Conditional expressions 74 | { 75 | code: "const a = true; const b = '0'; (a ? b : false) &&
;", 76 | errors: [{ messageId: "booleanConversion" }], 77 | output: 78 | "const a = true; const b = '0'; (Boolean(a ? b : false)) &&
;", 79 | }, 80 | 81 | // Scope of variables 82 | { 83 | code: "const a = '0'; const Component = () => a &&
;", 84 | errors: [{ messageId: "booleanConversion" }], 85 | output: "const a = '0'; const Component = () => Boolean(a) &&
;", 86 | }, 87 | 88 | // Rest 89 | { 90 | code: "const a = 0; a &&
;", 91 | errors: [{ messageId: "booleanConversion" }], 92 | output: "const a = 0; Boolean(a) &&
;", 93 | }, 94 | { 95 | code: "const a = null; a &&
;", 96 | errors: [{ messageId: "booleanConversion" }], 97 | output: "const a = null; Boolean(a) &&
;", 98 | }, 99 | { 100 | code: 'const a = ""; a &&
;', 101 | errors: [{ messageId: "booleanConversion" }], 102 | output: 'const a = ""; Boolean(a) &&
;', 103 | }, 104 | { 105 | code: "const a = undefined; a &&
;", 106 | errors: [{ messageId: "booleanConversion" }], 107 | output: "const a = undefined; Boolean(a) &&
;", 108 | }, 109 | { 110 | code: 'const a = "0"; a &&
;', 111 | errors: [{ messageId: "booleanConversion" }], 112 | output: 'const a = "0"; Boolean(a) &&
;', 113 | }, 114 | { 115 | code: "const a = []; a &&
;", 116 | errors: [{ messageId: "booleanConversion" }], 117 | output: "const a = []; Boolean(a) &&
;", 118 | }, 119 | { 120 | code: "const a = {}; a &&
;", 121 | errors: [{ messageId: "booleanConversion" }], 122 | output: "const a = {}; Boolean(a) &&
;", 123 | }, 124 | { 125 | code: "const a = NaN; a &&
;", 126 | errors: [{ messageId: "booleanConversion" }], 127 | output: "const a = NaN; Boolean(a) &&
;", 128 | }, 129 | { 130 | code: "const Component = ({ a }) => {a && {a}};", 131 | errors: [{ messageId: "booleanConversion" }], 132 | output: 133 | "const Component = ({ a }) => {Boolean(a) && {a}};", 134 | }, 135 | { 136 | code: "const a = 1; b = '0'; {!!a && b && };", 137 | errors: [{ messageId: "booleanConversion" }], 138 | output: 139 | "const a = 1; b = '0'; {Boolean(!!a && b) && };", 140 | }, 141 | { 142 | code: "const a = 1; const b = '0'; const c = 0; {a && !!b && !!c && };", 143 | errors: [{ messageId: "booleanConversion" }], 144 | output: 145 | "const a = 1; const b = '0'; const c = 0; {Boolean(a && !!b && !!c) && };", 146 | }, 147 | { 148 | code: "const a = undefined; a &&
;", 149 | errors: [{ messageId: "booleanConversion" }], 150 | output: "const a = undefined; Boolean(a) &&
;", 151 | }, 152 | { 153 | code: "const a = 0 + 0; a &&
;", 154 | errors: [{ messageId: "booleanConversion" }], 155 | output: "const a = 0 + 0; Boolean(a) &&
;", 156 | }, 157 | ], 158 | }); 159 | --------------------------------------------------------------------------------