├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ ├── test_vscode.yml │ └── workflow.yml ├── .gitignore ├── .prettierignore ├── .vscode ├── launch.json └── settings.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── dev ├── .vscode │ └── settings.json ├── html.json ├── package-lock.json ├── package.json ├── src │ ├── my-element-1.ts │ └── my-element-2.js └── tsconfig.json ├── docs └── readme │ ├── config-table.md │ ├── jsdoc.md │ └── rules.md ├── lerna.json ├── nodemon.json ├── package-lock.json ├── package.json ├── packages ├── lit-analyzer │ ├── README.md │ ├── cli.js │ ├── package-lock.json │ ├── package.json │ ├── readme.blueprint.md │ ├── readme.config.json │ ├── readme │ │ ├── config.md │ │ ├── header.md │ │ ├── install.md │ │ └── usage.md │ ├── src │ │ ├── index.ts │ │ ├── lib │ │ │ ├── analyze │ │ │ │ ├── component-analyzer │ │ │ │ │ └── component-analyzer.ts │ │ │ │ ├── constants.ts │ │ │ │ ├── data │ │ │ │ │ ├── extra-html-data.ts │ │ │ │ │ ├── get-built-in-html-collection.ts │ │ │ │ │ └── get-user-config-html-collection.ts │ │ │ │ ├── default-lit-analyzer-context.ts │ │ │ │ ├── document-analyzer │ │ │ │ │ ├── css │ │ │ │ │ │ ├── lit-css-document-analyzer.ts │ │ │ │ │ │ └── lit-css-vscode-service.ts │ │ │ │ │ └── html │ │ │ │ │ │ ├── code-fix │ │ │ │ │ │ └── code-fixes-for-html-document.ts │ │ │ │ │ │ ├── completion │ │ │ │ │ │ ├── completions-at-offset.ts │ │ │ │ │ │ ├── completions-for-html-attr-values.ts │ │ │ │ │ │ ├── completions-for-html-attrs.ts │ │ │ │ │ │ └── completions-for-html-nodes.ts │ │ │ │ │ │ ├── definition │ │ │ │ │ │ ├── definition-for-html-attr.ts │ │ │ │ │ │ └── definition-for-html-node.ts │ │ │ │ │ │ ├── diagnostic │ │ │ │ │ │ └── validate-html-document.ts │ │ │ │ │ │ ├── lit-html-document-analyzer.ts │ │ │ │ │ │ ├── lit-html-vscode-service.ts │ │ │ │ │ │ ├── quick-info │ │ │ │ │ │ ├── quick-info-for-html-attr.ts │ │ │ │ │ │ └── quick-info-for-html-node.ts │ │ │ │ │ │ └── rename-locations │ │ │ │ │ │ ├── rename-locations-at-offset.ts │ │ │ │ │ │ └── rename-locations-for-tag-name.ts │ │ │ │ ├── lit-analyzer-config.ts │ │ │ │ ├── lit-analyzer-context.ts │ │ │ │ ├── lit-analyzer-logger.ts │ │ │ │ ├── lit-analyzer.ts │ │ │ │ ├── parse │ │ │ │ │ ├── convert-component-definitions-to-html-collection.ts │ │ │ │ │ ├── document │ │ │ │ │ │ ├── parse-documents-in-source-file.ts │ │ │ │ │ │ ├── text-document │ │ │ │ │ │ │ ├── css-document │ │ │ │ │ │ │ │ └── css-document.ts │ │ │ │ │ │ │ ├── html-document │ │ │ │ │ │ │ │ ├── html-document.ts │ │ │ │ │ │ │ │ ├── parse-html-document.ts │ │ │ │ │ │ │ │ ├── parse-html-node │ │ │ │ │ │ │ │ │ ├── parse-html-attr-assignment.ts │ │ │ │ │ │ │ │ │ ├── parse-html-attr-context.ts │ │ │ │ │ │ │ │ │ ├── parse-html-attribute.ts │ │ │ │ │ │ │ │ │ ├── parse-html-context.ts │ │ │ │ │ │ │ │ │ └── parse-html-node.ts │ │ │ │ │ │ │ │ └── parse-html-p5 │ │ │ │ │ │ │ │ │ ├── parse-html-types.ts │ │ │ │ │ │ │ │ │ └── parse-html.ts │ │ │ │ │ │ │ └── text-document.ts │ │ │ │ │ │ └── virtual-document │ │ │ │ │ │ │ ├── virtual-ast-document.ts │ │ │ │ │ │ │ ├── virtual-css-document.ts │ │ │ │ │ │ │ ├── virtual-document.ts │ │ │ │ │ │ │ └── virtual-html-document.ts │ │ │ │ │ ├── parse-dependencies │ │ │ │ │ │ ├── parse-dependencies.ts │ │ │ │ │ │ └── visit-dependencies.ts │ │ │ │ │ ├── parse-html-data │ │ │ │ │ │ ├── html-tag.ts │ │ │ │ │ │ └── parse-vscode-html-data.ts │ │ │ │ │ └── tagged-template │ │ │ │ │ │ └── find-tagged-templates.ts │ │ │ │ ├── rule-collection.ts │ │ │ │ ├── store │ │ │ │ │ ├── analyzer-definition-store.ts │ │ │ │ │ ├── analyzer-dependency-store.ts │ │ │ │ │ ├── analyzer-document-store.ts │ │ │ │ │ ├── analyzer-html-store.ts │ │ │ │ │ ├── definition-store │ │ │ │ │ │ └── default-analyzer-definition-store.ts │ │ │ │ │ ├── dependency-store │ │ │ │ │ │ └── default-analyzer-dependency-store.ts │ │ │ │ │ ├── document-store │ │ │ │ │ │ └── default-analyzer-document-store.ts │ │ │ │ │ └── html-store │ │ │ │ │ │ ├── default-analyzer-html-store.ts │ │ │ │ │ │ ├── html-data-source-merged.ts │ │ │ │ │ │ └── html-data-source.ts │ │ │ │ ├── ts-module.ts │ │ │ │ ├── types │ │ │ │ │ ├── html-node │ │ │ │ │ │ ├── html-node-attr-assignment-types.ts │ │ │ │ │ │ ├── html-node-attr-types.ts │ │ │ │ │ │ └── html-node-types.ts │ │ │ │ │ ├── lit-closing-tag-info.ts │ │ │ │ │ ├── lit-code-fix-action.ts │ │ │ │ │ ├── lit-code-fix.ts │ │ │ │ │ ├── lit-completion-details.ts │ │ │ │ │ ├── lit-completion.ts │ │ │ │ │ ├── lit-definition.ts │ │ │ │ │ ├── lit-diagnostic.ts │ │ │ │ │ ├── lit-format-edit.ts │ │ │ │ │ ├── lit-outlining-span.ts │ │ │ │ │ ├── lit-quick-info.ts │ │ │ │ │ ├── lit-rename-info.ts │ │ │ │ │ ├── lit-rename-location.ts │ │ │ │ │ ├── lit-target-kind.ts │ │ │ │ │ ├── range.ts │ │ │ │ │ └── rule │ │ │ │ │ │ ├── rule-diagnostic.ts │ │ │ │ │ │ ├── rule-fix-action.ts │ │ │ │ │ │ ├── rule-fix.ts │ │ │ │ │ │ ├── rule-module-context.ts │ │ │ │ │ │ └── rule-module.ts │ │ │ │ └── util │ │ │ │ │ ├── array-util.ts │ │ │ │ │ ├── ast-util.ts │ │ │ │ │ ├── attribute-util.ts │ │ │ │ │ ├── changed-source-file-iterator.ts │ │ │ │ │ ├── component-util.ts │ │ │ │ │ ├── find-best-match.ts │ │ │ │ │ ├── general-util.ts │ │ │ │ │ ├── get-position-context-in-document.ts │ │ │ │ │ ├── is-valid-name.ts │ │ │ │ │ ├── iterable-util.ts │ │ │ │ │ ├── map-util.ts │ │ │ │ │ ├── range-util.ts │ │ │ │ │ ├── rule-diagnostic-util.ts │ │ │ │ │ ├── rule-fix-util.ts │ │ │ │ │ ├── str-util.ts │ │ │ │ │ └── type-util.ts │ │ │ ├── cli │ │ │ │ ├── analyze-command.ts │ │ │ │ ├── analyze-globs.ts │ │ │ │ ├── cli.ts │ │ │ │ ├── compile.ts │ │ │ │ ├── format │ │ │ │ │ ├── code-diagnostic-formatter.ts │ │ │ │ │ ├── diagnostic-formatter.ts │ │ │ │ │ ├── list-diagnostic-formatter.ts │ │ │ │ │ ├── markdown-formatter.ts │ │ │ │ │ ├── markdown-util.ts │ │ │ │ │ └── util.ts │ │ │ │ ├── lit-analyzer-cli-config.ts │ │ │ │ ├── parse-cli-arguments.ts │ │ │ │ └── util.ts │ │ │ └── rules │ │ │ │ ├── all-rules.ts │ │ │ │ ├── no-boolean-in-attribute-binding.ts │ │ │ │ ├── no-complex-attribute-binding.ts │ │ │ │ ├── no-expressionless-property-binding.ts │ │ │ │ ├── no-incompatible-property-type.ts │ │ │ │ ├── no-incompatible-type-binding.ts │ │ │ │ ├── no-invalid-attribute-name.ts │ │ │ │ ├── no-invalid-directive-binding.ts │ │ │ │ ├── no-invalid-tag-name.ts │ │ │ │ ├── no-legacy-attribute.ts │ │ │ │ ├── no-missing-element-type-definition.ts │ │ │ │ ├── no-missing-import.ts │ │ │ │ ├── no-noncallable-event-binding.ts │ │ │ │ ├── no-nullable-attribute-binding.ts │ │ │ │ ├── no-property-visibility-mismatch.ts │ │ │ │ ├── no-unclosed-tag.ts │ │ │ │ ├── no-unintended-mixed-binding.ts │ │ │ │ ├── no-unknown-attribute.ts │ │ │ │ ├── no-unknown-event.ts │ │ │ │ ├── no-unknown-property.ts │ │ │ │ ├── no-unknown-slot.ts │ │ │ │ ├── no-unknown-tag-name.ts │ │ │ │ └── util │ │ │ │ ├── directive │ │ │ │ ├── get-directive.ts │ │ │ │ └── is-lit-directive.ts │ │ │ │ └── type │ │ │ │ ├── extract-binding-types.ts │ │ │ │ ├── is-assignable-binding-under-security-system.ts │ │ │ │ ├── is-assignable-in-attribute-binding.ts │ │ │ │ ├── is-assignable-in-boolean-binding.ts │ │ │ │ ├── is-assignable-in-element-binding.ts │ │ │ │ ├── is-assignable-in-property-binding.ts │ │ │ │ ├── is-assignable-to-type.ts │ │ │ │ └── remove-undefined-from-type.ts │ │ ├── scripts │ │ │ └── check-version.ts │ │ └── test │ │ │ ├── helpers │ │ │ ├── analyze.ts │ │ │ ├── assert.ts │ │ │ ├── compile-files.ts │ │ │ ├── generate-test-file.ts │ │ │ ├── parse-html.ts │ │ │ └── ts-test.ts │ │ │ ├── indexer │ │ │ └── index-entries.ts │ │ │ ├── parser │ │ │ ├── css-document │ │ │ │ └── css-substitutions.ts │ │ │ ├── dependencies │ │ │ │ └── parse-dependencies.ts │ │ │ └── html-document │ │ │ │ └── parse-bindings.ts │ │ │ └── rules │ │ │ ├── no-boolean-in-attribute-binding.ts │ │ │ ├── no-complex-attribute-binding.ts │ │ │ ├── no-incompatible-property-type.ts │ │ │ ├── no-incompatible-type-binding.ts │ │ │ ├── no-invalid-boolean-binding.ts │ │ │ ├── no-invalid-directive-binding.ts │ │ │ ├── no-legacy-attribute.ts │ │ │ ├── no-missing-element-type-definition.ts │ │ │ ├── no-missing-import.ts │ │ │ ├── no-noncallable-event-binding.ts │ │ │ ├── no-nullable-attribute-binding.ts │ │ │ ├── no-property-visibility-mismatch.ts │ │ │ ├── no-unclosed-tag.ts │ │ │ ├── no-unintended-mixed-binding.ts │ │ │ ├── no-unknown-attribute.ts │ │ │ ├── no-unknown-event.ts │ │ │ ├── no-unknown-property.ts │ │ │ ├── no-unknown-slot.ts │ │ │ ├── no-unknown-tag-name.ts │ │ │ └── security-system.ts │ └── tsconfig.json ├── ts-lit-plugin │ ├── README.md │ ├── index.js │ ├── package-lock.json │ ├── package.json │ ├── readme.blueprint.md │ ├── readme.config.json │ ├── readme │ │ ├── config.md │ │ ├── header.md │ │ └── install.md │ ├── src │ │ ├── bazel-plugin.ts │ │ ├── decorate-language-service.ts │ │ ├── index.ts │ │ ├── logger.ts │ │ ├── ts-lit-plugin │ │ │ ├── lit-plugin-context.ts │ │ │ ├── translate │ │ │ │ ├── translate-code-fixes.ts │ │ │ │ ├── translate-completion-details.ts │ │ │ │ ├── translate-completions.ts │ │ │ │ ├── translate-definition.ts │ │ │ │ ├── translate-diagnostics.ts │ │ │ │ ├── translate-format-edits.ts │ │ │ │ ├── translate-outlining-spans.ts │ │ │ │ ├── translate-quick-info.ts │ │ │ │ ├── translate-range.ts │ │ │ │ ├── translate-rename-info.ts │ │ │ │ ├── translate-rename-locations.ts │ │ │ │ └── translate-target-kind.ts │ │ │ └── ts-lit-plugin.ts │ │ └── ts-module.ts │ └── tsconfig.json └── vscode-lit-plugin │ ├── .vscode │ ├── launch.json │ ├── settings.json │ └── tasks.json │ ├── CHANGELOG.md │ ├── LICENSE.md │ ├── README.md │ ├── copy-to-built.js │ ├── docs │ └── assets │ │ ├── lit-plugin.gif │ │ ├── lit-plugin@128w.png │ │ └── lit-plugin@256w.png │ ├── esbuild.script.mjs │ ├── package-lock.json │ ├── package.json │ ├── readme.blueprint.md │ ├── readme.config.json │ ├── readme │ ├── config.md │ ├── feature-comparison.md │ ├── header.md │ ├── how.md │ ├── install.md │ └── other.md │ ├── schemas │ └── tsconfig.schema.json │ ├── src │ ├── color-provider.ts │ ├── extension.ts │ └── test │ │ ├── fixtures │ │ ├── completions.ts │ │ ├── missing-elem-type.ts │ │ ├── missing-import.ts │ │ ├── my-defined-element.ts │ │ ├── my-other-element.ts │ │ └── tsconfig.json │ │ ├── scripts │ │ ├── mocha-driver.ts │ │ └── test-runner.ts │ │ └── simple-test.ts │ ├── syntaxes │ ├── vscode-lit-html │ │ ├── LICENSE │ │ ├── README.md │ │ ├── lit-html-string-injection.json │ │ ├── lit-html-style-injection.json │ │ ├── lit-html-svg.json │ │ └── lit-html.json │ └── vscode-styled-components │ │ ├── LICENSE │ │ ├── README.md │ │ ├── css.styled.json │ │ └── styled-components.json │ └── tsconfig.json ├── prettier.config.js ├── readme.blueprint.md ├── readme.config.json └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_size = tab 6 | indent_style = tab 7 | tab_width = 2 8 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | out 3 | dist 4 | dev 5 | 6 | /packages/*/lib 7 | /packages/*/out 8 | /packages/*/scripts 9 | /packages/*/test 10 | /packages/*/index.* 11 | /packages/vscode-lit-plugin/built 12 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true 5 | }, 6 | "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"], 7 | "parser": "@typescript-eslint/parser", 8 | "plugins": ["@typescript-eslint", "import"], 9 | "rules": { 10 | "no-console": "error", 11 | "prefer-rest-params": "off", 12 | "@typescript-eslint/no-this-alias": "off", 13 | "@typescript-eslint/no-empty-function": "off", 14 | "@typescript-eslint/no-use-before-define": "off", 15 | "@typescript-eslint/explicit-function-return-type": "off", 16 | "@typescript-eslint/no-object-literal-type-assertion": "off", 17 | "@typescript-eslint/explicit-member-accessibility": "off", 18 | "@typescript-eslint/no-parameter-properties": "off", 19 | "@typescript-eslint/no-var-requires": "off", 20 | "@typescript-eslint/interface-name-prefix": "off", 21 | "@typescript-eslint/no-unused-vars": "off", 22 | "@typescript-eslint/ban-types": "off", 23 | "@typescript-eslint/no-non-null-assertion": "off", 24 | "@typescript-eslint/prefer-interface": "off", 25 | "@typescript-eslint/no-empty-interface": "off", 26 | "no-dupe-class-members": "off", 27 | "import/extensions": ["error", "always"] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/test_vscode.yml: -------------------------------------------------------------------------------- 1 | name: VSCode Integration Test 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | test: 10 | strategy: 11 | matrix: 12 | os: [macos-latest, ubuntu-latest, windows-latest] 13 | runs-on: ${{ matrix.os }} 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v3 17 | - name: Install Node.js 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: 16.x 21 | - run: npm ci 22 | # Test normally on Mac 23 | - run: npm run test:packaged 24 | working-directory: packages/vscode-lit-plugin 25 | if: runner.os == 'macOS' 26 | # Run with xvfb on Linux so that the vscode window has an X to render 27 | # into. 28 | - run: xvfb-run -a npm run test:packaged 29 | working-directory: packages/vscode-lit-plugin 30 | if: runner.os == 'Linux' 31 | # The test packaging doesn't work on Windows just because we'd need 32 | # to use different commands to expand the vsix file back out, so just 33 | # run the dev test and package up the vsix file. 34 | - run: npm run package 35 | working-directory: packages/vscode-lit-plugin 36 | if: runner.os == 'Windows' 37 | - run: npm run test:normal 38 | working-directory: packages/vscode-lit-plugin 39 | if: runner.os == 'Windows' 40 | -------------------------------------------------------------------------------- /.github/workflows/workflow.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | test: 11 | name: Run 12 | 13 | runs-on: ${{ matrix.os }} 14 | 15 | strategy: 16 | matrix: 17 | os: [windows-latest, ubuntu-latest] 18 | node: [18, 20] 19 | exclude: 20 | - os: windows-latest 21 | node: "17" 22 | 23 | steps: 24 | - name: Checkout code 25 | uses: actions/checkout@v3 26 | 27 | - name: Setup Node 28 | uses: actions/setup-node@v3 29 | with: 30 | node-version: ${{ matrix.node }} 31 | 32 | # npm@9 doesn't support Node 17. 33 | - name: Install npm@8 34 | run: npm i -g npm@8 35 | 36 | - name: Install 37 | run: npm ci 38 | 39 | - name: Lint 40 | if: matrix.os == 'ubuntu-latest' 41 | run: npm run lint 42 | 43 | - name: test 44 | run: npm run test:headless 45 | 46 | - name: Package vscode plugin 47 | if: matrix.os == 'ubuntu-latest' 48 | run: npm run package && mv packages/vscode-lit-plugin/out/packaged.vsix vscode-lit-plugin.vsix 49 | 50 | - name: Upload artifacts 51 | uses: actions/upload-artifact@master 52 | if: matrix.os == 'ubuntu-latest' && matrix.node == 16 53 | with: 54 | name: vscode-lit-plugin.vsix 55 | path: vscode-lit-plugin.vsix 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | .vscode-test/ 4 | *.txt 5 | *.tgz 6 | *.log 7 | todo.md 8 | .DS_Store 9 | dist 10 | out 11 | *.vsix 12 | .wireit/ 13 | .tsbuildinfo 14 | 15 | /packages/*/lib 16 | /packages/*/out 17 | /packages/*/scripts 18 | /packages/*/test 19 | /packages/*/index.* 20 | 21 | /packages/vscode-lit-plugin/built 22 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .idea 3 | .editorconfig 4 | node_modules 5 | *.txt 6 | *.tgz 7 | *.log 8 | .DS_Store 9 | dist 10 | out 11 | *.vsix 12 | *.gif 13 | *.png 14 | *.log 15 | .eslintignore 16 | .gitignore 17 | .prettierignore 18 | .vscodeignore 19 | 20 | /packages/*/lib 21 | /packages/*/out 22 | /packages/*/scripts 23 | /packages/*/test 24 | /packages/*/index.* 25 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Extension Tests", 6 | "type": "extensionHost", 7 | "request": "launch", 8 | "runtimeExecutable": "${execPath}", 9 | "args": [ 10 | "--extensionDevelopmentPath=${workspaceFolder}/packages/vscode-lit-plugin", 11 | "--extensionTestsPath=${workspaceFolder}/packages/vscode-lit-plugin/out/test/scripts/mocha-driver.js" 12 | ], 13 | "outFiles": ["${workspaceFolder}/packages/vscode-lit-plugin/out/test/**/*.js"] 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "editor.wordWrapColumn": 100, 4 | "editor.rulers": [ 5 | { 6 | "column": 100 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Hi there, I really appreciate you considering contributing to this repository! This readme hopefully contains what you need to get started. If you have any questions please open an issue or PM me on twitter [@RuneMehlsen](https://twitter.com/RuneMehlsen). 4 | 5 | 1. Clone the monorepo: `git clone https://github.com/runem/lit-analyzer.git` 6 | 2. Install dependencies: `npm ci` 7 | 3. Run tests: `npm test` 8 | 9 | ## Contributing to readmes 10 | 11 | Readme's are built because a lot of information is repeated in individual readmes. If you want to change something in a readme, please change files in [/docs/readme](/docs/readme), [/packages/lit-analyzer/readme](/packages/lit-analyzer/readme), [/packages/ts-lit-plugin/readme](/packages/ts-lit-plugin/readme), [/packages/vscode-lit-plugin/readme](/packages/vscode-lit-plugin/readme). Never change the README.md directly because it will be overwritten. 12 | 13 | Please run `npm run readme` when you want to rebuild all readme files. 14 | 15 | ## Contributing to lit-analyzer or ts-lit-plugin 16 | 17 | ### Debugging the CLI 18 | 19 | You can always try out the CLI by running `./cli.js path-to-a-file.js` from `packages/lit-analyzer`. 20 | 21 | ### Debugging the language service 22 | 23 | You can try out changes to lit-analyzer and/or ts-lit-plugin directly from the Typescript Language Service in VS Code: 24 | 25 | 1. Run `npm run dev` from `/` to open a playground in VS Code (lit-plugin is disabled in that session to prevent interference). 26 | 2. Run `npm run dev:logs` from `/` to watch logs in real time. 27 | 28 | ### `npm run watch` / `npm run build` 29 | 30 | You can run either `npm run watch` or `npm run build` from the repository root or from any subpackage. 31 | 32 | ## Contributing to vscode-lit-plugin 33 | 34 | ### Debugging 35 | 36 | In order to debug `vscode-lit-plugin` you can open vscode from `packages/vscode-lit-plugin` and press the **start debugging** button in vscode. 37 | 38 | ### `npm run package` 39 | 40 | You can use this script if you want to generate an installable package of vscode-lit-plugin. Afterwards, run `code --install-extension ./packages/vscode-lit-plugin/out/packaged.vsix` to install it. 41 | 42 | ### Syntaxes 43 | 44 | All syntaxes come from [vscode-lit-html](https://github.com/mjbvz/vscode-lit-html) and [vscode-styled-components](https://github.com/styled-components/vscode-styled-components). Because these repositories are not published as npm-packages, they are instead installed from Github URLs. Therefore, as of now, changes to syntaxes must be upstreamed to one of these repositories. 45 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2018 Rune Mehlsen runemehlsen@gmail.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE 10 | -------------------------------------------------------------------------------- /dev/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsserver.log": "normal", 3 | "typescript.tsserver.trace": "messages", 4 | "typescript.tsdk": "node_modules/typescript/lib" 5 | } -------------------------------------------------------------------------------- /dev/html.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/Microsoft/vscode-html-languageservice/master/docs/customData.schema.json", 3 | "version": 1, 4 | "tags": [ 5 | { 6 | "name": "my-extra-button", 7 | "description": "My extra button", 8 | "attributes": [ 9 | { 10 | "name": "type", 11 | "description": "My button type", 12 | "values": [ 13 | { 14 | "name": "alert" 15 | } 16 | ] 17 | } 18 | ] 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /dev/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dev", 3 | "version": "1.0.0", 4 | "description": "", 5 | "dependencies": { 6 | "lit-element": "^2.3.1", 7 | "lit-html": "^1.2.1", 8 | "ts-lit-plugin": "file:../packages/ts-lit-plugin" 9 | }, 10 | "devDependencies": { 11 | "typescript": "~4.4.3" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "MIT" 16 | } 17 | -------------------------------------------------------------------------------- /dev/src/my-element-1.ts: -------------------------------------------------------------------------------- 1 | import { customElement, html, LitElement, property, internalProperty } from "lit-element"; 2 | import "./my-element-2"; 3 | 4 | @customElement("my-element") 5 | export class MyElement extends LitElement { 6 | @property({ attribute: "hell>o" }) test: number | undefined; 7 | 8 | @property({ type: Date }) test2: number | undefined; 9 | 10 | @internalProperty() internal: number | undefined; 11 | 12 | static get observedAttributes() { 13 | return ["this is a test", "testing"]; 14 | } 15 | 16 | render() { 17 | return html` 18 | 19 | 20 | 21 | 22 |
23 |
24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 | `; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /dev/src/my-element-2.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @slot - Unnamed slot 3 | * @slot right - Right slot 4 | * @slot left - Right slot 5 | */ 6 | export class MyElement2 extends HTMLElement { 7 | constructor() { 8 | super(); 9 | 10 | this.foo = "foo"; 11 | } 12 | } 13 | 14 | customElements.define("my-element2", MyElement2); 15 | -------------------------------------------------------------------------------- /dev/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "plugins": [ 4 | { 5 | "name": "ts-lit-plugin", 6 | "logging": "verbose", 7 | "strict": true, 8 | "cssTemplateTags": ["css", "scss", "sass"], 9 | "htmlTemplateTags": ["html", "md"], 10 | "globalTags": ["my-external-tag"], 11 | "globalAttributes": ["my-attribute"], 12 | "globalEvents": ["my-event"], 13 | "customHtmlData": { 14 | "version": 1, 15 | "tags": [ 16 | { 17 | "name": "my-tsconfig-element", 18 | "description": "Tag name from tsconfig", 19 | "attributes": [ 20 | { 21 | "name": "size", 22 | "description": "Attribute from tsconfig", 23 | "values": [ 24 | { 25 | "name": "small" 26 | }, 27 | { 28 | "name": "medium" 29 | }, 30 | { 31 | "name": "large" 32 | } 33 | ] 34 | } 35 | ] 36 | } 37 | ], 38 | "globalAttributes": [ 39 | { 40 | "name": "globalattribute", 41 | "description": "Global attribute from tsconfig" 42 | } 43 | ] 44 | } 45 | } 46 | ], 47 | "experimentalDecorators": true, 48 | "target": "es5", 49 | "module": "commonjs", 50 | "allowJs": true, 51 | "lib": ["esnext", "dom"], 52 | "strict": true, 53 | "esModuleInterop": true, 54 | "noEmit": true 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /docs/readme/config-table.md: -------------------------------------------------------------------------------- 1 | 2 | | Option | Description | Type | Default | 3 | | :----- | ----------- | ---- | ------- | 4 | | `strict` | Enabling strict mode will change which rules are applied as default (see list of [rules](https://github.com/runem/lit-analyzer/blob/master/docs/readme/rules.md)) | `boolean` | false | 5 | | `rules` | Enable/disable individual rules or set their severity. Example: `{"no-unknown-tag-name": "off"}` | `{"rule-name": "off" \| "warn" \| "error"}` | The default rules enabled depend on the `strict` option | 6 | | `disable` | Completely disable this plugin. | `boolean` | false | 7 | | `dontShowSuggestions` | This option sets strict as | `boolean` | false | 8 | | `htmlTemplateTags` | List of template tags to enable html support in. | `string[]` | ["html", "raw"] | | 9 | | `cssTemplateTags` | This option sets strict as | `string[]` | ["css"] | 10 | | `globalTags` | List of html tag names that you expect to be present at all times. | `string[]` | | 11 | | `globalAttributes` | List of html attributes names that you expect to be present at all times. | `string[]` | | 12 | | `globalEvents` | List of event names that you expect to be present at all times | `string[]` | | 13 | | `customHtmlData` | This plugin supports the [custom vscode html data format](https://code.visualstudio.com/updates/v1_31#_html-and-css-custom-data-support) through this setting. | [Vscode Custom HTML Data Format](https://github.com/Microsoft/vscode-html-languageservice/blob/master/docs/customData.md). Supports arrays, objects and relative file paths | | 14 | | `maxProjectImportDepth` | Determines how many modules deep dependencies are followed to determine whether a custom element is available in the current file. When `-1` is used, dependencies will be followed infinitely deep. | `number` | `-1` | 15 | | `maxNodeModuleImportDepth` | Determines how many modules deep dependencies in __npm packages__ are followed to determine whether a custom element is available in the current file. When `-1` is used, dependencies in __npm packages__ will be followed infinitely deep.| `number` | `1` | 16 | -------------------------------------------------------------------------------- /docs/readme/jsdoc.md: -------------------------------------------------------------------------------- 1 | ## Documenting slots, events, attributes and properties 2 | 3 | Code is analyzed using [web-component-analyzer](https://github.com/runem/web-component-analyzer) in order to find properties, attributes and events. Unfortunately, sometimes it's not possible to analyze these things by looking at the code, and you will have to document how your component looks using `jsdoc`like this: 4 | 5 | 6 | ```js 7 | /** 8 | * This is my element 9 | * @attr size 10 | * @attr {red|blue} color - The color of my element 11 | * @prop {String} value 12 | * @prop {Boolean} myProp - This is my property 13 | * @fires change 14 | * @fires my-event - This is my own event 15 | * @slot - This is a comment for the unnamed slot 16 | * @slot right - Right content 17 | * @slot left 18 | * @cssprop {Color} --border-color 19 | * @csspart header 20 | */ 21 | class MyElement extends HTMLElement { 22 | } 23 | 24 | customElements.define("my-element", MyElement); 25 | ``` 26 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.2.1", 3 | "packages": ["packages/*"], 4 | "nohoist": ["vscode", "vscode-styled-components", "lit-html", "typescript"] 5 | } 6 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": [ 3 | "./packages/lit-analyzer/lib/", 4 | "./packages/lit-analyzer/package.json", 5 | "./packages/ts-lit-plugin/lib/", 6 | "./packages/ts-lit-plugin/package.json" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /packages/lit-analyzer/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require("./index.js") 4 | .cli() 5 | // eslint-disable-next-line no-console 6 | .catch(console.log); 7 | -------------------------------------------------------------------------------- /packages/lit-analyzer/readme.blueprint.md: -------------------------------------------------------------------------------- 1 | {{ template:title }} 2 | {{ template:description }} 3 | {{ template:badges }} 4 | 5 | {{ load:./readme/header.md }} 6 | 7 | {{ load:./readme/install.md }} 8 | {{ load:./readme/usage.md }} 9 | {{ load:./readme/config.md }} 10 | {{ load:./../../docs/readme/rules.md }} 11 | {{ load:./../../docs/readme/jsdoc.md }} 12 | 13 | {{ template:contributors }} 14 | {{ template:license }} 15 | -------------------------------------------------------------------------------- /packages/lit-analyzer/readme.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "line": "rainbow", 3 | "ids": { 4 | "github": "runem/lit-analyzer", 5 | "npm": "lit-analyzer" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/lit-analyzer/readme/config.md: -------------------------------------------------------------------------------- 1 | ## Configuration 2 | 3 | You can configure the CLI with arguments: 4 | 5 | 6 | ```bash 7 | lit-analyzer --strict --rules.no-unknown-tag-name off --format markdown 8 | ``` 9 | 10 | **Note:** You can also configure the CLI using a `tsconfig.json` file (see [ts-lit-plugin](https://github.com/runem/lit-analyzer/blob/master/packages/ts-lit-plugin)). 11 | 12 | ### Available arguments 13 | 14 | 15 | | Option | Description | Type | Default | 16 | | :----- | ----------- | ---- | ------- | 17 | | `--help` | Print help message | `boolean` | | 18 | | `--rules.rule-name` | Enable or disable rules (example: --rules.no-unknown-tag-name off). Severity can be "off" \| "warn" \| "error". See a list of rules [here](https://github.com/runem/lit-analyzer/blob/master/docs/readme/rules.md). | `{"rule-name": "off" \| "warn" \| "error"}` | | 19 | | `--strict` | Enable strict mode. This changes the default ruleset | `boolean` | | 20 | | `--format` | Change the format of how diagnostics are reported | `code` \| `list` \| `markdown` | code | 21 | | `--maxWarnings` | Fail only when the number of warnings is larger than this number | `number` | -1 | 22 | | `--outFile` | Emit all output to a single file | `filePath` | | 23 | | `--quiet` | Report only errors and not warnings | `boolean` | | 24 | | `--failFast` | Exit the process right after the first problem has been found | `boolean` | | 25 | | `--debug` | Enable CLI debug mode | `boolean` | | 26 | -------------------------------------------------------------------------------- /packages/lit-analyzer/readme/header.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runem/lit-analyzer/b0e79a9b369dd48203e846e7c2796c8aa0a371dd/packages/lit-analyzer/readme/header.md -------------------------------------------------------------------------------- /packages/lit-analyzer/readme/install.md: -------------------------------------------------------------------------------- 1 | ## Installation 2 | 3 | 4 | ```bash 5 | npm install lit-analyzer -g 6 | ``` 7 | 8 | **Note:** 9 | 10 | - If you use Visual Studio Code you can also install the [lit-plugin](https://marketplace.visualstudio.com/items?itemName=runem.lit-plugin) extension. 11 | - If you use Typescript you can also install [ts-lit-plugin](https://github.com/runem/lit-analyzer/blob/master/packages/ts-lit-plugin). 12 | -------------------------------------------------------------------------------- /packages/lit-analyzer/readme/usage.md: -------------------------------------------------------------------------------- 1 | ## Usage 2 | 3 | `lit-analyzer` analyzes an optional `input glob` and emits the output to the console as default. When the `input glob` is omitted it will analyze all components in `src`. 4 | 5 | 6 | ```bash 7 | lit-analyzer src 8 | lit-analyzer "src/**/*.{js,ts}" 9 | lit-analyzer my-element.js 10 | lit-analyzer --format markdown --outFile result.md 11 | ``` 12 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./lib/analyze/lit-analyzer.js"; 2 | export * from "./lib/analyze/constants.js"; 3 | export * from "./lib/analyze/lit-analyzer-config.js"; 4 | export * from "./lib/analyze/lit-analyzer-context.js"; 5 | export * from "./lib/analyze/lit-analyzer-logger.js"; 6 | export * from "./lib/analyze/default-lit-analyzer-context.js"; 7 | 8 | export * from "./lib/analyze/types/range.js"; 9 | export * from "./lib/analyze/types/lit-closing-tag-info.js"; 10 | export * from "./lib/analyze/types/lit-code-fix.js"; 11 | export * from "./lib/analyze/types/lit-code-fix-action.js"; 12 | export * from "./lib/analyze/types/lit-completion.js"; 13 | export * from "./lib/analyze/types/lit-completion-details.js"; 14 | export * from "./lib/analyze/types/lit-definition.js"; 15 | export * from "./lib/analyze/types/lit-diagnostic.js"; 16 | export * from "./lib/analyze/types/lit-format-edit.js"; 17 | export * from "./lib/analyze/types/lit-outlining-span.js"; 18 | export * from "./lib/analyze/types/lit-quick-info.js"; 19 | export * from "./lib/analyze/types/lit-quick-info.js"; 20 | export * from "./lib/analyze/types/lit-rename-info.js"; 21 | export * from "./lib/analyze/types/lit-rename-location.js"; 22 | export * from "./lib/analyze/types/lit-target-kind.js"; 23 | 24 | export * from "./lib/cli/cli.js"; 25 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/component-analyzer/component-analyzer.ts: -------------------------------------------------------------------------------- 1 | import { ComponentDeclaration, ComponentDefinition } from "web-component-analyzer"; 2 | import { LitAnalyzerContext } from "../lit-analyzer-context.js"; 3 | import { ReportedRuleDiagnostic } from "../rule-collection.js"; 4 | import { LitCodeFix } from "../types/lit-code-fix.js"; 5 | import { LitDiagnostic } from "../types/lit-diagnostic.js"; 6 | import { SourceFileRange } from "../types/range.js"; 7 | import { arrayDefined, arrayFlat } from "../util/array-util.js"; 8 | import { intersects } from "../util/range-util.js"; 9 | import { convertRuleDiagnosticToLitDiagnostic } from "../util/rule-diagnostic-util.js"; 10 | import { converRuleFixToLitCodeFix } from "../util/rule-fix-util.js"; 11 | 12 | export class ComponentAnalyzer { 13 | getDiagnostics(definitionOrDeclaration: ComponentDefinition | ComponentDeclaration, context: LitAnalyzerContext): LitDiagnostic[] { 14 | return this.getRuleDiagnostics(definitionOrDeclaration, context).map(d => convertRuleDiagnosticToLitDiagnostic(d, context)); 15 | } 16 | 17 | getCodeFixesAtOffsetRange( 18 | definitionOrDeclaration: ComponentDefinition | ComponentDeclaration, 19 | range: SourceFileRange, 20 | context: LitAnalyzerContext 21 | ): LitCodeFix[] { 22 | return arrayFlat( 23 | arrayDefined( 24 | this.getRuleDiagnostics(definitionOrDeclaration, context) 25 | .filter(({ diagnostic }) => intersects(range, diagnostic.location)) 26 | .map(({ diagnostic }) => diagnostic.fix?.()) 27 | ) 28 | ).map(ruleFix => converRuleFixToLitCodeFix(ruleFix)); 29 | } 30 | 31 | private getRuleDiagnostics( 32 | definitionOrDeclaration: ComponentDefinition | ComponentDeclaration, 33 | context: LitAnalyzerContext 34 | ): ReportedRuleDiagnostic[] { 35 | if ("tagName" in definitionOrDeclaration) { 36 | return context.rules.getDiagnosticsFromDefinition(definitionOrDeclaration, context); 37 | } else { 38 | return context.rules.getDiagnosticsFromDeclaration(definitionOrDeclaration, context); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/constants.ts: -------------------------------------------------------------------------------- 1 | export type LitHtmlAttributeModifier = "." | "?" | "@"; 2 | 3 | export const LIT_HTML_PROP_ATTRIBUTE_MODIFIER = "."; 4 | 5 | export const LIT_HTML_BOOLEAN_ATTRIBUTE_MODIFIER = "?"; 6 | 7 | export const LIT_HTML_EVENT_LISTENER_ATTRIBUTE_MODIFIER = "@"; 8 | 9 | export const LIT_HTML_ATTRIBUTE_MODIFIERS: LitHtmlAttributeModifier[] = [ 10 | LIT_HTML_PROP_ATTRIBUTE_MODIFIER, 11 | LIT_HTML_BOOLEAN_ATTRIBUTE_MODIFIER, 12 | LIT_HTML_EVENT_LISTENER_ATTRIBUTE_MODIFIER 13 | ]; 14 | 15 | export const DIAGNOSTIC_SOURCE = "lit-plugin"; 16 | 17 | export const TS_IGNORE_FLAG = "@ts-ignore"; 18 | 19 | export const VERSION = "2.0.3"; 20 | 21 | export const MAX_RUNNING_TIME_PER_OPERATION = 150; // Default to small timeouts. Opt in to larger timeouts where necessary. 22 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/document-analyzer/html/code-fix/code-fixes-for-html-document.ts: -------------------------------------------------------------------------------- 1 | import { LitAnalyzerContext } from "../../../lit-analyzer-context.js"; 2 | import { HtmlDocument } from "../../../parse/document/text-document/html-document/html-document.js"; 3 | import { LitCodeFix } from "../../../types/lit-code-fix.js"; 4 | import { DocumentRange } from "../../../types/range.js"; 5 | import { arrayDefined, arrayFlat } from "../../../util/array-util.js"; 6 | import { documentRangeToSFRange, intersects } from "../../../util/range-util.js"; 7 | import { converRuleFixToLitCodeFix } from "../../../util/rule-fix-util.js"; 8 | 9 | export function codeFixesForHtmlDocument(htmlDocument: HtmlDocument, range: DocumentRange, context: LitAnalyzerContext): LitCodeFix[] { 10 | return arrayFlat( 11 | arrayDefined( 12 | context.rules 13 | .getDiagnosticsFromDocument(htmlDocument, context) 14 | .filter(({ diagnostic }) => intersects(documentRangeToSFRange(htmlDocument, range), diagnostic.location)) 15 | .map(({ diagnostic }) => diagnostic.fix?.()) 16 | ) 17 | ).map(ruleFix => converRuleFixToLitCodeFix(ruleFix)); 18 | } 19 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/document-analyzer/html/completion/completions-at-offset.ts: -------------------------------------------------------------------------------- 1 | import { LitAnalyzerContext } from "../../../lit-analyzer-context.js"; 2 | import { HtmlDocument } from "../../../parse/document/text-document/html-document/html-document.js"; 3 | import { LitCompletion } from "../../../types/lit-completion.js"; 4 | import { DocumentOffset } from "../../../types/range.js"; 5 | import { getPositionContextInDocument } from "../../../util/get-position-context-in-document.js"; 6 | import { rangeFromHtmlNodeAttr } from "../../../util/range-util.js"; 7 | import { completionsForHtmlAttrValues } from "./completions-for-html-attr-values.js"; 8 | import { completionsForHtmlAttrs } from "./completions-for-html-attrs.js"; 9 | import { completionsForHtmlNodes } from "./completions-for-html-nodes.js"; 10 | 11 | export function completionsAtOffset(document: HtmlDocument, offset: DocumentOffset, context: LitAnalyzerContext): LitCompletion[] { 12 | const positionContext = getPositionContextInDocument(document, offset); 13 | 14 | const { beforeWord } = positionContext; 15 | 16 | // Get possible intersecting html attribute or attribute area. 17 | const intersectingAttr = document.htmlAttrNameAtOffset(offset); 18 | const intersectingAttrAreaNode = document.htmlAttrAreaAtOffset(offset); 19 | const intersectingAttrAssignment = document.htmlAttrAssignmentAtOffset(offset); 20 | const intersectingClosestNode = document.htmlNodeClosestToOffset(offset); 21 | 22 | // Get entries from the extensions 23 | if (intersectingAttr != null) { 24 | const entries = completionsForHtmlAttrs(intersectingAttr.htmlNode, positionContext, context); 25 | 26 | // Make sure that every entry overwrites the entire attribute name. 27 | return entries.map(entry => ({ 28 | ...entry, 29 | range: rangeFromHtmlNodeAttr(intersectingAttr) 30 | })); 31 | } else if (intersectingAttrAssignment != null) { 32 | return completionsForHtmlAttrValues(intersectingAttrAssignment, positionContext, context); 33 | } else if (intersectingAttrAreaNode != null) { 34 | return completionsForHtmlAttrs(intersectingAttrAreaNode, positionContext, context); 35 | } else if (beforeWord === "<" || beforeWord === "/") { 36 | return completionsForHtmlNodes(document, intersectingClosestNode, positionContext, context); 37 | } 38 | 39 | return []; 40 | } 41 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/document-analyzer/html/completion/completions-for-html-attr-values.ts: -------------------------------------------------------------------------------- 1 | import { isSimpleTypeLiteral, SimpleType } from "ts-simple-type"; 2 | import { LitAnalyzerContext } from "../../../lit-analyzer-context.js"; 3 | import { HtmlNodeAttrAssignmentKind } from "../../../types/html-node/html-node-attr-assignment-types.js"; 4 | import { HtmlNodeAttr, HtmlNodeAttrKind } from "../../../types/html-node/html-node-attr-types.js"; 5 | import { LitCompletion } from "../../../types/lit-completion.js"; 6 | import { DocumentPositionContext } from "../../../util/get-position-context-in-document.js"; 7 | 8 | export function completionsForHtmlAttrValues( 9 | htmlNodeAttr: HtmlNodeAttr, 10 | location: DocumentPositionContext, 11 | { htmlStore }: LitAnalyzerContext 12 | ): LitCompletion[] { 13 | // There is not point in showing completions for event listener bindings 14 | if (htmlNodeAttr.kind === HtmlNodeAttrKind.EVENT_LISTENER) return []; 15 | 16 | // Don't show completions inside assignments with expressions 17 | if (htmlNodeAttr.assignment && htmlNodeAttr.assignment.kind === HtmlNodeAttrAssignmentKind.EXPRESSION) return []; 18 | 19 | const htmlTagMember = htmlStore.getHtmlAttrTarget(htmlNodeAttr); 20 | if (htmlTagMember == null) return []; 21 | 22 | // Special case for handling slot attr as we need to look at its parent 23 | if (htmlNodeAttr.name === "slot") { 24 | const parentHtmlTag = htmlNodeAttr.htmlNode.parent && htmlStore.getHtmlTag(htmlNodeAttr.htmlNode.parent); 25 | if (parentHtmlTag != null && parentHtmlTag.slots.length > 0) { 26 | return parentHtmlTag.slots.map( 27 | slot => 28 | ({ 29 | name: slot.name || " ", 30 | insert: slot.name || "", 31 | documentation: () => slot.description, 32 | kind: "enumElement" 33 | } as LitCompletion) 34 | ); 35 | } 36 | } 37 | 38 | const options = getOptionsFromType(htmlTagMember.getType()); 39 | 40 | return options.map( 41 | option => 42 | ({ 43 | name: option, 44 | insert: option, 45 | kind: "enumElement" 46 | } as LitCompletion) 47 | ); 48 | } 49 | 50 | function getOptionsFromType(type: SimpleType): string[] { 51 | switch (type.kind) { 52 | case "UNION": 53 | return type.types.filter(isSimpleTypeLiteral).map(t => t.value.toString()); 54 | case "ENUM": 55 | return type.types 56 | .map(m => m.type) 57 | .filter(isSimpleTypeLiteral) 58 | .map(t => t.value.toString()); 59 | case "ALIAS": 60 | return getOptionsFromType(type.target); 61 | } 62 | 63 | return []; 64 | } 65 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/document-analyzer/html/definition/definition-for-html-attr.ts: -------------------------------------------------------------------------------- 1 | import { LitAnalyzerContext } from "../../../lit-analyzer-context.js"; 2 | import { isHtmlEvent, isHtmlMember } from "../../../parse/parse-html-data/html-tag.js"; 3 | import { HtmlNodeAttr } from "../../../types/html-node/html-node-attr-types.js"; 4 | import { LitDefinition } from "../../../types/lit-definition.js"; 5 | import { getNodeIdentifier } from "../../../util/ast-util.js"; 6 | import { rangeFromHtmlNodeAttr } from "../../../util/range-util.js"; 7 | 8 | export function definitionForHtmlAttr(htmlAttr: HtmlNodeAttr, { htmlStore, ts }: LitAnalyzerContext): LitDefinition | undefined { 9 | const target = htmlStore.getHtmlAttrTarget(htmlAttr); 10 | if (target == null) return undefined; 11 | 12 | if (isHtmlMember(target) && target.declaration != null) { 13 | const node = target.declaration.node; 14 | 15 | return { 16 | fromRange: rangeFromHtmlNodeAttr(htmlAttr), 17 | targets: [ 18 | { 19 | kind: "node", 20 | node: getNodeIdentifier(node, ts) || node, 21 | name: target.name 22 | } 23 | ] 24 | }; 25 | } else if (isHtmlEvent(target) && target.declaration != null) { 26 | const node = target.declaration.node; 27 | 28 | return { 29 | fromRange: rangeFromHtmlNodeAttr(htmlAttr), 30 | targets: [ 31 | { 32 | kind: "node", 33 | node: getNodeIdentifier(node, ts) || node, 34 | name: target.name 35 | } 36 | ] 37 | }; 38 | } 39 | return; 40 | } 41 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/document-analyzer/html/definition/definition-for-html-node.ts: -------------------------------------------------------------------------------- 1 | import { LitAnalyzerContext } from "../../../lit-analyzer-context.js"; 2 | import { HtmlNode } from "../../../types/html-node/html-node-types.js"; 3 | import { LitDefinition } from "../../../types/lit-definition.js"; 4 | import { getNodeIdentifier } from "../../../util/ast-util.js"; 5 | import { rangeFromHtmlNode } from "../../../util/range-util.js"; 6 | 7 | export function definitionForHtmlNode(htmlNode: HtmlNode, { htmlStore, ts }: LitAnalyzerContext): LitDefinition | undefined { 8 | const tag = htmlStore.getHtmlTag(htmlNode); 9 | if (tag == null || tag.declaration == null) return undefined; 10 | 11 | const node = tag.declaration.node; 12 | 13 | return { 14 | fromRange: rangeFromHtmlNode(htmlNode), 15 | targets: [ 16 | { 17 | kind: "node", 18 | node: getNodeIdentifier(node, ts) || node 19 | } 20 | ] 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/document-analyzer/html/diagnostic/validate-html-document.ts: -------------------------------------------------------------------------------- 1 | import { LitAnalyzerContext } from "../../../lit-analyzer-context.js"; 2 | import { HtmlDocument } from "../../../parse/document/text-document/html-document/html-document.js"; 3 | import { LitDiagnostic } from "../../../types/lit-diagnostic.js"; 4 | import { convertRuleDiagnosticToLitDiagnostic } from "../../../util/rule-diagnostic-util.js"; 5 | 6 | export function validateHTMLDocument(htmlDocument: HtmlDocument, context: LitAnalyzerContext): LitDiagnostic[] { 7 | return context.rules.getDiagnosticsFromDocument(htmlDocument, context).map(d => convertRuleDiagnosticToLitDiagnostic(d, context)); 8 | } 9 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/document-analyzer/html/quick-info/quick-info-for-html-attr.ts: -------------------------------------------------------------------------------- 1 | import { LitAnalyzerContext } from "../../../lit-analyzer-context.js"; 2 | import { descriptionForTarget, targetKindAndTypeText } from "../../../parse/parse-html-data/html-tag.js"; 3 | import { HtmlNodeAttr } from "../../../types/html-node/html-node-attr-types.js"; 4 | import { LitQuickInfo } from "../../../types/lit-quick-info.js"; 5 | import { rangeFromHtmlNodeAttr } from "../../../util/range-util.js"; 6 | 7 | export function quickInfoForHtmlAttr(htmlAttr: HtmlNodeAttr, { htmlStore }: LitAnalyzerContext): LitQuickInfo | undefined { 8 | const target = htmlStore.getHtmlAttrTarget(htmlAttr); 9 | if (target == null) return undefined; 10 | 11 | return { 12 | range: rangeFromHtmlNodeAttr(htmlAttr), 13 | primaryInfo: targetKindAndTypeText(target, { modifier: htmlAttr.modifier }), 14 | secondaryInfo: descriptionForTarget(target, { markdown: true }) 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/document-analyzer/html/quick-info/quick-info-for-html-node.ts: -------------------------------------------------------------------------------- 1 | import { LitAnalyzerContext } from "../../../lit-analyzer-context.js"; 2 | import { documentationForHtmlTag } from "../../../parse/parse-html-data/html-tag.js"; 3 | import { HtmlNode } from "../../../types/html-node/html-node-types.js"; 4 | import { LitQuickInfo } from "../../../types/lit-quick-info.js"; 5 | import { rangeFromHtmlNode } from "../../../util/range-util.js"; 6 | 7 | export function quickInfoForHtmlNode(htmlNode: HtmlNode, { htmlStore }: LitAnalyzerContext): LitQuickInfo | undefined { 8 | const htmlTag = htmlStore.getHtmlTag(htmlNode); 9 | if (htmlTag == null) return undefined; 10 | 11 | return { 12 | range: rangeFromHtmlNode(htmlNode), 13 | primaryInfo: `<${htmlNode.tagName}>`, 14 | secondaryInfo: documentationForHtmlTag(htmlTag, { markdown: true }) 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/document-analyzer/html/rename-locations/rename-locations-at-offset.ts: -------------------------------------------------------------------------------- 1 | import { LitAnalyzerContext } from "../../../lit-analyzer-context.js"; 2 | import { HtmlDocument } from "../../../parse/document/text-document/html-document/html-document.js"; 3 | import { isHTMLNode } from "../../../types/html-node/html-node-types.js"; 4 | import { LitRenameLocation } from "../../../types/lit-rename-location.js"; 5 | import { DocumentOffset } from "../../../types/range.js"; 6 | import { renameLocationsForTagName } from "./rename-locations-for-tag-name.js"; 7 | 8 | export function renameLocationsAtOffset(document: HtmlDocument, offset: DocumentOffset, context: LitAnalyzerContext): LitRenameLocation[] { 9 | const hit = document.htmlNodeOrAttrAtOffset(offset); 10 | if (hit == null) return []; 11 | 12 | if (isHTMLNode(hit)) { 13 | return renameLocationsForTagName(hit.tagName, context); 14 | } 15 | 16 | return []; 17 | } 18 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/lit-analyzer-context.ts: -------------------------------------------------------------------------------- 1 | import * as tsMod from "typescript"; 2 | import { Program, SourceFile } from "typescript"; 3 | import * as tsServer from "typescript/lib/tsserverlibrary.js"; 4 | import { LitAnalyzerConfig } from "./lit-analyzer-config.js"; 5 | import { LitAnalyzerLogger } from "./lit-analyzer-logger.js"; 6 | import { RuleCollection } from "./rule-collection.js"; 7 | import { AnalyzerDefinitionStore } from "./store/analyzer-definition-store.js"; 8 | import { AnalyzerDependencyStore } from "./store/analyzer-dependency-store.js"; 9 | import { AnalyzerDocumentStore } from "./store/analyzer-document-store.js"; 10 | import { AnalyzerHtmlStore } from "./store/analyzer-html-store.js"; 11 | 12 | export interface LitAnalyzerContext { 13 | readonly ts: typeof tsMod; 14 | readonly program: Program; 15 | readonly project: tsServer.server.Project | undefined; 16 | readonly config: LitAnalyzerConfig; 17 | 18 | // Stores 19 | readonly htmlStore: AnalyzerHtmlStore; 20 | readonly dependencyStore: AnalyzerDependencyStore; 21 | readonly documentStore: AnalyzerDocumentStore; 22 | readonly definitionStore: AnalyzerDefinitionStore; 23 | 24 | readonly logger: LitAnalyzerLogger; 25 | readonly rules: RuleCollection; 26 | 27 | readonly currentFile: SourceFile; 28 | readonly currentRunningTime: number; 29 | readonly isCancellationRequested: boolean; 30 | 31 | updateConfig(config: LitAnalyzerConfig): void; 32 | updateDependencies(file: SourceFile): void; 33 | updateComponents(file: SourceFile): void; 34 | 35 | setContextBase(contextBase: LitAnalyzerContextBaseOptions): void; 36 | } 37 | 38 | export interface LitAnalyzerContextBaseOptions { 39 | file: SourceFile | undefined; 40 | timeout?: number; 41 | throwOnCancellation?: boolean; 42 | } 43 | 44 | export interface LitPluginContextHandler { 45 | ts?: typeof tsMod; 46 | getProgram(): Program; 47 | getProject?(): tsServer.server.Project; 48 | } 49 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/parse/document/text-document/css-document/css-document.ts: -------------------------------------------------------------------------------- 1 | import { VirtualAstCssDocument } from "../../virtual-document/virtual-css-document.js"; 2 | import { TextDocument } from "../text-document.js"; 3 | 4 | export class CssDocument extends TextDocument { 5 | constructor(virtualDocument: VirtualAstCssDocument) { 6 | super(virtualDocument); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/parse/document/text-document/html-document/parse-html-document.ts: -------------------------------------------------------------------------------- 1 | import { Expression, TaggedTemplateExpression } from "typescript"; 2 | import { DocumentRange } from "../../../../types/range.js"; 3 | import { VirtualAstHtmlDocument } from "../../virtual-document/virtual-html-document.js"; 4 | import { HtmlDocument } from "./html-document.js"; 5 | import { ParseHtmlContext } from "./parse-html-node/parse-html-context.js"; 6 | import { parseHtmlNodes } from "./parse-html-node/parse-html-node.js"; 7 | import { parseHtml } from "./parse-html-p5/parse-html.js"; 8 | 9 | export function parseHtmlDocuments(nodes: TaggedTemplateExpression[]): HtmlDocument[] { 10 | return nodes.map(parseHtmlDocument); 11 | } 12 | 13 | export function parseHtmlDocument(node: TaggedTemplateExpression): HtmlDocument { 14 | const virtualDocument = new VirtualAstHtmlDocument(node); 15 | const html = virtualDocument.text; 16 | const htmlAst = parseHtml(html); 17 | const document = new HtmlDocument(virtualDocument, []); 18 | 19 | const context: ParseHtmlContext = { 20 | html, 21 | document, 22 | getPartsAtOffsetRange(range: DocumentRange): (Expression | string)[] { 23 | return virtualDocument.getPartsAtDocumentRange(range); 24 | } 25 | }; 26 | 27 | document.rootNodes = parseHtmlNodes(htmlAst.childNodes, undefined, context); 28 | 29 | return document; 30 | } 31 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/parse/document/text-document/html-document/parse-html-node/parse-html-attr-context.ts: -------------------------------------------------------------------------------- 1 | import { HtmlNode } from "../../../../../types/html-node/html-node-types.js"; 2 | import { ParseHtmlContext } from "./parse-html-context.js"; 3 | 4 | export interface ParseHtmlAttrContext extends ParseHtmlContext { 5 | htmlNode: HtmlNode; 6 | } 7 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/parse/document/text-document/html-document/parse-html-node/parse-html-context.ts: -------------------------------------------------------------------------------- 1 | import { Expression } from "typescript"; 2 | import { Range } from "../../../../../types/range.js"; 3 | import { HtmlDocument } from "../html-document.js"; 4 | 5 | export interface ParseHtmlContext { 6 | html: string; 7 | document: HtmlDocument; 8 | getPartsAtOffsetRange(range: Range): (string | Expression)[]; 9 | } 10 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/parse/document/text-document/html-document/parse-html-p5/parse-html-types.ts: -------------------------------------------------------------------------------- 1 | export interface IP5BaseSourceCodeLocation { 2 | startLine: number; 3 | startCol: number; 4 | startOffset: number; 5 | endLine: number; 6 | endCol: number; 7 | endOffset: number; 8 | attrs?: Record; 9 | } 10 | 11 | export interface IP5NodeAttr { 12 | name: string; 13 | value: string; 14 | prefix?: string; 15 | } 16 | 17 | export interface IP5NodeBase { 18 | nodeName: string; 19 | namespaceURI: string; 20 | } 21 | 22 | export interface IP5DocumentFragmentNode extends IP5NodeBase { 23 | nodeName: "#document-fragment"; 24 | childNodes: P5Node[]; 25 | } 26 | 27 | export interface IP5TextNode extends IP5NodeBase { 28 | nodeName: "#text"; 29 | value: string; 30 | parentNode: P5Node; 31 | } 32 | 33 | export interface IP5CommentNode extends IP5NodeBase { 34 | nodeName: "#comment"; 35 | value: string; 36 | data?: string; 37 | parentNode: P5Node; 38 | } 39 | 40 | export interface IP5TagNode extends IP5NodeBase { 41 | nodeName: string; 42 | tagName: string; 43 | attrs: IP5NodeAttr[]; 44 | namespaceURI: string; 45 | childNodes?: []; 46 | parentNodes: P5Node; 47 | } 48 | 49 | export interface IP5NodeSourceLocation extends IP5BaseSourceCodeLocation { 50 | startTag: IP5BaseSourceCodeLocation; 51 | endTag?: IP5BaseSourceCodeLocation; 52 | } 53 | 54 | export type P5Node = IP5TextNode | IP5TagNode | IP5CommentNode; 55 | 56 | export function getSourceLocation(node: IP5TagNode): IP5NodeSourceLocation | null; 57 | export function getSourceLocation(node: P5Node): IP5BaseSourceCodeLocation | null; 58 | export function getSourceLocation(node: P5Node): IP5BaseSourceCodeLocation | null { 59 | interface NodeWithSourceLocations extends IP5NodeBase { 60 | sourceCodeLocation: IP5BaseSourceCodeLocation | null; 61 | __location: IP5BaseSourceCodeLocation | null; 62 | } 63 | const nodeWithLocation = node as unknown as NodeWithSourceLocations; 64 | return nodeWithLocation.sourceCodeLocation || nodeWithLocation.__location; 65 | } 66 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/parse/document/text-document/html-document/parse-html-p5/parse-html.ts: -------------------------------------------------------------------------------- 1 | import { IP5CommentNode, IP5DocumentFragmentNode, IP5NodeBase, IP5TagNode, IP5TextNode, P5Node } from "./parse-html-types.js"; 2 | 3 | const { parseFragment } = require("parse5"); 4 | 5 | /** 6 | * Returns if a p5Node is a tag node. 7 | * @param node 8 | */ 9 | export function isTagNode(node: P5Node): node is IP5TagNode { 10 | return !node.nodeName.includes("#"); 11 | } 12 | 13 | /** 14 | * Returns if a p5Node is a document fragment. 15 | * @param node 16 | */ 17 | export function isDocumentFragmentNode(node: IP5NodeBase): node is IP5DocumentFragmentNode { 18 | return node.nodeName === "#document-fragment"; 19 | } 20 | 21 | /** 22 | * Returns if a p5Node is a text node. 23 | * @param node 24 | */ 25 | export function isTextNode(node: P5Node): node is IP5TextNode { 26 | return node.nodeName === "#text"; 27 | } 28 | 29 | /** 30 | * Returns if a p5Node is a comment node. 31 | * @param node 32 | */ 33 | export function isCommentNode(node: P5Node): node is IP5CommentNode { 34 | return node.nodeName === "#comment"; 35 | } 36 | 37 | /** 38 | * Parse a html string into p5Nodes. 39 | * @param html 40 | */ 41 | export function parseHtml(html: string): IP5DocumentFragmentNode { 42 | return parseFragment(html, { sourceCodeLocationInfo: true, locationInfo: true }); 43 | } 44 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/parse/document/text-document/text-document.ts: -------------------------------------------------------------------------------- 1 | import { VirtualDocument } from "../virtual-document/virtual-document.js"; 2 | 3 | export class TextDocument { 4 | constructor(public virtualDocument: VirtualDocument) {} 5 | } 6 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/parse/document/virtual-document/virtual-css-document.ts: -------------------------------------------------------------------------------- 1 | import { Expression } from "typescript"; 2 | import { VirtualAstDocument } from "./virtual-ast-document.js"; 3 | 4 | export class VirtualAstCssDocument extends VirtualAstDocument { 5 | protected substituteExpression(length: number, expression: Expression, prev: string, next: string | undefined, _index: number): string { 6 | const hasLeftColon = prev.match(/:[^;{]*\${$/) != null; 7 | const hasRightColon = next != null && next.match(/^}\s*:\s+/) != null; 8 | const hasRightSemicolon = next != null && next.match(/^}\s*;/) != null; 9 | const hasRightPercentage = next != null && next.match(/^}%/) != null; 10 | 11 | // Inspired by https://github.com/Microsoft/typescript-styled-plugin/blob/909d4f17d61562fe77f24587ea443713b8da851d/src/_substituter.ts#L62 12 | // If this substitution contains both a property and a key, replace it with "$_:_" 13 | // Example: 14 | // div { 15 | // ${unsafeCSS("color: red)}; 16 | // } 17 | if (hasRightSemicolon && !hasLeftColon) { 18 | const prefix = "$_:_"; 19 | return `${prefix}${"_".repeat(Math.max(0, length - prefix.length))}`.slice(0, length); 20 | } 21 | 22 | // If there is "%" to the right of this substitution, replace with a number, because the parser expects a number unit 23 | // Example: 24 | // div { 25 | // transform-origin: ${x}% ${y}%; 26 | // } 27 | else if (hasRightPercentage) { 28 | return "0".repeat(length); 29 | } 30 | 31 | // If there is a ": " to the right of this substitution, replace it with an identifier 32 | // Example: 33 | // div { 34 | // ${unsafeCSS("color")}: red 35 | // } 36 | else if (hasRightColon) { 37 | return `$${"_".repeat(length - 1)}`; 38 | } 39 | 40 | // Else replace with an identifier "_" 41 | return "_".repeat(length); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/parse/document/virtual-document/virtual-document.ts: -------------------------------------------------------------------------------- 1 | import { Expression } from "typescript"; 2 | import { DocumentOffset, DocumentRange, Range, SourceFilePosition, SourceFileRange } from "../../../types/range.js"; 3 | 4 | export interface VirtualDocument { 5 | fileName: string; 6 | location: SourceFileRange; 7 | text: string; 8 | getPartsAtDocumentRange(range?: DocumentRange): (Expression | string)[]; 9 | sfPositionToDocumentOffset(position: SourceFilePosition): DocumentOffset; 10 | documentOffsetToSFPosition(offset: DocumentOffset): SourceFilePosition; 11 | } 12 | 13 | export function textPartsToRanges(parts: (Expression | string)[]): Range[] { 14 | let offset = 0; 15 | 16 | return parts 17 | .map(p => { 18 | if (typeof p === "string") { 19 | const startOffset = offset; 20 | offset += p.length; 21 | return { 22 | start: startOffset, 23 | end: offset 24 | } as Range; 25 | } else { 26 | offset += p.getText().length + 3; 27 | } 28 | return; 29 | }) 30 | .filter((r): r is Range => r != null); 31 | } 32 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/parse/document/virtual-document/virtual-html-document.ts: -------------------------------------------------------------------------------- 1 | import { VirtualAstDocument } from "./virtual-ast-document.js"; 2 | 3 | export class VirtualAstHtmlDocument extends VirtualAstDocument {} 4 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/parse/tagged-template/find-tagged-templates.ts: -------------------------------------------------------------------------------- 1 | import { Node, SourceFile, TaggedTemplateExpression } from "typescript"; 2 | import { tsModule } from "../../ts-module.js"; 3 | import { findParent, getNodeAtPosition } from "../../util/ast-util.js"; 4 | 5 | /** 6 | * Returns all virtual documents in a given file. 7 | * @param sourceFile 8 | * @param templateTags 9 | */ 10 | export function findTaggedTemplates(sourceFile: SourceFile, templateTags: string[]): TaggedTemplateExpression[]; 11 | export function findTaggedTemplates(sourceFile: SourceFile, templateTags: string[], position?: number): TaggedTemplateExpression | undefined; 12 | export function findTaggedTemplates( 13 | sourceFile: SourceFile, 14 | templateTags: string[], 15 | position?: number 16 | ): TaggedTemplateExpression[] | TaggedTemplateExpression | undefined { 17 | if (position != null) { 18 | const token = getNodeAtPosition(sourceFile, position); 19 | const node = findParent(token, tsModule.ts.isTaggedTemplateExpression); 20 | 21 | if (node != null && tsModule.ts.isTaggedTemplateExpression(node)) { 22 | if (templateTags.includes(node.tag.getText())) { 23 | return node; 24 | } 25 | } 26 | 27 | return undefined; 28 | } else { 29 | const taggedTemplates: TaggedTemplateExpression[] = []; 30 | 31 | visitTaggedTemplateNodes(sourceFile, { 32 | shouldCheckTemplateTag(templateTag: string) { 33 | return templateTags.includes(templateTag); 34 | }, 35 | emitTaggedTemplateNode(node: TaggedTemplateExpression) { 36 | taggedTemplates.push(node); 37 | } 38 | }); 39 | 40 | return taggedTemplates; 41 | } 42 | } 43 | 44 | export interface TaggedTemplateVisitContext { 45 | parent?: TaggedTemplateExpression; 46 | emitTaggedTemplateNode(node: TaggedTemplateExpression): void; 47 | shouldCheckTemplateTag(templateTag: string): boolean; 48 | } 49 | 50 | export function visitTaggedTemplateNodes(astNode: Node, context: TaggedTemplateVisitContext): void { 51 | const newContext = { ...context }; 52 | if (tsModule.ts.isTaggedTemplateExpression(astNode) && context.shouldCheckTemplateTag(astNode.tag.getText())) { 53 | // Only visit the template expression if the leading comments does not include the ts-ignore flag. 54 | //if (!leadingCommentsIncludes(astNode.getSourceFile().getText(), astNode.getFullStart(), TS_IGNORE_FLAG)) { 55 | newContext.parent = astNode; 56 | context.emitTaggedTemplateNode(astNode); 57 | } 58 | 59 | astNode.forEachChild(child => visitTaggedTemplateNodes(child, context)); 60 | } 61 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/store/analyzer-definition-store.ts: -------------------------------------------------------------------------------- 1 | import { SourceFile } from "typescript"; 2 | import { AnalyzerResult, ComponentDeclaration, ComponentDefinition } from "web-component-analyzer"; 3 | 4 | export interface AnalyzerDefinitionStore { 5 | getAnalysisResultForFile(sourceFile: SourceFile): AnalyzerResult | undefined; 6 | getDefinitionsWithDeclarationInFile(sourceFile: SourceFile): ComponentDefinition[]; 7 | getComponentDeclarationsInFile(sourceFile: SourceFile): ComponentDeclaration[]; 8 | getDefinitionForTagName(tagName: string): ComponentDefinition | undefined; 9 | getDefinitionsInFile(sourceFile: SourceFile): ComponentDefinition[]; 10 | } 11 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/store/analyzer-dependency-store.ts: -------------------------------------------------------------------------------- 1 | export interface AnalyzerDependencyStore { 2 | hasTagNameBeenImported(fileName: string, tagName: string): boolean; 3 | } 4 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/store/analyzer-document-store.ts: -------------------------------------------------------------------------------- 1 | import { SourceFile } from "typescript"; 2 | import { LitAnalyzerConfig } from "../lit-analyzer-config.js"; 3 | import { TextDocument } from "../parse/document/text-document/text-document.js"; 4 | import { SourceFilePosition } from "../types/range.js"; 5 | 6 | export interface AnalyzerDocumentStore { 7 | getDocumentAtPosition(sourceFile: SourceFile, position: SourceFilePosition, options: LitAnalyzerConfig): TextDocument | undefined; 8 | getDocumentsInFile(sourceFile: SourceFile, config: LitAnalyzerConfig): TextDocument[]; 9 | } 10 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/store/analyzer-html-store.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HtmlAttr, 3 | HtmlAttrTarget, 4 | HtmlCssPart, 5 | HtmlCssProperty, 6 | HtmlEvent, 7 | HtmlMember, 8 | HtmlProp, 9 | HtmlSlot, 10 | HtmlTag 11 | } from "../parse/parse-html-data/html-tag.js"; 12 | import { 13 | HtmlNodeAttr, 14 | IHtmlNodeAttr, 15 | IHtmlNodeAttrEventListener, 16 | IHtmlNodeAttrProp, 17 | IHtmlNodeBooleanAttribute 18 | } from "../types/html-node/html-node-attr-types.js"; 19 | import { HtmlNode } from "../types/html-node/html-node-types.js"; 20 | 21 | export interface AnalyzerHtmlStore { 22 | /*absorbAnalysisResult(sourceFile: SourceFile, result: AnalyzeComponentsResult): void; 23 | absorbSubclassExtension(name: string, extension: HtmlTag): void; 24 | absorbCollection(collection: HtmlDataCollection, register: HtmlStoreDataSource): void; 25 | forgetTagsDefinedInFile(sourceFile: SourceFile): void;*/ 26 | 27 | /*getDefinitionsWithDeclarationInFile(sourceFile: SourceFile): ComponentDefinition[]; 28 | getDefinitionForTagName(tagName: string): ComponentDefinition | undefined; 29 | getDefinitionsInFile(sourceFile: SourceFile): ComponentDefinition[]; 30 | hasTagNameBeenImported(fileName: string, tagName: string): boolean;*/ 31 | 32 | getHtmlTag(htmlNode: HtmlNode | string): HtmlTag | undefined; 33 | getGlobalTags(): Iterable; 34 | getAllAttributesForTag(htmlNode: HtmlNode | string): Iterable; 35 | getAllPropertiesForTag(htmlNode: HtmlNode | string): Iterable; 36 | getAllEventsForTag(htmlNode: HtmlNode | string): Iterable; 37 | getAllSlotsForTag(htmlNode: HtmlNode | string): Iterable; 38 | getAllCssPartsForTag(htmlNode: HtmlNode | string): Iterable; 39 | getAllCssPropertiesForTag(htmlNode: HtmlNode | string): Iterable; 40 | 41 | getHtmlAttrTarget(htmlNodeAttr: IHtmlNodeAttrProp): HtmlProp | undefined; 42 | getHtmlAttrTarget(htmlNodeAttr: IHtmlNodeAttr | IHtmlNodeBooleanAttribute): HtmlAttr | undefined; 43 | getHtmlAttrTarget(htmlNodeAttr: IHtmlNodeAttr | IHtmlNodeBooleanAttribute | IHtmlNodeAttrProp): HtmlMember | undefined; 44 | getHtmlAttrTarget(htmlNodeAttr: IHtmlNodeAttrEventListener): HtmlEvent | undefined; 45 | getHtmlAttrTarget(htmlNodeAttr: HtmlNodeAttr): HtmlAttrTarget | undefined; 46 | getHtmlAttrTarget(htmlNodeAttr: HtmlNodeAttr): HtmlAttrTarget | undefined; 47 | } 48 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/store/dependency-store/default-analyzer-dependency-store.ts: -------------------------------------------------------------------------------- 1 | import { SourceFile } from "typescript"; 2 | import { ComponentDefinition } from "web-component-analyzer"; 3 | import { AnalyzerDependencyStore } from "../analyzer-dependency-store.js"; 4 | 5 | export class DefaultAnalyzerDependencyStore implements AnalyzerDependencyStore { 6 | private importedComponentDefinitionsInFile = new Map(); 7 | 8 | absorbComponentDefinitionsForFile(sourceFile: SourceFile, result: ComponentDefinition[]): void { 9 | this.importedComponentDefinitionsInFile.set(sourceFile.fileName, result); 10 | } 11 | 12 | /** 13 | * Returns if a component for a specific file has been imported. 14 | * @param fileName 15 | * @param tagName 16 | */ 17 | hasTagNameBeenImported(fileName: string, tagName: string): boolean { 18 | for (const file of this.importedComponentDefinitionsInFile.get(fileName) || []) { 19 | if (file.tagName === tagName) { 20 | return true; 21 | } 22 | } 23 | 24 | return false; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/store/document-store/default-analyzer-document-store.ts: -------------------------------------------------------------------------------- 1 | import { SourceFile } from "typescript"; 2 | import { LitAnalyzerConfig } from "../../lit-analyzer-config.js"; 3 | import { parseDocumentsInSourceFile } from "../../parse/document/parse-documents-in-source-file.js"; 4 | import { TextDocument } from "../../parse/document/text-document/text-document.js"; 5 | import { SourceFilePosition } from "../../types/range.js"; 6 | import { AnalyzerDocumentStore } from "../analyzer-document-store.js"; 7 | 8 | export class DefaultAnalyzerDocumentStore implements AnalyzerDocumentStore { 9 | getDocumentAtPosition(sourceFile: SourceFile, position: SourceFilePosition, options: LitAnalyzerConfig): TextDocument | undefined { 10 | return parseDocumentsInSourceFile( 11 | sourceFile, 12 | { 13 | htmlTags: options.htmlTemplateTags, 14 | cssTags: options.cssTemplateTags 15 | }, 16 | position 17 | ); 18 | } 19 | 20 | getDocumentsInFile(sourceFile: SourceFile, config: LitAnalyzerConfig): TextDocument[] { 21 | return parseDocumentsInSourceFile(sourceFile, { 22 | htmlTags: config.htmlTemplateTags, 23 | cssTags: config.cssTemplateTags 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/ts-module.ts: -------------------------------------------------------------------------------- 1 | import { setTypescriptModule as tsSimpleTypeSetTypescriptModule } from "ts-simple-type"; 2 | import * as tsModuleType from "typescript"; 3 | 4 | export const tsModule: { ts: typeof tsModuleType } = { ts: tsModuleType }; 5 | 6 | export function setTypescriptModule(newModule: typeof tsModuleType): void { 7 | tsModule.ts = newModule; 8 | 9 | tsSimpleTypeSetTypescriptModule(newModule); 10 | } 11 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/types/html-node/html-node-attr-assignment-types.ts: -------------------------------------------------------------------------------- 1 | import { Expression } from "typescript"; 2 | import { Range } from "../range.js"; 3 | import { HtmlNodeAttr } from "./html-node-attr-types.js"; 4 | 5 | export enum HtmlNodeAttrAssignmentKind { 6 | BOOLEAN = "BOOLEAN", 7 | EXPRESSION = "EXPRESSION", 8 | STRING = "STRING", 9 | MIXED = "MIXED", 10 | ELEMENT_EXPRESSION = "ELEMENT_EXPRESSION" 11 | } 12 | 13 | export interface IHtmlNodeAttrAssignmentBase { 14 | location?: Range; 15 | htmlAttr: HtmlNodeAttr; 16 | } 17 | 18 | export interface IHtmlNodeAttrAssignmentExpression extends IHtmlNodeAttrAssignmentBase { 19 | kind: HtmlNodeAttrAssignmentKind.EXPRESSION; 20 | location: Range; 21 | expression: Expression; 22 | } 23 | 24 | export interface IHtmlNodeAttrAssignmentElement extends IHtmlNodeAttrAssignmentBase { 25 | kind: HtmlNodeAttrAssignmentKind.ELEMENT_EXPRESSION; 26 | expression: Expression; 27 | } 28 | 29 | export interface IHtmlNodeAttrAssignmentString extends IHtmlNodeAttrAssignmentBase { 30 | kind: HtmlNodeAttrAssignmentKind.STRING; 31 | location: Range; 32 | value: string; 33 | } 34 | 35 | export interface IHtmlNodeAttrAssignmentBoolean extends IHtmlNodeAttrAssignmentBase { 36 | kind: HtmlNodeAttrAssignmentKind.BOOLEAN; 37 | } 38 | 39 | export interface IHtmlNodeAttrAssignmentMixed extends IHtmlNodeAttrAssignmentBase { 40 | kind: HtmlNodeAttrAssignmentKind.MIXED; 41 | location: Range; 42 | values: (Expression | string)[]; 43 | } 44 | 45 | export type HtmlNodeAttrAssignment = 46 | | IHtmlNodeAttrAssignmentBoolean 47 | | IHtmlNodeAttrAssignmentExpression 48 | | IHtmlNodeAttrAssignmentString 49 | | IHtmlNodeAttrAssignmentMixed 50 | | IHtmlNodeAttrAssignmentElement; 51 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/types/html-node/html-node-attr-types.ts: -------------------------------------------------------------------------------- 1 | import { LitHtmlAttributeModifier } from "../../constants.js"; 2 | import { HtmlDocument } from "../../parse/document/text-document/html-document/html-document.js"; 3 | import { Range } from "../range.js"; 4 | import { HtmlNodeAttrAssignment } from "./html-node-attr-assignment-types.js"; 5 | import { HtmlNode } from "./html-node-types.js"; 6 | 7 | export enum HtmlNodeAttrKind { 8 | EVENT_LISTENER = "EVENT_LISTENER", 9 | ATTRIBUTE = "ATTRIBUTE", 10 | BOOLEAN_ATTRIBUTE = "BOOLEAN_ATTRIBUTE", 11 | PROPERTY = "PROPERTY" 12 | } 13 | 14 | export interface IHtmlNodeAttrSourceCodeLocation extends Range { 15 | name: Range; 16 | } 17 | 18 | export interface IHtmlNodeAttrBase { 19 | name: string; 20 | modifier?: LitHtmlAttributeModifier; 21 | location: IHtmlNodeAttrSourceCodeLocation; 22 | assignment?: HtmlNodeAttrAssignment; 23 | htmlNode: HtmlNode; 24 | document: HtmlDocument; 25 | } 26 | 27 | export interface IHtmlNodeAttrEventListener extends IHtmlNodeAttrBase { 28 | kind: HtmlNodeAttrKind.EVENT_LISTENER; 29 | modifier: "@"; 30 | } 31 | 32 | export interface IHtmlNodeAttrProp extends IHtmlNodeAttrBase { 33 | kind: HtmlNodeAttrKind.PROPERTY; 34 | modifier: "."; 35 | } 36 | 37 | export interface IHtmlNodeBooleanAttribute extends IHtmlNodeAttrBase { 38 | kind: HtmlNodeAttrKind.BOOLEAN_ATTRIBUTE; 39 | modifier: "?"; 40 | } 41 | 42 | export interface IHtmlNodeAttr extends IHtmlNodeAttrBase { 43 | kind: HtmlNodeAttrKind.ATTRIBUTE; 44 | modifier: undefined; 45 | } 46 | 47 | export type HtmlNodeAttr = IHtmlNodeAttrEventListener | IHtmlNodeAttrProp | IHtmlNodeAttr | IHtmlNodeBooleanAttribute; 48 | 49 | export function isHTMLAttr(obj: object): obj is IHtmlNodeAttrBase { 50 | return "name" in obj && "location" in obj && "htmlNode" in obj; 51 | } 52 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/types/html-node/html-node-types.ts: -------------------------------------------------------------------------------- 1 | import { HtmlDocument } from "../../parse/document/text-document/html-document/html-document.js"; 2 | import { Range } from "../range.js"; 3 | import { HtmlNodeAttr } from "./html-node-attr-types.js"; 4 | 5 | export interface IHtmlNodeSourceCodeLocation extends Range { 6 | name: Range; 7 | startTag: Range; 8 | endTag?: Range; 9 | } 10 | 11 | export enum HtmlNodeKind { 12 | NODE = "NODE", 13 | SVG = "SVG", 14 | STYLE = "STYLE" 15 | } 16 | 17 | export interface IHtmlNodeBase { 18 | tagName: string; 19 | location: IHtmlNodeSourceCodeLocation; 20 | attributes: HtmlNodeAttr[]; 21 | parent?: HtmlNode; 22 | children: HtmlNode[]; 23 | selfClosed: boolean; 24 | document: HtmlDocument; 25 | } 26 | 27 | export interface IHtmlNode extends IHtmlNodeBase { 28 | kind: HtmlNodeKind.NODE; 29 | } 30 | 31 | export interface IHtmlNodeStyleTag extends IHtmlNodeBase { 32 | kind: HtmlNodeKind.STYLE; 33 | } 34 | 35 | export interface IHtmlNodeSvgTag extends IHtmlNodeBase { 36 | kind: HtmlNodeKind.SVG; 37 | } 38 | 39 | export type HtmlNode = IHtmlNode | IHtmlNodeStyleTag | IHtmlNodeSvgTag; 40 | 41 | export function isHTMLNode(obj: object): obj is IHtmlNodeBase { 42 | return "tagName" in obj && "location" in obj && "attributes" in obj; 43 | } 44 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/types/lit-closing-tag-info.ts: -------------------------------------------------------------------------------- 1 | export interface LitClosingTagInfo { 2 | newText: string; 3 | } 4 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/types/lit-code-fix-action.ts: -------------------------------------------------------------------------------- 1 | import { SourceFileRange } from "./range.js"; 2 | 3 | export interface LitCodeFixAction { 4 | range: SourceFileRange; 5 | newText: string; 6 | } 7 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/types/lit-code-fix.ts: -------------------------------------------------------------------------------- 1 | import { LitCodeFixAction } from "./lit-code-fix-action.js"; 2 | 3 | export interface LitCodeFix { 4 | name: string; 5 | message: string; 6 | actions: LitCodeFixAction[]; 7 | } 8 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/types/lit-completion-details.ts: -------------------------------------------------------------------------------- 1 | import { LitTargetKind } from "./lit-target-kind.js"; 2 | 3 | export interface LitCompletionDetails { 4 | name: string; 5 | kind: LitTargetKind; 6 | primaryInfo: string; 7 | secondaryInfo?: string; 8 | } 9 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/types/lit-completion.ts: -------------------------------------------------------------------------------- 1 | import { LitTargetKind } from "./lit-target-kind.js"; 2 | import { SourceFileRange } from "./range.js"; 3 | 4 | export interface LitCompletion { 5 | name: string; 6 | kind: LitTargetKind; 7 | kindModifiers?: "color"; 8 | insert: string; 9 | range?: SourceFileRange; 10 | importance?: "high" | "medium" | "low"; 11 | sortText?: string; 12 | documentation?(): string | undefined; 13 | } 14 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/types/lit-definition.ts: -------------------------------------------------------------------------------- 1 | import { Node, SourceFile } from "typescript"; 2 | import { SourceFileRange } from "./range.js"; 3 | 4 | export type LitDefinitionTargetKind = "node" | "range"; 5 | 6 | export interface LitDefinitionTargetBase { 7 | kind: LitDefinitionTargetKind; 8 | } 9 | 10 | export interface LitDefinitionTargetNode extends LitDefinitionTargetBase { 11 | kind: "node"; 12 | node: Node; 13 | name?: string; 14 | } 15 | 16 | export interface LitDefinitionTargetRange { 17 | kind: "range"; 18 | sourceFile: SourceFile; 19 | range: SourceFileRange; 20 | name?: string; 21 | } 22 | 23 | export type LitDefinitionTarget = LitDefinitionTargetNode | LitDefinitionTargetRange; 24 | 25 | export interface LitDefinition { 26 | fromRange: SourceFileRange; 27 | targets: LitDefinitionTarget[]; 28 | } 29 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/types/lit-diagnostic.ts: -------------------------------------------------------------------------------- 1 | import { SourceFile } from "typescript"; 2 | import { LitAnalyzerRuleId } from "../lit-analyzer-config.js"; 3 | import { SourceFileRange } from "./range.js"; 4 | 5 | export type LitDiagnosticSeverity = "error" | "warning"; 6 | 7 | export interface LitDiagnostic { 8 | location: SourceFileRange; 9 | code?: number; 10 | message: string; 11 | fixMessage?: string; 12 | suggestion?: string; 13 | source: LitAnalyzerRuleId; 14 | severity: LitDiagnosticSeverity; 15 | file: SourceFile; 16 | } 17 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/types/lit-format-edit.ts: -------------------------------------------------------------------------------- 1 | import { SourceFileRange } from "./range.js"; 2 | 3 | export interface LitFormatEdit { 4 | range: SourceFileRange; 5 | newText: string; 6 | } 7 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/types/lit-outlining-span.ts: -------------------------------------------------------------------------------- 1 | import { SourceFileRange } from "./range.js"; 2 | 3 | export enum LitOutliningSpanKind { 4 | Comment = "comment", 5 | Region = "region", 6 | Code = "code", 7 | Imports = "imports" 8 | } 9 | 10 | export interface LitOutliningSpan { 11 | location: SourceFileRange; 12 | bannerText: string; 13 | autoCollapse?: boolean; 14 | kind: LitOutliningSpanKind; 15 | } 16 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/types/lit-quick-info.ts: -------------------------------------------------------------------------------- 1 | import { SourceFileRange } from "./range.js"; 2 | 3 | export interface LitQuickInfo { 4 | range: SourceFileRange; 5 | primaryInfo: string; 6 | secondaryInfo?: string; 7 | } 8 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/types/lit-rename-info.ts: -------------------------------------------------------------------------------- 1 | import { ComponentDefinition } from "web-component-analyzer"; 2 | import { HtmlDocument } from "../parse/document/text-document/html-document/html-document.js"; 3 | import { HtmlNode } from "./html-node/html-node-types.js"; 4 | import { LitTargetKind } from "./lit-target-kind.js"; 5 | import { SourceFileRange } from "./range.js"; 6 | 7 | export interface RenameInfoBase { 8 | kind: LitTargetKind; 9 | displayName: string; 10 | fullDisplayName: string; 11 | range: SourceFileRange; 12 | } 13 | 14 | export interface RenameHtmlNodeInfo extends RenameInfoBase { 15 | document: HtmlDocument; 16 | target: ComponentDefinition | HtmlNode; 17 | } 18 | 19 | export interface RenameComponentDefinitionInfo extends RenameInfoBase { 20 | target: ComponentDefinition; 21 | } 22 | 23 | export type LitRenameInfo = RenameHtmlNodeInfo | RenameComponentDefinitionInfo; 24 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/types/lit-rename-location.ts: -------------------------------------------------------------------------------- 1 | import { SourceFileRange } from "./range.js"; 2 | 3 | export interface LitRenameLocation { 4 | fileName: string; 5 | prefixText?: string; 6 | suffixText?: string; 7 | range: SourceFileRange; 8 | } 9 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/types/lit-target-kind.ts: -------------------------------------------------------------------------------- 1 | export type LitTargetKind = 2 | | "memberFunctionElement" 3 | | "functionElement" 4 | | "constructorImplementationElement" 5 | | "variableElement" 6 | | "classElement" 7 | | "interfaceElement" 8 | | "moduleElement" 9 | | "memberVariableElement" 10 | | "constElement" 11 | | "enumElement" 12 | | "keyword" 13 | | "constElement" 14 | | "alias" 15 | | "moduleElement" 16 | | "member" 17 | | "label" 18 | | "unknown"; 19 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/types/range.ts: -------------------------------------------------------------------------------- 1 | export interface Range { 2 | start: number; 3 | end: number; 4 | } 5 | 6 | // Offsets and positions 7 | export type DocumentOffset = number; 8 | 9 | export type SourceFilePosition = number; 10 | 11 | /*export type DocumentOffset = number & { _documentOffset: void }; 12 | 13 | export type SourceFilePosition = number & { _sourceFilePosition: void };*/ 14 | 15 | /*export function makeDocumentOffset(offset: number): DocumentOffset { 16 | return offset as DocumentOffset; 17 | } 18 | 19 | export function makeDocumentPosition(position: number): SourceFilePosition { 20 | return position as SourceFilePosition; 21 | }*/ 22 | 23 | // Ranges 24 | export type DocumentRange = { start: DocumentOffset; end: DocumentOffset } & { _brand: "document" }; 25 | 26 | export type SourceFileRange = { start: SourceFilePosition; end: SourceFilePosition } & { _brand: "sourcefile" }; 27 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/types/rule/rule-diagnostic.ts: -------------------------------------------------------------------------------- 1 | import { SourceFileRange } from "../range.js"; 2 | import { RuleFix } from "./rule-fix.js"; 3 | 4 | export interface RuleDiagnostic { 5 | location: SourceFileRange; 6 | message: string; 7 | fixMessage?: string; 8 | suggestion?: string; 9 | fix?: () => RuleFix[] | RuleFix; 10 | } 11 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/types/rule/rule-fix-action.ts: -------------------------------------------------------------------------------- 1 | import { Identifier, SourceFile } from "typescript"; 2 | import { HtmlNodeAttrAssignment } from "../html-node/html-node-attr-assignment-types.js"; 3 | import { HtmlNodeAttr } from "../html-node/html-node-attr-types.js"; 4 | import { HtmlNode } from "../html-node/html-node-types.js"; 5 | import { SourceFileRange } from "../range.js"; 6 | 7 | export type RuleFixActionKind = 8 | | "changeTagName" 9 | | "addAttribute" 10 | | "changeAttributeName" 11 | | "changeAttributeModifier" 12 | | "changeAssignment" 13 | | "import" 14 | | "extendGlobalDeclaration" 15 | | "changeRange" 16 | | "changeIdentifier"; 17 | 18 | export interface RuleFixActionBase { 19 | kind: RuleFixActionKind; 20 | file?: SourceFile; 21 | } 22 | 23 | export interface RuleFixActionChangeTagName extends RuleFixActionBase { 24 | kind: "changeTagName"; 25 | htmlNode: HtmlNode; 26 | newName: string; 27 | } 28 | 29 | export interface RuleFixActionAddAttribute extends RuleFixActionBase { 30 | kind: "addAttribute"; 31 | htmlNode: HtmlNode; 32 | name: string; 33 | value?: string; 34 | } 35 | 36 | export interface RuleFixActionChangeAttributeName extends RuleFixActionBase { 37 | kind: "changeAttributeName"; 38 | htmlAttr: HtmlNodeAttr; 39 | newName: string; 40 | } 41 | 42 | export interface RuleFixActionChangeAttributeModifier extends RuleFixActionBase { 43 | kind: "changeAttributeModifier"; 44 | htmlAttr: HtmlNodeAttr; 45 | newModifier: string; 46 | } 47 | 48 | export interface RuleFixActionChangeAssignment extends RuleFixActionBase { 49 | kind: "changeAssignment"; 50 | assignment: HtmlNodeAttrAssignment; 51 | newValue: string; 52 | } 53 | 54 | export interface RuleFixActionChangeIdentifier extends RuleFixActionBase { 55 | kind: "changeIdentifier"; 56 | identifier: Identifier; 57 | newText: string; 58 | } 59 | 60 | export interface RuleFixActionImport extends RuleFixActionBase { 61 | kind: "import"; 62 | file: SourceFile; 63 | path: string; 64 | identifiers?: string[]; 65 | } 66 | 67 | export interface RuleFixActionChangeRange extends RuleFixActionBase { 68 | kind: "changeRange"; 69 | range: SourceFileRange; 70 | newText: string; 71 | } 72 | 73 | export interface RuleFixActionExtendGlobalDeclaration extends RuleFixActionBase { 74 | kind: "extendGlobalDeclaration"; 75 | name: string; 76 | newMembers: string[]; 77 | } 78 | 79 | export type RuleFixAction = 80 | | RuleFixActionChangeTagName 81 | | RuleFixActionAddAttribute 82 | | RuleFixActionChangeAttributeName 83 | | RuleFixActionImport 84 | | RuleFixActionChangeAttributeModifier 85 | | RuleFixActionChangeAssignment 86 | | RuleFixActionChangeIdentifier 87 | | RuleFixActionExtendGlobalDeclaration 88 | | RuleFixActionChangeRange; 89 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/types/rule/rule-fix.ts: -------------------------------------------------------------------------------- 1 | import { RuleFixAction } from "./rule-fix-action.js"; 2 | 3 | export interface RuleFix { 4 | message: string; 5 | actions: RuleFixAction[]; 6 | } 7 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/types/rule/rule-module-context.ts: -------------------------------------------------------------------------------- 1 | import * as tsMod from "typescript"; 2 | import { Program, SourceFile } from "typescript"; 3 | import { LitAnalyzerConfig } from "../../lit-analyzer-config.js"; 4 | import { LitAnalyzerLogger } from "../../lit-analyzer-logger.js"; 5 | import { AnalyzerDefinitionStore } from "../../store/analyzer-definition-store.js"; 6 | import { AnalyzerDependencyStore } from "../../store/analyzer-dependency-store.js"; 7 | import { AnalyzerDocumentStore } from "../../store/analyzer-document-store.js"; 8 | import { AnalyzerHtmlStore } from "../../store/analyzer-html-store.js"; 9 | import { RuleDiagnostic } from "./rule-diagnostic.js"; 10 | 11 | export interface RuleModuleContext { 12 | readonly ts: typeof tsMod; 13 | readonly program: Program; 14 | readonly file: SourceFile; 15 | 16 | readonly htmlStore: AnalyzerHtmlStore; 17 | readonly dependencyStore: AnalyzerDependencyStore; 18 | readonly documentStore: AnalyzerDocumentStore; 19 | readonly definitionStore: AnalyzerDefinitionStore; 20 | 21 | readonly logger: LitAnalyzerLogger; 22 | readonly config: LitAnalyzerConfig; 23 | 24 | report(diagnostic: RuleDiagnostic): void; 25 | break(): void; 26 | } 27 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/types/rule/rule-module.ts: -------------------------------------------------------------------------------- 1 | import { ComponentDeclaration, ComponentDefinition, ComponentMember } from "web-component-analyzer"; 2 | import { LitAnalyzerRuleId } from "../../lit-analyzer-config.js"; 3 | import { HtmlNodeAttrAssignment } from "../html-node/html-node-attr-assignment-types.js"; 4 | import { HtmlNodeAttr } from "../html-node/html-node-attr-types.js"; 5 | import { HtmlNode } from "../html-node/html-node-types.js"; 6 | import { RuleModuleContext } from "./rule-module-context.js"; 7 | 8 | export type RuleModulePriority = "low" | "medium" | "high"; 9 | 10 | //export type RuleModuleCategory = "HTML" | "CSS" | "Component"; 11 | 12 | export interface RuleModuleImplementation { 13 | // Document based rules 14 | visitHtmlNode?(node: HtmlNode, context: RuleModuleContext): void; 15 | visitHtmlAttribute?(attribute: HtmlNodeAttr, context: RuleModuleContext): void; 16 | visitHtmlAssignment?(assignment: HtmlNodeAttrAssignment, context: RuleModuleContext): void; 17 | 18 | // Component based rules 19 | visitComponentDefinition?(definition: ComponentDefinition, context: RuleModuleContext): void; 20 | visitComponentDeclaration?(declaration: ComponentDeclaration, context: RuleModuleContext): void; 21 | visitComponentMember?(declaration: ComponentMember, context: RuleModuleContext): void; 22 | } 23 | 24 | export interface RuleModule extends RuleModuleImplementation { 25 | id: LitAnalyzerRuleId; 26 | 27 | meta?: { 28 | priority?: RuleModulePriority; 29 | /*docs?: { 30 | description: string; 31 | category: RuleModuleCategory; 32 | };*/ 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/util/array-util.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Flattens an array. 3 | * Use this function to keep support for node 10 4 | * @param items 5 | */ 6 | export function arrayFlat(items: (T[] | T)[]): T[] { 7 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 8 | if ("flat" in (items as any)) { 9 | return items.flat() as T[]; 10 | } 11 | 12 | const flattenArray: T[] = []; 13 | for (const item of items) { 14 | if (Array.isArray(item)) { 15 | flattenArray.push(...item); 16 | } else { 17 | flattenArray.push(item); 18 | } 19 | } 20 | return flattenArray; 21 | } 22 | 23 | /** 24 | * Filters an array returning only defined items 25 | * @param array 26 | */ 27 | export function arrayDefined(array: (T | undefined)[]): T[] { 28 | return array.filter((item): item is NonNullable => item != null); 29 | } 30 | 31 | /** 32 | * Joins an array with a custom final splitter 33 | * @param items 34 | * @param splitter 35 | * @param finalSplitter 36 | */ 37 | export function joinArray(items: string[], splitter = ", ", finalSplitter = "or"): string { 38 | return items.join(splitter).replace(/, ([^,]*)$/, ` ${finalSplitter} $1`); 39 | } 40 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/util/attribute-util.ts: -------------------------------------------------------------------------------- 1 | import { HtmlAttrTarget } from "../parse/parse-html-data/html-tag.js"; 2 | import { AnalyzerHtmlStore } from "../store/analyzer-html-store.js"; 3 | import { HtmlNodeAttr, HtmlNodeAttrKind } from "../types/html-node/html-node-attr-types.js"; 4 | import { findBestMatch } from "./find-best-match.js"; 5 | 6 | export function suggestTargetForHtmlAttr(htmlNodeAttr: HtmlNodeAttr, htmlStore: AnalyzerHtmlStore): HtmlAttrTarget | undefined { 7 | const properties = htmlStore.getAllPropertiesForTag(htmlNodeAttr.htmlNode); 8 | const attributes = htmlStore.getAllAttributesForTag(htmlNodeAttr.htmlNode); 9 | const events = htmlStore.getAllEventsForTag(htmlNodeAttr.htmlNode); 10 | 11 | switch (htmlNodeAttr.kind) { 12 | case HtmlNodeAttrKind.EVENT_LISTENER: 13 | return findSuggestedTarget(htmlNodeAttr.name, [events]); 14 | case HtmlNodeAttrKind.PROPERTY: 15 | return findSuggestedTarget(htmlNodeAttr.name, [properties, attributes]); 16 | case HtmlNodeAttrKind.ATTRIBUTE: 17 | case HtmlNodeAttrKind.BOOLEAN_ATTRIBUTE: 18 | return findSuggestedTarget(htmlNodeAttr.name, [attributes, properties]); 19 | } 20 | } 21 | 22 | function findSuggestedTarget(name: string, tests: Iterable[]): HtmlAttrTarget | undefined { 23 | for (const test of tests) { 24 | let items = [...test]; 25 | 26 | // If the search string starts with "on"/"aria", only check members starting with "on"/"aria" 27 | // If not, remove members starting with "on"/"aria" from the list of items 28 | if (name.startsWith("on")) { 29 | items = items.filter(item => item.name.startsWith("on")); 30 | } else if (name.startsWith("aria")) { 31 | items = items.filter(item => item.name.startsWith("aria")); 32 | } else { 33 | items = items.filter(item => !item.name.startsWith("on") && !item.name.startsWith("aria")); 34 | } 35 | 36 | const match = findBestMatch(name, items, { matchKey: "name", caseSensitive: false }); 37 | if (match != null) { 38 | return match; 39 | } 40 | } 41 | return; 42 | } 43 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/util/changed-source-file-iterator.ts: -------------------------------------------------------------------------------- 1 | import { SourceFile } from "typescript"; 2 | 3 | export type ChangedSourceFileIterator = ((sourceFiles: readonly SourceFile[]) => Iterable) & { 4 | invalidate(sourceFile: SourceFile): void; 5 | }; 6 | 7 | /** 8 | * Yields source files that have changed since last time this function was called. 9 | */ 10 | export function changedSourceFileIterator(): ChangedSourceFileIterator { 11 | const sourceFileCache = new WeakSet(); 12 | 13 | const iterator = function* (sourceFiles: readonly SourceFile[]): Iterable { 14 | for (const sourceFile of sourceFiles) { 15 | if (!sourceFileCache.has(sourceFile)) { 16 | yield sourceFile; 17 | sourceFileCache.add(sourceFile); 18 | } 19 | } 20 | }; 21 | 22 | return Object.assign(iterator, { 23 | invalidate(sourceFile: SourceFile) { 24 | sourceFileCache.delete(sourceFile); 25 | } 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/util/component-util.ts: -------------------------------------------------------------------------------- 1 | import { SourceFile } from "typescript"; 2 | import { ComponentDeclaration, ComponentDefinition, visitAllHeritageClauses } from "web-component-analyzer"; 3 | 4 | export function getDeclarationsInFile(definition: ComponentDefinition, sourceFile: SourceFile): ComponentDeclaration[] { 5 | const declarations = new Set(); 6 | emitDeclarationsInFile(definition, sourceFile, decl => declarations.add(decl)); 7 | return Array.from(declarations); 8 | } 9 | 10 | function emitDeclarationsInFile(definition: ComponentDefinition, sourceFile: SourceFile, emit: (decl: ComponentDeclaration) => unknown): void { 11 | const declaration = definition.declaration; 12 | 13 | if (declaration == null) { 14 | return; 15 | } 16 | 17 | if (declaration.sourceFile.fileName === sourceFile.fileName) { 18 | if (emit(declaration) === false) { 19 | return; 20 | } 21 | } 22 | 23 | visitAllHeritageClauses(declaration, clause => { 24 | if (clause.declaration && clause.declaration.sourceFile === sourceFile) { 25 | if (emit(clause.declaration) === false) { 26 | return; 27 | } 28 | } 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/util/find-best-match.ts: -------------------------------------------------------------------------------- 1 | import didYouMean, * as dym from "didyoumean2"; 2 | import { Omit } from "./general-util.js"; 3 | 4 | export interface FindBestMatchOptions { 5 | threshold?: number; 6 | caseSensitive?: boolean; 7 | matchKey: keyof T; 8 | } 9 | 10 | /** 11 | * Finds the best match between a string and elements in a list. 12 | * @param find 13 | * @param elements 14 | * @param options 15 | */ 16 | export function findBestMatch(find: string, elements: T[], options: FindBestMatchOptions): T | undefined { 17 | options.caseSensitive = "caseSensitive" in options ? options.caseSensitive : false; 18 | options.threshold = "threshold" in options ? options.threshold : 0.5; 19 | 20 | return ( 21 | didYouMean(find, elements as never[], { 22 | caseSensitive: options.caseSensitive, 23 | threshold: options.threshold, 24 | matchPath: [options.matchKey] as [string], 25 | returnType: dym.ReturnTypeEnums.FIRST_CLOSEST_MATCH, 26 | trimSpaces: false 27 | }) || undefined 28 | ); 29 | } 30 | 31 | export function findBestStringMatch( 32 | find: string, 33 | elements: string[], 34 | { caseSensitive = true, threshold = 0.5 }: Omit, "matchKey"> = {} 35 | ): string | undefined { 36 | const matches = didYouMean(find, elements, { caseSensitive, threshold }); 37 | return typeof matches === "string" ? matches : Array.isArray(matches) ? matches[0] : undefined; 38 | } 39 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/util/general-util.ts: -------------------------------------------------------------------------------- 1 | import { LitHtmlAttributeModifier } from "../constants.js"; 2 | 3 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 4 | export type Newable = { new (...args: any[]): T }; 5 | 6 | export type Omit = Pick>; 7 | 8 | /** 9 | * Parses an attribute name returning a name and eg. a modifier. 10 | * Examples: 11 | * - ?disabled="..." 12 | * - .myProp="..." 13 | * - @click="..." 14 | * @param attributeName 15 | */ 16 | export function parseLitAttrName(attributeName: string): { name: string; modifier?: LitHtmlAttributeModifier } { 17 | const [, modifier, name] = attributeName.match(/^([.?@])?(.*)/) || ["", "", ""]; 18 | return { name, modifier: modifier as LitHtmlAttributeModifier }; 19 | } 20 | 21 | export function lazy(func: T): T { 22 | let called = false; 23 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 24 | let value: any; 25 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 26 | return ((...args: any[]) => { 27 | if (called) return value; 28 | called = true; 29 | return (value = func(...args)); 30 | }) as unknown as T; 31 | } 32 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/util/get-position-context-in-document.ts: -------------------------------------------------------------------------------- 1 | import { TextDocument } from "../parse/document/text-document/text-document.js"; 2 | import { DocumentOffset } from "../types/range.js"; 3 | 4 | export interface DocumentPositionContext { 5 | text: string; 6 | offset: DocumentOffset; 7 | word: string; 8 | leftWord: string; 9 | rightWord: string; 10 | beforeWord: string; 11 | afterWord: string; 12 | } 13 | 14 | /** 15 | * Returns information about the position in a document. 16 | * @param document 17 | * @param offset 18 | */ 19 | export function getPositionContextInDocument(document: TextDocument, offset: DocumentOffset): DocumentPositionContext { 20 | const text = document.virtualDocument.text; 21 | 22 | const stopChar = /[/=<>\s"${}():]/; 23 | 24 | const leftWord = grabWordInDirection({ 25 | direction: "left", 26 | startOffset: offset, 27 | stopChar, 28 | text 29 | }); 30 | 31 | const rightWord = grabWordInDirection({ 32 | direction: "right", 33 | startOffset: offset, 34 | stopChar, 35 | text 36 | }); 37 | 38 | const word = leftWord + rightWord; 39 | 40 | const beforeWord = text[Math.max(0, offset - leftWord.length - 1)]; 41 | const afterWord = text[Math.min(text.length - 1, offset + rightWord.length)]; 42 | 43 | return { 44 | offset, 45 | text, 46 | word, 47 | leftWord, 48 | rightWord, 49 | beforeWord, 50 | afterWord 51 | }; 52 | } 53 | 54 | /** 55 | * Reads a word in a specific direction. 56 | * Stops if "stopChar" is encountered. 57 | * @param startPosition 58 | * @param stopChar 59 | * @param direction 60 | * @param text 61 | */ 62 | export function grabWordInDirection({ 63 | startOffset, 64 | stopChar, 65 | direction, 66 | text 67 | }: { 68 | stopChar: RegExp; 69 | direction: "left" | "right"; 70 | text: string; 71 | startOffset: DocumentOffset; 72 | }): string { 73 | const dir = direction === "left" ? -1 : 1; 74 | let curPosition = startOffset - (dir < 0 ? 1 : 0); 75 | while (curPosition > 0 && curPosition < text.length) { 76 | if (text[curPosition].match(stopChar)) break; 77 | curPosition += dir; 78 | if (curPosition > text.length || curPosition < 0) return ""; 79 | } 80 | 81 | const a = curPosition; 82 | const b = startOffset; 83 | return text.substring(Math.min(a, b) + (dir < 0 ? 1 : 0), Math.max(a, b)); 84 | } 85 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/util/is-valid-name.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * According to {@link https://html.spec.whatwg.org/multipage/syntax.html#attributes-2}, the following Unicode characters are illegal in an attribute name 3 | * @type {RegExp} 4 | */ 5 | const ILLEGAL_UNICODE_CHARACTERS = /[\u0020\u0022\u0027\u003E\u002F\u003D]/g; 6 | 7 | /** 8 | * According to {@link https://infra.spec.whatwg.org/#noncharacter}, 9 | * a noncharacter is a codepoint that matches any of the given unicode characters 10 | * @type {RegExp} 11 | */ 12 | const NONCHARACTERS = 13 | /[\uFFFF\uFFFE\uFDD1\uFDD2\uFDD3\uFDD4\uFDD5\uFDD6\uFDD7\uFDD8\uFDD9\uFDDA\uFDDB\uFDDC\uFDDD\uFDDE\uFDDF\uFDE0\uFDE1\uFDE2\uFDE3\uFDE4\uFDE5\uFDE6\uFDE7\uFDE8\uFDE9\uFDEA\uFDEB\uFDEC\uFDED\uFDEE\uFDEF]/g; 14 | 15 | function hasOnlyValidCharacters(name: string): boolean { 16 | return name.match(ILLEGAL_UNICODE_CHARACTERS) == null && name.match(NONCHARACTERS) == null; 17 | } 18 | 19 | /** 20 | * Returns true if the given input is a valid attribute name 21 | * @param {string} input 22 | * @return {boolean} 23 | */ 24 | export function isValidAttributeName(input: string): boolean { 25 | return hasOnlyValidCharacters(input); 26 | } 27 | 28 | /** 29 | * Returns true if the given input is a valid custom element name. 30 | * @param {string} input 31 | * @return {boolean} 32 | */ 33 | export function isValidCustomElementName(input: string): boolean { 34 | return input.includes("-") && input.toLowerCase() === input && hasOnlyValidCharacters(input); 35 | } 36 | 37 | export function isCustomElementTagName(tagName: string): boolean { 38 | return tagName.includes("-"); 39 | } 40 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/util/iterable-util.ts: -------------------------------------------------------------------------------- 1 | export function* iterableFlatten(...iterables: Iterable[]): Iterable { 2 | for (const iterable of iterables) { 3 | for (const item of iterable) { 4 | yield item; 5 | } 6 | } 7 | } 8 | 9 | export function* iterableMap(iterable: Iterable, map: (item: T) => U): Iterable { 10 | for (const item of iterable) { 11 | yield map(item); 12 | } 13 | } 14 | 15 | export function* iterableFilter(iterable: Iterable, filter: (item: T) => boolean): Iterable { 16 | for (const item of iterable) { 17 | if (filter(item)) { 18 | yield item; 19 | } 20 | } 21 | } 22 | 23 | export function iterableFind(iterable: Iterable, match: (item: T) => boolean): T | undefined { 24 | for (const item of iterable) { 25 | if (match(item)) { 26 | return item; 27 | } 28 | } 29 | return; 30 | } 31 | 32 | export function* iterableUnique(iterable: Iterable, on: (item: T) => U): Iterable { 33 | const unique = new Set(); 34 | for (const item of iterable) { 35 | const u = on(item); 36 | if (!unique.has(u)) { 37 | unique.add(u); 38 | yield item; 39 | } 40 | } 41 | } 42 | 43 | export function iterableDefined(iterable: (T | undefined | null)[]): T[] { 44 | return iterable.filter((i): i is T => i != null); 45 | } 46 | 47 | export function iterableFirst(iterable: Iterator | Set | Map | undefined): T | undefined { 48 | if (iterable == null) { 49 | return iterable; 50 | } 51 | 52 | if (iterable instanceof Map || iterable instanceof Set) { 53 | return iterableFirst(iterable.values()); 54 | } 55 | 56 | return iterable.next().value; 57 | } 58 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/util/map-util.ts: -------------------------------------------------------------------------------- 1 | export function mapMerge(...maps: (Map | Map[])[]): Map { 2 | return new Map( 3 | (function* () { 4 | for (const map of maps) { 5 | if (Array.isArray(map)) { 6 | for (const m of map) { 7 | yield* m; 8 | } 9 | } else { 10 | yield* map; 11 | } 12 | } 13 | })() 14 | ); 15 | } 16 | 17 | export function mapMap(map: Map, callback: (key: K, val: T) => U): Map { 18 | return new Map( 19 | (function* () { 20 | for (const [key, val] of map.entries()) { 21 | yield [key, callback(key, val)] as [K, U]; 22 | } 23 | })() 24 | ); 25 | } 26 | 27 | export function arrayToMap(array: T[], callback: (val: T) => K): Map { 28 | return new Map( 29 | (function* () { 30 | for (const val of array) { 31 | yield [callback(val), val] as [K, T]; 32 | } 33 | })() 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/util/range-util.ts: -------------------------------------------------------------------------------- 1 | import { Node } from "typescript"; 2 | import { TextDocument } from "../parse/document/text-document/text-document.js"; 3 | import { HtmlNodeAttr } from "../types/html-node/html-node-attr-types.js"; 4 | import { HtmlNode } from "../types/html-node/html-node-types.js"; 5 | import { DocumentRange, Range, SourceFileRange } from "../types/range.js"; 6 | 7 | export function makeSourceFileRange(range: Range): SourceFileRange { 8 | return range as SourceFileRange; 9 | } 10 | 11 | export function makeDocumentRange(range: Range): DocumentRange { 12 | return range as DocumentRange; 13 | } 14 | 15 | export function rangeFromHtmlNodeAttr(htmlAttr: HtmlNodeAttr): SourceFileRange { 16 | return documentRangeToSFRange(htmlAttr.document, htmlAttr.location.name); 17 | //return { document: htmlAttr.document, ...htmlAttr.location.name }; 18 | } 19 | 20 | export function rangeFromHtmlNode(htmlNode: HtmlNode): SourceFileRange { 21 | return documentRangeToSFRange(htmlNode.document, htmlNode.location.name); 22 | //return { document: htmlNode.document, ...htmlNode.location.name }; 23 | } 24 | 25 | export function rangeFromNode(node: Node): SourceFileRange { 26 | //return { file: node.getSourceFile(), start: node.getStart(), end: node.getEnd() }; 27 | return makeSourceFileRange({ start: node.getStart(), end: node.getEnd() }); 28 | } 29 | 30 | export function documentRangeToSFRange(document: TextDocument, range: DocumentRange | Range): SourceFileRange { 31 | return makeSourceFileRange({ 32 | start: document.virtualDocument.documentOffsetToSFPosition(range.start), 33 | end: document.virtualDocument.documentOffsetToSFPosition(range.end) 34 | }); 35 | } 36 | 37 | export function sfRangeToDocumentRange(document: TextDocument, range: SourceFileRange | Range): DocumentRange { 38 | return makeDocumentRange({ 39 | start: document.virtualDocument.sfPositionToDocumentOffset(range.start), 40 | end: document.virtualDocument.sfPositionToDocumentOffset(range.end) 41 | }); 42 | } 43 | 44 | /** 45 | * Returns if a position is within start and end. 46 | * @param position 47 | * @param start 48 | * @param end 49 | */ 50 | //export function intersects(position: SourceFilePosition | SourceFileRange, { start, end }: SourceFileRange): boolean; 51 | //export function intersects(position: DocumentOffset | DocumentRange, { start, end }: DocumentRange): boolean; 52 | export function intersects(position: number | Range, { start, end }: Range): boolean { 53 | if (typeof position === "number") { 54 | return start <= position && position <= end; 55 | } else { 56 | return start <= position.start && position.end <= end; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/util/rule-diagnostic-util.ts: -------------------------------------------------------------------------------- 1 | import { litDiagnosticRuleSeverity, ruleIdCode } from "../lit-analyzer-config.js"; 2 | import { LitAnalyzerContext } from "../lit-analyzer-context.js"; 3 | import { ReportedRuleDiagnostic } from "../rule-collection.js"; 4 | import { LitDiagnostic } from "../types/lit-diagnostic.js"; 5 | 6 | export function convertRuleDiagnosticToLitDiagnostic(reported: ReportedRuleDiagnostic, context: LitAnalyzerContext): LitDiagnostic { 7 | const source = reported.source; 8 | const { message, location, fixMessage, suggestion } = reported.diagnostic; 9 | 10 | return { 11 | fixMessage, 12 | location, 13 | suggestion, 14 | message, 15 | source, 16 | file: context.currentFile, 17 | severity: litDiagnosticRuleSeverity(context.config, source), 18 | code: ruleIdCode(source) 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/util/str-util.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Compares two strings case insensitive. 3 | * @param strA 4 | * @param strB 5 | */ 6 | export function caseInsensitiveEquals(strA: string, strB: string): boolean { 7 | return strA.localeCompare(strB, undefined, { sensitivity: "accent" }) === 0; 8 | } 9 | 10 | export function replacePrefix(str: string, prefix: string): string { 11 | return str.replace(new RegExp("^" + escapeRegExp(prefix)), ""); 12 | } 13 | 14 | function escapeRegExp(text: string): string { 15 | return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); 16 | } 17 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/analyze/util/type-util.ts: -------------------------------------------------------------------------------- 1 | import { SimpleType, SimpleTypeUnion } from "ts-simple-type"; 2 | 3 | const PRIMITIVE_STRING_ARRAY_TYPE_BRAND = Symbol("PRIMITIVE_STRING_ARRAY_TYPE"); 4 | 5 | /** 6 | * Brands a union as a primitive array type 7 | * This type is used for the "role" attribute that is a whitespace separated list 8 | * @param union 9 | */ 10 | export function makePrimitiveArrayType(union: SimpleTypeUnion): SimpleTypeUnion { 11 | const extendedUnion: SimpleTypeUnion = { 12 | ...union 13 | }; 14 | 15 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 16 | (extendedUnion as any)[PRIMITIVE_STRING_ARRAY_TYPE_BRAND] = true; 17 | 18 | return extendedUnion; 19 | } 20 | 21 | /** 22 | * Returns if a simple type is branded as a primitive array type 23 | * @param simpleType 24 | */ 25 | export function isPrimitiveArrayType(simpleType: SimpleType): simpleType is SimpleTypeUnion { 26 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 27 | return simpleType.kind === "UNION" && (simpleType as any)[PRIMITIVE_STRING_ARRAY_TYPE_BRAND] === true; 28 | } 29 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/cli/format/diagnostic-formatter.ts: -------------------------------------------------------------------------------- 1 | import { SourceFile } from "typescript"; 2 | import { LitDiagnostic } from "../../analyze/types/lit-diagnostic.js"; 3 | import { LitAnalyzerCliConfig } from "../lit-analyzer-cli-config.js"; 4 | 5 | export interface AnalysisStats { 6 | diagnostics: number; 7 | errors: number; 8 | warnings: number; 9 | filesWithProblems: number; 10 | totalFiles: number; 11 | } 12 | 13 | export interface DiagnosticFormatter { 14 | report(stats: AnalysisStats, config: LitAnalyzerCliConfig): string | undefined; 15 | diagnosticTextForFile(file: SourceFile, diagnostics: LitDiagnostic[], config: LitAnalyzerCliConfig): string | undefined; 16 | } 17 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/cli/format/list-diagnostic-formatter.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import { SourceFile } from "typescript"; 3 | import { LitDiagnostic } from "../../analyze/types/lit-diagnostic.js"; 4 | import { AnalysisStats, DiagnosticFormatter } from "./diagnostic-formatter.js"; 5 | import { generalReport, relativeFileName, textPad } from "./util.js"; 6 | 7 | export class ListDiagnosticFormatter implements DiagnosticFormatter { 8 | report(stats: AnalysisStats): string | undefined { 9 | return generalReport(stats); 10 | } 11 | 12 | diagnosticTextForFile(file: SourceFile, diagnostics: LitDiagnostic[]): string | undefined { 13 | if (diagnostics.length === 0) return undefined; 14 | 15 | return diagnosticTextForFile(file, diagnostics); 16 | } 17 | } 18 | 19 | function diagnosticTextForFile(file: SourceFile, diagnostics: LitDiagnostic[]): string { 20 | const diagnosticText = diagnostics.map(diagnostic => litDiagnosticToErrorText(file, diagnostic)).join("\n"); 21 | 22 | return ` 23 | ${chalk.underline(`${relativeFileName(file.fileName)}`)} 24 | ${diagnosticText}`; 25 | } 26 | 27 | function litDiagnosticToErrorText(file: SourceFile, diagnostic: LitDiagnostic): string { 28 | const lineContext = file.getLineAndCharacterOfPosition(diagnostic.location.start); 29 | const linePart = `${textPad(`${lineContext.line + 1}`, { width: 5 })}:${textPad(`${lineContext.character}`, { 30 | width: 4, 31 | dir: "right" 32 | })}`; 33 | const severityPart = `${textPad(diagnostic.severity === "warning" ? chalk.yellow("warning") : chalk.red("error"), { 34 | width: 18, 35 | dir: "right" 36 | })}`; 37 | const messagePart = diagnostic.message; 38 | return `${linePart} ${severityPart} ${messagePart}`; 39 | } 40 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/cli/format/markdown-formatter.ts: -------------------------------------------------------------------------------- 1 | import { SourceFile } from "typescript"; 2 | import { LitDiagnostic } from "../../analyze/types/lit-diagnostic.js"; 3 | import { AnalysisStats, DiagnosticFormatter } from "./diagnostic-formatter.js"; 4 | import { markdownHeader, markdownHighlight, markdownTable } from "./markdown-util.js"; 5 | import { relativeFileName } from "./util.js"; 6 | 7 | export class MarkdownDiagnosticFormatter implements DiagnosticFormatter { 8 | report(stats: AnalysisStats): string | undefined { 9 | return ` 10 | ${markdownHeader(2, "Summary")} 11 | ${markdownTable([ 12 | ["Files analyzed", "Files with problems", "Problems", "Errors", "Warnings"], 13 | [stats.totalFiles, stats.filesWithProblems, stats.diagnostics, stats.errors, stats.warnings].map(v => v.toString()) 14 | ])}`; 15 | } 16 | 17 | diagnosticTextForFile(file: SourceFile, diagnostics: LitDiagnostic[]): string | undefined { 18 | if (diagnostics.length === 0) return undefined; 19 | 20 | return ` 21 | ${markdownHeader(2, `${relativeFileName(file.fileName)}`)} 22 | ${markdownDiagnosticTable(file, diagnostics)}`; 23 | } 24 | } 25 | 26 | function markdownDiagnosticTable(file: SourceFile, diagnostics: LitDiagnostic[]): string { 27 | const headerRow: string[] = ["Line", "Column", "Type", "Rule", "Message"]; 28 | 29 | const rows: string[][] = diagnostics.map((diagnostic): string[] => { 30 | const lineContext = file.getLineAndCharacterOfPosition(diagnostic.location.start); 31 | 32 | return [ 33 | (lineContext.line + 1).toString(), 34 | (lineContext.character + 1).toString(), 35 | diagnostic.severity === "error" ? markdownHighlight("error") : "warning", 36 | diagnostic.source || "", 37 | diagnostic.message 38 | ]; 39 | }); 40 | 41 | return markdownTable([headerRow, ...rows], { removeEmptyColumns: true }); 42 | } 43 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/cli/format/util.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import { TextSpan } from "typescript"; 3 | import { AnalysisStats } from "./diagnostic-formatter.js"; 4 | 5 | export function generalReport(stats: AnalysisStats): string { 6 | function numberStatText(n: number, text: string): string { 7 | return `${n} ${text}${n === 1 ? "" : "s"}`; 8 | } 9 | 10 | if (stats.diagnostics > 0) { 11 | const message = ` ✖ ${numberStatText(stats.diagnostics, "problem")} in ${numberStatText(stats.filesWithProblems, "file")} (${numberStatText( 12 | stats.errors, 13 | "error" 14 | )}, ${numberStatText(stats.warnings, "warning")})`; 15 | 16 | if (stats.errors > 0) { 17 | return `\n${chalk.red(message)}`; 18 | } else { 19 | return `\n${chalk.yellow(message)}`; 20 | } 21 | } else { 22 | return `\n${chalk.green(` ✓ Found 0 problems in ${numberStatText(stats.totalFiles, "file")}`)}`; 23 | } 24 | } 25 | 26 | export function relativeFileName(fileName: string): string { 27 | return fileName.replace(process.cwd(), "."); 28 | } 29 | 30 | export function markText(text: string, range: TextSpan, colorFunction: (str: string) => string = chalk.bgRedBright): string { 31 | return ( 32 | text.substring(0, range.start) + chalk.bold(colorFunction(text.substr(range.start, range.length))) + text.substring(range.start + range.length) 33 | ); 34 | } 35 | 36 | export function textPad(str: string, { width, fill, dir }: { width: number; fill?: string; dir?: "left" | "right" }): string { 37 | const padding = (fill || " ").repeat(Math.max(0, width - str.length)); 38 | return `${dir !== "right" ? padding : ""}${str}${dir === "right" ? padding : ""}`; 39 | } 40 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/cli/lit-analyzer-cli-config.ts: -------------------------------------------------------------------------------- 1 | import { LitAnalyzerRules } from "../analyze/lit-analyzer-config.js"; 2 | 3 | export type FormatterFormat = "code" | "list" | "markdown"; 4 | 5 | export interface LitAnalyzerCliConfig { 6 | debug?: boolean; 7 | help?: boolean; 8 | noColor?: boolean; 9 | maxWarnings?: number; 10 | outFile?: string; 11 | failFast?: boolean; 12 | quiet?: boolean; 13 | strict?: boolean; 14 | format?: FormatterFormat; 15 | rules?: LitAnalyzerRules; 16 | maxProjectImportDepth?: number; 17 | maxNodeModuleImportDepth?: number; 18 | } 19 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/cli/parse-cli-arguments.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { dashToCamelCase } from "./util.js"; 3 | 4 | export type CliArguments = { _: string[] } & Record; 5 | 6 | /** 7 | * Parses CLI arguments. 8 | * @param args 9 | */ 10 | export function parseCliArguments(args: string[]): CliArguments { 11 | const result = { _: [] as string[] } as CliArguments; 12 | 13 | for (let i = 0; i < args.length; i++) { 14 | const arg = args[i]; 15 | 16 | // Parses: "--key", "-k", "--key=value", "--key value" 17 | if (arg.startsWith("-")) { 18 | // Parses: "--key=value" 19 | if (arg.includes("=")) { 20 | const [key, value] = arg.split("="); 21 | assignValue(result, key, value); 22 | } 23 | 24 | // Parses: "--key value", "--key", "-k 25 | else { 26 | const key = transformKey(arg); 27 | 28 | // Parses: "--key value" 29 | if (i < args.length - 1) { 30 | const value = args[i + 1]; 31 | if (!value.startsWith("-")) { 32 | assignValue(result, key, value); 33 | i++; 34 | continue; 35 | } 36 | } 37 | 38 | // Parses: "--key", "-k" 39 | assignValue(result, key, true); 40 | } 41 | } 42 | 43 | // Parses: "arg1", "arg2", ... 44 | else { 45 | result._.push(arg); 46 | } 47 | } 48 | 49 | return result; 50 | } 51 | 52 | /** 53 | * Transform a string to a primitive type. 54 | * @param value 55 | */ 56 | function transformValue(value: any): string | boolean | number { 57 | if (typeof value === "boolean") { 58 | return value; 59 | } else if (!isNaN(Number(value))) { 60 | return Number(value); 61 | } else if (value === "true" || value === "false") { 62 | return value === "true"; 63 | } 64 | 65 | return value; 66 | } 67 | 68 | /** 69 | * Transform a key by removing the first "-" characters. 70 | * @param key 71 | */ 72 | function transformKey(key: string): string { 73 | return dashToCamelCase(key.replace(/^-*/g, "")); 74 | } 75 | 76 | /** 77 | * Assigns a value on a specific key and transforms the value at the same time. 78 | * @param obj 79 | * @param key 80 | * @param value 81 | */ 82 | function assignValue(obj: any, key: string, value: any) { 83 | // The key could be "nested.key" 84 | const keys = transformKey(key).split("."); 85 | 86 | keys.forEach((k, i) => { 87 | // Assign the final value 88 | if (i >= keys.length - 1) { 89 | obj[k] = transformValue(value); 90 | } 91 | 92 | // Create nested objects 93 | else { 94 | if (!(k in obj)) { 95 | obj[k] = {}; 96 | } 97 | obj = obj[k]; 98 | } 99 | }); 100 | } 101 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/cli/util.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Converts from snake case to camel case 3 | * @param str 4 | */ 5 | export function dashToCamelCase(str: string): string { 6 | return str.replace(/(-\w)/g, m => m[1].toUpperCase()); 7 | } 8 | 9 | /** 10 | * Converts from camel case to snake case 11 | * @param str 12 | */ 13 | export function camelToDashCase(str: string): string { 14 | return str.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`); 15 | } 16 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/rules/all-rules.ts: -------------------------------------------------------------------------------- 1 | import { RuleModule } from "../analyze/types/rule/rule-module.js"; 2 | import noBooleanInAttributeBindingRule from "./no-boolean-in-attribute-binding.js"; 3 | import noComplexAttributeBindingRule from "./no-complex-attribute-binding.js"; 4 | import noExpressionlessPropertyBindingRule from "./no-expressionless-property-binding.js"; 5 | import noIncompatiblePropertyType from "./no-incompatible-property-type.js"; 6 | import noIncompatibleTypeBindingRule from "./no-incompatible-type-binding.js"; 7 | import noInvalidAttributeName from "./no-invalid-attribute-name.js"; 8 | import noInvalidDirectiveBindingRule from "./no-invalid-directive-binding.js"; 9 | import noInvalidTagName from "./no-invalid-tag-name.js"; 10 | import noLegacyAttribute from "./no-legacy-attribute.js"; 11 | import noMissingElementTypeDefinition from "./no-missing-element-type-definition.js"; 12 | import noMissingImport from "./no-missing-import.js"; 13 | import noNoncallableEventBindingRule from "./no-noncallable-event-binding.js"; 14 | import noNullableAttributeBindingRule from "./no-nullable-attribute-binding.js"; 15 | import noPropertyVisibilityMismatch from "./no-property-visibility-mismatch.js"; 16 | import noUnclosedTag from "./no-unclosed-tag.js"; 17 | import noUnintendedMixedBindingRule from "./no-unintended-mixed-binding.js"; 18 | import noUnknownAttribute from "./no-unknown-attribute.js"; 19 | import noUnknownEvent from "./no-unknown-event.js"; 20 | import noUnknownProperty from "./no-unknown-property.js"; 21 | import noUnknownSlotRule from "./no-unknown-slot.js"; 22 | import noUnknownTagName from "./no-unknown-tag-name.js"; 23 | 24 | export const ALL_RULES: RuleModule[] = [ 25 | noExpressionlessPropertyBindingRule, 26 | noUnintendedMixedBindingRule, 27 | noUnknownSlotRule, 28 | noNoncallableEventBindingRule, 29 | noNullableAttributeBindingRule, 30 | noComplexAttributeBindingRule, 31 | noBooleanInAttributeBindingRule, 32 | noInvalidDirectiveBindingRule, 33 | noIncompatibleTypeBindingRule, 34 | noMissingImport, 35 | noUnclosedTag, 36 | noUnknownTagName, 37 | noUnknownAttribute, 38 | noUnknownProperty, 39 | noUnknownEvent, 40 | noIncompatiblePropertyType, 41 | noInvalidTagName, 42 | noInvalidAttributeName, 43 | noPropertyVisibilityMismatch, 44 | noLegacyAttribute, 45 | noMissingElementTypeDefinition 46 | ]; 47 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/rules/no-expressionless-property-binding.ts: -------------------------------------------------------------------------------- 1 | import { HtmlNodeAttrAssignmentKind } from "../analyze/types/html-node/html-node-attr-assignment-types.js"; 2 | import { HtmlNodeAttrKind } from "../analyze/types/html-node/html-node-attr-types.js"; 3 | import { RuleModule } from "../analyze/types/rule/rule-module.js"; 4 | import { rangeFromHtmlNodeAttr } from "../analyze/util/range-util.js"; 5 | 6 | /** 7 | * This rule validates that non-attribute bindings are always used with an expression. 8 | */ 9 | const rule: RuleModule = { 10 | id: "no-expressionless-property-binding", 11 | meta: { 12 | priority: "high" 13 | }, 14 | 15 | visitHtmlAssignment(assignment, context) { 16 | const { htmlAttr } = assignment; 17 | 18 | // Only make this check non-expression type assignments. 19 | switch (assignment.kind) { 20 | case HtmlNodeAttrAssignmentKind.STRING: 21 | case HtmlNodeAttrAssignmentKind.BOOLEAN: 22 | switch (htmlAttr.kind) { 23 | case HtmlNodeAttrKind.EVENT_LISTENER: 24 | context.report({ 25 | location: rangeFromHtmlNodeAttr(htmlAttr), 26 | message: `You are using an event listener binding without an expression` 27 | }); 28 | break; 29 | case HtmlNodeAttrKind.BOOLEAN_ATTRIBUTE: 30 | context.report({ 31 | location: rangeFromHtmlNodeAttr(htmlAttr), 32 | message: `You are using a boolean attribute binding without an expression` 33 | }); 34 | break; 35 | case HtmlNodeAttrKind.PROPERTY: 36 | context.report({ 37 | location: rangeFromHtmlNodeAttr(htmlAttr), 38 | message: `You are using a property binding without an expression` 39 | }); 40 | break; 41 | } 42 | } 43 | } 44 | }; 45 | 46 | export default rule; 47 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/rules/no-incompatible-type-binding.ts: -------------------------------------------------------------------------------- 1 | import { 2 | LIT_HTML_BOOLEAN_ATTRIBUTE_MODIFIER, 3 | LIT_HTML_EVENT_LISTENER_ATTRIBUTE_MODIFIER, 4 | LIT_HTML_PROP_ATTRIBUTE_MODIFIER 5 | } from "../analyze/constants.js"; 6 | import { HtmlNodeAttrAssignmentKind } from "../analyze/types/html-node/html-node-attr-assignment-types.js"; 7 | import { RuleModule } from "../analyze/types/rule/rule-module.js"; 8 | import { extractBindingTypes } from "./util/type/extract-binding-types.js"; 9 | import { isAssignableInAttributeBinding } from "./util/type/is-assignable-in-attribute-binding.js"; 10 | import { isAssignableInBooleanBinding } from "./util/type/is-assignable-in-boolean-binding.js"; 11 | import { isAssignableInPropertyBinding } from "./util/type/is-assignable-in-property-binding.js"; 12 | import { isAssignableInElementBinding } from "./util/type/is-assignable-in-element-binding.js"; 13 | 14 | /** 15 | * This rule validate if the types of a binding are assignable. 16 | */ 17 | const rule: RuleModule = { 18 | id: "no-incompatible-type-binding", 19 | meta: { 20 | priority: "low" 21 | }, 22 | visitHtmlAssignment(assignment, context) { 23 | const { htmlAttr } = assignment; 24 | 25 | if (assignment.kind === HtmlNodeAttrAssignmentKind.ELEMENT_EXPRESSION) { 26 | // For element bindings we only care about the expression type 27 | const { typeB } = extractBindingTypes(assignment, context); 28 | isAssignableInElementBinding(htmlAttr, typeB, context); 29 | } 30 | 31 | if (context.htmlStore.getHtmlAttrTarget(htmlAttr) == null) { 32 | return; 33 | } 34 | 35 | const { typeA, typeB } = extractBindingTypes(assignment, context); 36 | 37 | // Validate types based on the binding in which they appear 38 | switch (htmlAttr.modifier) { 39 | case LIT_HTML_BOOLEAN_ATTRIBUTE_MODIFIER: 40 | isAssignableInBooleanBinding(htmlAttr, { typeA, typeB }, context); 41 | break; 42 | 43 | case LIT_HTML_PROP_ATTRIBUTE_MODIFIER: 44 | isAssignableInPropertyBinding(htmlAttr, { typeA, typeB }, context); 45 | break; 46 | 47 | case LIT_HTML_EVENT_LISTENER_ATTRIBUTE_MODIFIER: 48 | break; 49 | 50 | default: { 51 | isAssignableInAttributeBinding(htmlAttr, { typeA, typeB }, context); 52 | break; 53 | } 54 | } 55 | } 56 | }; 57 | 58 | export default rule; 59 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/rules/no-invalid-attribute-name.ts: -------------------------------------------------------------------------------- 1 | import { Node } from "typescript"; 2 | import { RuleModule } from "../analyze/types/rule/rule-module.js"; 3 | import { isValidAttributeName } from "../analyze/util/is-valid-name.js"; 4 | import { rangeFromNode } from "../analyze/util/range-util.js"; 5 | 6 | const rule: RuleModule = { 7 | id: "no-invalid-attribute-name", 8 | meta: { 9 | priority: "low" 10 | }, 11 | visitComponentMember(member, context) { 12 | // Check if the tag name is invalid 13 | let attrName: undefined | string; 14 | let attrNameNode: undefined | Node; 15 | 16 | if (member.kind === "attribute") { 17 | attrName = member.attrName; 18 | attrNameNode = member.node; 19 | } else if (typeof member.meta?.attribute === "string") { 20 | attrName = member.meta.attribute; 21 | attrNameNode = member.meta.node?.attribute || member.node; 22 | } 23 | 24 | if (attrName != null && attrNameNode != null && attrNameNode.getSourceFile() === context.file && !isValidAttributeName(attrName)) { 25 | context.report({ 26 | location: rangeFromNode(attrNameNode), 27 | message: `'${attrName}' is not a valid attribute name.` 28 | }); 29 | } 30 | } 31 | }; 32 | 33 | export default rule; 34 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/rules/no-invalid-tag-name.ts: -------------------------------------------------------------------------------- 1 | import { RuleModule } from "../analyze/types/rule/rule-module.js"; 2 | import { isValidCustomElementName } from "../analyze/util/is-valid-name.js"; 3 | import { iterableFirst } from "../analyze/util/iterable-util.js"; 4 | import { rangeFromNode } from "../analyze/util/range-util.js"; 5 | 6 | const rule: RuleModule = { 7 | id: "no-invalid-tag-name", 8 | meta: { 9 | priority: "low" 10 | }, 11 | visitComponentDefinition(definition, context) { 12 | // Check if the tag name is invalid 13 | if (!isValidCustomElementName(definition.tagName) && definition.tagName !== "") { 14 | const node = iterableFirst(definition.tagNameNodes) || iterableFirst(definition.identifierNodes); 15 | 16 | // Only report diagnostic if the tag is not built in, 17 | // because this function among other things tests for missing "-" in custom element names 18 | const tag = context.htmlStore.getHtmlTag(definition.tagName); 19 | if (node != null && tag != null && !tag.builtIn) { 20 | context.report({ 21 | location: rangeFromNode(node), 22 | message: `The tag name '${definition.tagName}' is not a valid custom element name. Remember that a hyphen (-) is required.` 23 | }); 24 | } 25 | } 26 | } 27 | }; 28 | 29 | export default rule; 30 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/rules/no-missing-element-type-definition.ts: -------------------------------------------------------------------------------- 1 | import { RuleModule } from "../analyze/types/rule/rule-module.js"; 2 | import { findParent, getNodeIdentifier } from "../analyze/util/ast-util.js"; 3 | import { iterableFind } from "../analyze/util/iterable-util.js"; 4 | import { rangeFromNode } from "../analyze/util/range-util.js"; 5 | 6 | /** 7 | * This rule validates that legacy Polymer attribute bindings are not used. 8 | */ 9 | const rule: RuleModule = { 10 | id: "no-missing-element-type-definition", 11 | meta: { 12 | priority: "low" 13 | }, 14 | visitComponentDefinition(definition, context) { 15 | // Don't run this rule on non-typescript files and declaration files 16 | if (context.file.isDeclarationFile || !context.file.fileName.endsWith(".ts")) { 17 | return; 18 | } 19 | 20 | // Try to find the tag name node on "interface HTMLElementTagNameMap" 21 | const htmlElementTagNameMapTagNameNode = iterableFind( 22 | definition.tagNameNodes, 23 | node => 24 | findParent( 25 | node, 26 | node => context.ts.isInterfaceDeclaration(node) && context.ts.isModuleBlock(node.parent) && node.name.getText() === "HTMLElementTagNameMap" 27 | ) != null 28 | ); 29 | 30 | // Don't continue if the node was found 31 | if (htmlElementTagNameMapTagNameNode != null) { 32 | return; 33 | } 34 | 35 | // Find the identifier node 36 | const declarationIdentifier = definition.declaration != null ? getNodeIdentifier(definition.declaration.node, context.ts) : undefined; 37 | if (declarationIdentifier == null) { 38 | return; 39 | } 40 | 41 | // Only report diagnostic if the tag is not built in, 42 | const tag = context.htmlStore.getHtmlTag(definition.tagName); 43 | 44 | if (!tag?.builtIn) { 45 | context.report({ 46 | location: rangeFromNode(declarationIdentifier), 47 | message: `'${definition.tagName}' has not been registered on HTMLElementTagNameMap`, 48 | fix: () => { 49 | return { 50 | message: `Register '${definition.tagName}' on HTMLElementTagNameMap`, 51 | actions: [ 52 | { 53 | kind: "extendGlobalDeclaration", 54 | file: context.file, 55 | name: "HTMLElementTagNameMap", 56 | newMembers: [`"${definition.tagName}": ${declarationIdentifier.text}`] 57 | } 58 | ] 59 | }; 60 | } 61 | }); 62 | } 63 | } 64 | }; 65 | 66 | export default rule; 67 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/rules/no-noncallable-event-binding.ts: -------------------------------------------------------------------------------- 1 | import { isAssignableToSimpleTypeKind, SimpleType, typeToString, validateType } from "ts-simple-type"; 2 | import { HtmlNodeAttrKind } from "../analyze/types/html-node/html-node-attr-types.js"; 3 | import { RuleModule } from "../analyze/types/rule/rule-module.js"; 4 | import { rangeFromHtmlNodeAttr } from "../analyze/util/range-util.js"; 5 | import { extractBindingTypes } from "./util/type/extract-binding-types.js"; 6 | 7 | /** 8 | * This rule validates that only callable types are used within event binding expressions. 9 | * This rule catches typos like: @click="onClick()" 10 | */ 11 | const rule: RuleModule = { 12 | id: "no-noncallable-event-binding", 13 | meta: { 14 | priority: "high" 15 | }, 16 | visitHtmlAssignment(assignment, context) { 17 | // Only validate event listener bindings. 18 | const { htmlAttr } = assignment; 19 | if (htmlAttr.kind !== HtmlNodeAttrKind.EVENT_LISTENER) return; 20 | 21 | const { typeB } = extractBindingTypes(assignment, context); 22 | 23 | // Make sure that the expression given to the event listener binding a function or an object with "handleEvent" property. 24 | if (!isTypeBindableToEventListener(typeB)) { 25 | context.report({ 26 | location: rangeFromHtmlNodeAttr(htmlAttr), 27 | message: `You are setting up an event listener with a non-callable type '${typeToString(typeB)}'` 28 | }); 29 | } 30 | } 31 | }; 32 | 33 | export default rule; 34 | 35 | /** 36 | * Returns if this type can be used in a event listener binding 37 | * @param type 38 | */ 39 | function isTypeBindableToEventListener(type: SimpleType): boolean { 40 | // Return "true" if the type has a call signature 41 | if ("call" in type && type.call != null) { 42 | return true; 43 | } 44 | 45 | // Callable types can be used in the binding 46 | if (isAssignableToSimpleTypeKind(type, ["FUNCTION", "METHOD", "UNKNOWN"], { matchAny: true })) { 47 | return true; 48 | } 49 | 50 | return validateType(type, simpleType => { 51 | switch (simpleType.kind) { 52 | // Object types with attributes for the setup function of the event listener can be used 53 | case "OBJECT": 54 | case "INTERFACE": { 55 | // The "handleEvent" property must be present 56 | const handleEventFunction = simpleType.members != null ? simpleType.members.find(m => m.name === "handleEvent") : undefined; 57 | 58 | // The "handleEvent" property must be callable 59 | if (handleEventFunction != null) { 60 | return isTypeBindableToEventListener(handleEventFunction.type); 61 | } 62 | } 63 | } 64 | 65 | return undefined; 66 | }); 67 | } 68 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/rules/no-nullable-attribute-binding.ts: -------------------------------------------------------------------------------- 1 | import { isAssignableToSimpleTypeKind, typeToString } from "ts-simple-type"; 2 | import { HtmlNodeAttrAssignmentKind } from "../analyze/types/html-node/html-node-attr-assignment-types.js"; 3 | import { HtmlNodeAttrKind } from "../analyze/types/html-node/html-node-attr-types.js"; 4 | import { RuleModule } from "../analyze/types/rule/rule-module.js"; 5 | import { rangeFromHtmlNodeAttr } from "../analyze/util/range-util.js"; 6 | import { extractBindingTypes } from "./util/type/extract-binding-types.js"; 7 | 8 | /** 9 | * This rule validates that "null" and "undefined" types are not bound in an attribute binding. 10 | */ 11 | const rule: RuleModule = { 12 | id: "no-nullable-attribute-binding", 13 | meta: { 14 | priority: "high" 15 | }, 16 | visitHtmlAssignment(assignment, context) { 17 | // Only validate "expression" kind bindings. 18 | if (assignment.kind !== HtmlNodeAttrAssignmentKind.EXPRESSION) return; 19 | 20 | // Only validate "attribute" bindings because these will coerce null|undefined to a string. 21 | const { htmlAttr } = assignment; 22 | if (htmlAttr.kind !== HtmlNodeAttrKind.ATTRIBUTE) return; 23 | 24 | const { typeB } = extractBindingTypes(assignment, context); 25 | const isAssignableToNull = isAssignableToSimpleTypeKind(typeB, "NULL"); 26 | 27 | // Test if removing "undefined" or "null" from typeB would work and suggest using "ifDefined". 28 | if (isAssignableToNull || isAssignableToSimpleTypeKind(typeB, "UNDEFINED")) { 29 | context.report({ 30 | location: rangeFromHtmlNodeAttr(htmlAttr), 31 | message: `This attribute binds the type '${typeToString(typeB)}' which can end up binding the string '${ 32 | isAssignableToNull ? "null" : "undefined" 33 | }'.`, 34 | fixMessage: "Use the 'ifDefined' directive?", 35 | fix: () => ({ 36 | message: `Use the 'ifDefined' directive.`, 37 | actions: [{ kind: "changeAssignment", assignment, newValue: `ifDefined(${assignment.expression.getText()})` }] 38 | }) 39 | }); 40 | } 41 | } 42 | }; 43 | export default rule; 44 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/rules/no-unclosed-tag.ts: -------------------------------------------------------------------------------- 1 | import { RuleModule } from "../analyze/types/rule/rule-module.js"; 2 | import { isCustomElementTagName } from "../analyze/util/is-valid-name.js"; 3 | import { rangeFromHtmlNode } from "../analyze/util/range-util.js"; 4 | 5 | /** 6 | * This rule validates that all tags are closed properly. 7 | */ 8 | const rule: RuleModule = { 9 | id: "no-unclosed-tag", 10 | meta: { 11 | priority: "low" 12 | }, 13 | visitHtmlNode(htmlNode, context) { 14 | if (!htmlNode.selfClosed && htmlNode.location.endTag == null) { 15 | // Report specifically that a custom element cannot be self closing 16 | // if the user is trying to close a custom element. 17 | const isCustomElement = isCustomElementTagName(htmlNode.tagName); 18 | 19 | context.report({ 20 | location: rangeFromHtmlNode(htmlNode), 21 | message: `This tag isn't closed.${isCustomElement ? " Custom elements cannot be self closing." : ""}` 22 | }); 23 | } 24 | 25 | return; 26 | } 27 | }; 28 | 29 | export default rule; 30 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/rules/no-unintended-mixed-binding.ts: -------------------------------------------------------------------------------- 1 | import { HtmlNodeAttrAssignmentKind } from "../analyze/types/html-node/html-node-attr-assignment-types.js"; 2 | import { HtmlNodeAttrKind } from "../analyze/types/html-node/html-node-attr-types.js"; 3 | import { RuleModule } from "../analyze/types/rule/rule-module.js"; 4 | import { rangeFromHtmlNodeAttr } from "../analyze/util/range-util.js"; 5 | 6 | const CONTROL_CHARACTERS = ["'", '"', "}", "/"]; 7 | 8 | /** 9 | * This rule validates that bindings are not followed by certain characters that indicate typos. 10 | * 11 | * Examples: 12 | * 13 | * 14 | * 15 | */ 16 | const rule: RuleModule = { 17 | id: "no-unintended-mixed-binding", 18 | meta: { 19 | priority: "high" 20 | }, 21 | visitHtmlAssignment(assignment, context) { 22 | // Check only mixed bindings 23 | if (assignment.kind !== HtmlNodeAttrAssignmentKind.MIXED) { 24 | return; 25 | } 26 | 27 | // Only check mixed bindings with 2 values 28 | if (assignment.values.length !== 2) { 29 | return; 30 | } 31 | 32 | // Event listener binding ignores mixed bindings. 33 | // This kind of binding only uses the first expression present in the mixed binding. 34 | if (assignment.htmlAttr.kind === HtmlNodeAttrKind.EVENT_LISTENER) { 35 | return; 36 | } 37 | 38 | // Ensure the last value is a string literal 39 | const secondAssignment = assignment.values[1]; 40 | if (typeof secondAssignment !== "string") { 41 | return; 42 | } 43 | 44 | // Report error if the string literal is one of the control characters 45 | if (CONTROL_CHARACTERS.includes(secondAssignment)) { 46 | const quoteChar = secondAssignment === "'" ? '"' : "'"; 47 | 48 | const message = (() => { 49 | switch (secondAssignment) { 50 | case "/": 51 | return `This binding is directly followed by a '/' which is probably unintended.`; 52 | default: 53 | return `This binding is directly followed by an unmatched ${quoteChar}${secondAssignment}${quoteChar} which is probably unintended.`; 54 | } 55 | })(); 56 | 57 | context.report({ 58 | location: rangeFromHtmlNodeAttr(assignment.htmlAttr), 59 | message 60 | }); 61 | } 62 | 63 | return; 64 | } 65 | }; 66 | 67 | export default rule; 68 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/rules/no-unknown-tag-name.ts: -------------------------------------------------------------------------------- 1 | import { HtmlNodeKind } from "../analyze/types/html-node/html-node-types.js"; 2 | import { RuleModule } from "../analyze/types/rule/rule-module.js"; 3 | import { findBestStringMatch } from "../analyze/util/find-best-match.js"; 4 | import { rangeFromHtmlNode } from "../analyze/util/range-util.js"; 5 | 6 | /** 7 | * This rule checks that all tag names used in a template are defined. 8 | */ 9 | const rule: RuleModule = { 10 | id: "no-unknown-tag-name", 11 | meta: { 12 | priority: "low" 13 | }, 14 | visitHtmlNode(htmlNode, context) { 15 | const { htmlStore, config } = context; 16 | 17 | // Don't validate style and svg yet 18 | if (htmlNode.kind !== HtmlNodeKind.NODE) return; 19 | 20 | // Get the html tag from the html store 21 | const htmlTag = htmlStore.getHtmlTag(htmlNode); 22 | 23 | // Add diagnostics if the tag couldn't be found (not defined anywhere) 24 | if (htmlTag == null) { 25 | // Find a suggested name in the set of defined tag names. Maybe this tag name is a typo? 26 | const suggestedName = findBestStringMatch( 27 | htmlNode.tagName, 28 | Array.from(htmlStore.getGlobalTags()).map(tag => tag.tagName) 29 | ); 30 | 31 | // Build a suggestion text 32 | let suggestion = `Check that you've imported the element, and that it's declared on the HTMLElementTagNameMap.`; 33 | 34 | if (!config.dontSuggestConfigChanges) { 35 | suggestion += ` If it can't be imported, consider adding it to the 'globalTags' plugin configuration or disabling the 'no-unknown-tag' rule.`; 36 | } 37 | 38 | context.report({ 39 | location: rangeFromHtmlNode(htmlNode), 40 | message: `Unknown tag <${htmlNode.tagName}>.`, 41 | fixMessage: suggestedName == null ? undefined : `Did you mean <${suggestedName}>?`, 42 | suggestion, 43 | fix: 44 | suggestedName == null 45 | ? undefined 46 | : () => ({ 47 | message: `Change tag name to '${suggestedName}'`, 48 | actions: [ 49 | { 50 | kind: "changeTagName", 51 | htmlNode, 52 | newName: suggestedName 53 | } 54 | ] 55 | }) 56 | }); 57 | } 58 | 59 | return; 60 | } 61 | }; 62 | 63 | export default rule; 64 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/rules/util/directive/is-lit-directive.ts: -------------------------------------------------------------------------------- 1 | import { SimpleType } from "ts-simple-type"; 2 | 3 | const partTypeNames: ReadonlySet = new Set([ 4 | "Part", 5 | "NodePart", 6 | "AttributePart", 7 | "BooleanAttributePart", 8 | "PropertyPart", 9 | "EventPart" 10 | ]); 11 | 12 | /** 13 | * Checks whether a type is a lit-html 1.x or Lit 2 directive. 14 | */ 15 | export function isLitDirective(type: SimpleType): boolean { 16 | return isLit1Directive(type) || isLit2Directive(type); 17 | } 18 | 19 | /** 20 | * Checks whether a type is a lit-html 1.x directive. 21 | */ 22 | export function isLit1Directive(type: SimpleType): boolean { 23 | switch (type.kind) { 24 | case "ALIAS": 25 | return type.name === "DirectiveFn" || isLit1Directive(type.target); 26 | case "OBJECT": 27 | return type.call != null && isLit1Directive(type.call); 28 | case "FUNCTION": { 29 | // We expect a directive to be a function with at least one argument that 30 | // returns void. 31 | if ( 32 | type.kind !== "FUNCTION" || 33 | type.parameters == null || 34 | type.parameters.length === 0 || 35 | type.returnType == null || 36 | type.returnType.kind !== "VOID" 37 | ) { 38 | return false; 39 | } 40 | // And that one argument must all be lit Part types. 41 | const firstArg = type.parameters[0].type; 42 | if (firstArg.kind === "UNION") { 43 | return firstArg.types.every(t => partTypeNames.has(t.name)); 44 | } 45 | return partTypeNames.has(firstArg.name); 46 | } 47 | case "GENERIC_ARGUMENTS": 48 | // Test for the built in type from lit-html: Directive 49 | return (type.target.kind === "FUNCTION" && type.target.name === "Directive") || isLit1Directive(type.target); 50 | default: 51 | return false; 52 | } 53 | } 54 | 55 | /** 56 | * Checks whether a type is a Lit 2 directive. 57 | */ 58 | export function isLit2Directive(type: SimpleType): boolean { 59 | switch (type.kind) { 60 | case "INTERFACE": { 61 | return type.name === "DirectiveResult"; 62 | } 63 | case "GENERIC_ARGUMENTS": { 64 | return isLit2Directive(type.target); 65 | } 66 | default: 67 | return false; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/rules/util/type/is-assignable-in-boolean-binding.ts: -------------------------------------------------------------------------------- 1 | import { SimpleType, typeToString } from "ts-simple-type"; 2 | import { HtmlNodeAttr } from "../../../analyze/types/html-node/html-node-attr-types.js"; 3 | import { RuleModuleContext } from "../../../analyze/types/rule/rule-module-context.js"; 4 | import { rangeFromHtmlNodeAttr } from "../../../analyze/util/range-util.js"; 5 | import { isAssignableToType } from "./is-assignable-to-type.js"; 6 | 7 | export function isAssignableInBooleanBinding( 8 | htmlAttr: HtmlNodeAttr, 9 | { typeA, typeB }: { typeA: SimpleType; typeB: SimpleType }, 10 | context: RuleModuleContext 11 | ): boolean | undefined { 12 | // Test if the user is trying to use ? modifier on a non-boolean type. 13 | if (!isAssignableToType({ typeA: { kind: "UNION", types: [{ kind: "BOOLEAN" }, { kind: "UNDEFINED" }, { kind: "NULL" }] }, typeB }, context)) { 14 | context.report({ 15 | location: rangeFromHtmlNodeAttr(htmlAttr), 16 | message: `Type '${typeToString(typeB)}' is not assignable to 'boolean'` 17 | }); 18 | 19 | return false; 20 | } 21 | 22 | // Test if the user is trying to use the ? modifier on a non-boolean type. 23 | if (!isAssignableToType({ typeA, typeB: { kind: "BOOLEAN" } }, context)) { 24 | context.report({ 25 | location: rangeFromHtmlNodeAttr(htmlAttr), 26 | message: `You are using a boolean binding on a non boolean type '${typeToString(typeA)}'`, 27 | fix: () => { 28 | const htmlAttrTarget = context.htmlStore.getHtmlAttrTarget(htmlAttr); 29 | const newModifier = htmlAttrTarget == null ? "." : ""; 30 | 31 | return { 32 | message: newModifier.length === 0 ? `Remove '${htmlAttr.modifier || ""}' modifier` : `Use '${newModifier}' modifier instead`, 33 | actions: [ 34 | { 35 | kind: "changeAttributeModifier", 36 | htmlAttr, 37 | newModifier 38 | } 39 | ] 40 | }; 41 | } 42 | }); 43 | 44 | return false; 45 | } 46 | 47 | return true; 48 | } 49 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/rules/util/type/is-assignable-in-element-binding.ts: -------------------------------------------------------------------------------- 1 | import { SimpleType, typeToString } from "ts-simple-type"; 2 | import { HtmlNodeAttr } from "../../../analyze/types/html-node/html-node-attr-types.js"; 3 | import { RuleModuleContext } from "../../../analyze/types/rule/rule-module-context.js"; 4 | import { rangeFromHtmlNodeAttr } from "../../../analyze/util/range-util.js"; 5 | import { isLit2Directive, isLit1Directive } from "../directive/is-lit-directive.js"; 6 | 7 | /** 8 | * Checks that the type represents a Lit 2 directive, which is the only valid 9 | * value for element expressions. 10 | */ 11 | export function isAssignableInElementBinding(htmlAttr: HtmlNodeAttr, type: SimpleType, context: RuleModuleContext): boolean | undefined { 12 | // TODO (justinfagnani): is there a better way to determine if the 13 | // type *contains* any, rather than *is* any? 14 | if (!isLit2Directive(type) && type.kind !== "ANY") { 15 | if (isLit1Directive(type)) { 16 | context.report({ 17 | location: rangeFromHtmlNodeAttr(htmlAttr), 18 | message: `Type '${typeToString(type)}' is a lit-html 1.0 directive, not a Lit 2 directive'` 19 | }); 20 | } else { 21 | context.report({ 22 | location: rangeFromHtmlNodeAttr(htmlAttr), 23 | message: `Type '${typeToString(type)}' is not a Lit 2 directive'` 24 | }); 25 | } 26 | return false; 27 | } 28 | 29 | return true; 30 | } 31 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/rules/util/type/is-assignable-in-property-binding.ts: -------------------------------------------------------------------------------- 1 | import { SimpleType, typeToString } from "ts-simple-type"; 2 | import { HtmlNodeAttr } from "../../../analyze/types/html-node/html-node-attr-types.js"; 3 | import { RuleModuleContext } from "../../../analyze/types/rule/rule-module-context.js"; 4 | import { rangeFromHtmlNodeAttr } from "../../../analyze/util/range-util.js"; 5 | import { isAssignableBindingUnderSecuritySystem } from "./is-assignable-binding-under-security-system.js"; 6 | import { isAssignableToType } from "./is-assignable-to-type.js"; 7 | 8 | export function isAssignableInPropertyBinding( 9 | htmlAttr: HtmlNodeAttr, 10 | { typeA, typeB }: { typeA: SimpleType; typeB: SimpleType }, 11 | context: RuleModuleContext 12 | ): boolean | undefined { 13 | const securitySystemResult = isAssignableBindingUnderSecuritySystem(htmlAttr, { typeA, typeB }, context); 14 | if (securitySystemResult !== undefined) { 15 | // The security diagnostics take precedence here, 16 | // and we should not do any more checking. 17 | return securitySystemResult; 18 | } 19 | 20 | if (!isAssignableToType({ typeA, typeB }, context)) { 21 | context.report({ 22 | location: rangeFromHtmlNodeAttr(htmlAttr), 23 | message: `Type '${typeToString(typeB)}' is not assignable to '${typeToString(typeA)}'` 24 | }); 25 | 26 | return false; 27 | } 28 | 29 | return true; 30 | } 31 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/rules/util/type/is-assignable-to-type.ts: -------------------------------------------------------------------------------- 1 | import { isAssignableToType as _isAssignableToType, SimpleType, SimpleTypeComparisonOptions } from "ts-simple-type"; 2 | import { RuleModuleContext } from "../../../analyze/types/rule/rule-module-context.js"; 3 | 4 | export function isAssignableToType( 5 | { typeA, typeB }: { typeA: SimpleType; typeB: SimpleType }, 6 | context: RuleModuleContext, 7 | options?: SimpleTypeComparisonOptions 8 | ): boolean { 9 | const inJsFile = context.file.fileName.endsWith(".js"); 10 | const expandedOptions = { 11 | ...(inJsFile ? { strict: false } : {}), 12 | options: context.ts, 13 | ...(options || {}) 14 | }; 15 | return _isAssignableToType(typeA, typeB, context.program, expandedOptions); 16 | } 17 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/lib/rules/util/type/remove-undefined-from-type.ts: -------------------------------------------------------------------------------- 1 | import { isAssignableToSimpleTypeKind, SimpleType } from "ts-simple-type"; 2 | 3 | export function removeUndefinedFromType(type: SimpleType): SimpleType { 4 | switch (type.kind) { 5 | case "ALIAS": 6 | return { 7 | ...type, 8 | target: removeUndefinedFromType(type.target) 9 | }; 10 | case "UNION": 11 | return { 12 | ...type, 13 | types: type.types.filter(t => !isAssignableToSimpleTypeKind(t, "UNDEFINED")) 14 | }; 15 | } 16 | 17 | return type; 18 | } 19 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/scripts/check-version.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as path from "path"; 3 | 4 | const pkg = require("../package.json"); 5 | const { version } = pkg; 6 | 7 | const constantsPath = path.resolve("src/lib/analyze/constants.ts"); 8 | const constantsSource = fs.readFileSync(constantsPath, "utf-8"); 9 | 10 | if (!constantsSource.includes(`"${version}"`)) { 11 | // eslint-disable-next-line no-console 12 | console.log(`\nExpected src/lib/analyze/constants.ts to contain the current version "${version}"`); 13 | process.exit(1); 14 | } 15 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/test/helpers/assert.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext } from "ava"; 2 | import { LitAnalyzerRuleId } from "../../lib/analyze/lit-analyzer-config.js"; 3 | import { LitDiagnostic } from "../../lib/analyze/types/lit-diagnostic.js"; 4 | 5 | export function hasDiagnostic(t: ExecutionContext, diagnostics: LitDiagnostic[], ruleName: LitAnalyzerRuleId): void { 6 | if (diagnostics.length !== 1) { 7 | prettyLogDiagnostics(t, diagnostics); 8 | } 9 | t.is(diagnostics.length, 1); 10 | t.is(diagnostics[0].source, ruleName); 11 | } 12 | 13 | export function hasNoDiagnostics(t: ExecutionContext, diagnostics: LitDiagnostic[]): void { 14 | if (diagnostics.length !== 0) { 15 | prettyLogDiagnostics(t, diagnostics); 16 | } 17 | t.is(diagnostics.length, 0); 18 | } 19 | 20 | function prettyLogDiagnostics(t: ExecutionContext, diagnostics: LitDiagnostic[]) { 21 | t.log(diagnostics.map(diagnostic => `${diagnostic.source}: ${diagnostic.message}`)); 22 | } 23 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/test/helpers/generate-test-file.ts: -------------------------------------------------------------------------------- 1 | import { TestFile } from "./compile-files.js"; 2 | 3 | export function makeElement({ properties, slots }: { properties?: string[]; slots?: string[] }): TestFile { 4 | return { 5 | fileName: "my-element.ts", 6 | text: ` 7 | /** 8 | ${(slots || []).map(slot => ` * @slot ${slot}`)} 9 | */ 10 | class MyElement extends HTMLElement { 11 | ${(properties || []).map(prop => `@property() ${prop}`).join("\n")} 12 | }; 13 | customElements.define("my-element", MyElement); 14 | ` 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/test/helpers/parse-html.ts: -------------------------------------------------------------------------------- 1 | import { Node, TaggedTemplateExpression } from "typescript"; 2 | import { HtmlDocument } from "../../lib/analyze/parse/document/text-document/html-document/html-document.js"; 3 | import { parseHtmlDocument } from "../../lib/analyze/parse/document/text-document/html-document/parse-html-document.js"; 4 | import { compileFiles } from "./compile-files.js"; 5 | import { getCurrentTsModule } from "./ts-test.js"; 6 | 7 | export function parseHtml(html: string): HtmlDocument { 8 | const { sourceFile } = compileFiles([`html\`${html}\``]); 9 | const taggedTemplateExpression = findTaggedTemplateExpression(sourceFile)!; 10 | return parseHtmlDocument(taggedTemplateExpression); 11 | } 12 | 13 | function findTaggedTemplateExpression(node: Node): TaggedTemplateExpression | undefined { 14 | if (getCurrentTsModule().isTaggedTemplateExpression(node)) { 15 | return node; 16 | } 17 | 18 | return node.forEachChild(findTaggedTemplateExpression); 19 | } 20 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/test/parser/css-document/css-substitutions.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext } from "ava"; 2 | import { CssDocument } from "../../../lib/analyze/parse/document/text-document/css-document/css-document.js"; 3 | import { VirtualAstCssDocument } from "../../../lib/analyze/parse/document/virtual-document/virtual-css-document.js"; 4 | import { findTaggedTemplates } from "../../../lib/analyze/parse/tagged-template/find-tagged-templates.js"; 5 | 6 | import { compileFiles } from "../../helpers/compile-files.js"; 7 | import { tsTest } from "../../helpers/ts-test.js"; 8 | 9 | function createCssDocument(testFile: string) { 10 | const { sourceFile } = compileFiles(testFile); 11 | const taggedTemplates = findTaggedTemplates(sourceFile, ["css"]); 12 | return new CssDocument(new VirtualAstCssDocument(taggedTemplates[0])); 13 | } 14 | 15 | function isTemplateText(t: ExecutionContext, text: string, testFile: string) { 16 | t.is(text, createCssDocument(testFile).virtualDocument.text); 17 | } 18 | 19 | tsTest("Substitute for template followed by percent", t => { 20 | isTemplateText(t, "{ div { transform-origin: 0000% 0000%; } }", "css`{ div { transform-origin: ${x}% ${y}%; } }`"); 21 | }); 22 | 23 | tsTest("Substitute for template last in css list", t => { 24 | isTemplateText(t, "{ div { border: 2px solid ________; } }", "css`{ div { border: 2px solid ${COLOR}; } }`"); 25 | }); 26 | 27 | tsTest("Substitute for template first in css list", t => { 28 | isTemplateText(t, "{ div { border: ________ solid #ffffff; } }", "css`{ div { border: ${WIDTH} solid #ffffff; } }`"); 29 | }); 30 | 31 | tsTest("Substitute for template middle in css list", t => { 32 | isTemplateText(t, "{ div { border: 2px ________ #ffffff; } }", "css`{ div { border: 2px ${STYLE} #ffffff; } }`"); 33 | }); 34 | 35 | tsTest("Substitute for template css key-value pair", t => { 36 | isTemplateText(t, "{ div { $_:_______________________; } }", "css`{ div { ${unsafeCSS('color: red')}; } }`"); 37 | }); 38 | 39 | tsTest("Substitute for template css value only", t => { 40 | isTemplateText(t, "{ div { color: ___________________; } }", "css`{ div { color: ${unsafeCSS('red')}; } }`"); 41 | }); 42 | 43 | tsTest("Substitute for template css key only", t => { 44 | isTemplateText(t, "{ div { $____________________: red; } }", "css`{ div { ${unsafeCSS('color')}: red; } }`"); 45 | }); 46 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/test/rules/no-boolean-in-attribute-binding.ts: -------------------------------------------------------------------------------- 1 | import { getDiagnostics } from "../helpers/analyze.js"; 2 | import { hasDiagnostic, hasNoDiagnostics } from "../helpers/assert.js"; 3 | import { tsTest } from "../helpers/ts-test.js"; 4 | 5 | tsTest("Non-boolean-binding with an empty string value is valid", t => { 6 | const { diagnostics } = getDiagnostics('html``', { rules: { "no-boolean-in-attribute-binding": true } }); 7 | hasNoDiagnostics(t, diagnostics); 8 | }); 9 | 10 | tsTest("Non-boolean-binding with a boolean type expression is not valid", t => { 11 | const { diagnostics } = getDiagnostics('html``', { rules: { "no-boolean-in-attribute-binding": true } }); 12 | hasDiagnostic(t, diagnostics, "no-boolean-in-attribute-binding"); 13 | }); 14 | 15 | tsTest("Non-boolean-binding on a boolean type attribute with a non-boolean type expression is not valid", t => { 16 | const { diagnostics } = getDiagnostics('html``', { rules: { "no-boolean-in-attribute-binding": true } }); 17 | hasDiagnostic(t, diagnostics, "no-boolean-in-attribute-binding"); 18 | }); 19 | 20 | tsTest("Boolean assigned to 'true|'false' doesn't emit 'no-boolean-in-attribute-binding' warning", t => { 21 | const { diagnostics } = getDiagnostics('let b: boolean = true; html``', { 22 | rules: { "no-boolean-in-attribute-binding": true } 23 | }); 24 | hasNoDiagnostics(t, diagnostics); 25 | }); 26 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/test/rules/no-complex-attribute-binding.ts: -------------------------------------------------------------------------------- 1 | import { getDiagnostics } from "../helpers/analyze.js"; 2 | import { hasDiagnostic, hasNoDiagnostics } from "../helpers/assert.js"; 3 | import { makeElement } from "../helpers/generate-test-file.js"; 4 | import { tsTest } from "../helpers/ts-test.js"; 5 | 6 | tsTest("Complex types are not assignable using an attribute binding", t => { 7 | const { diagnostics } = getDiagnostics('html``'); 8 | hasDiagnostic(t, diagnostics, "no-complex-attribute-binding"); 9 | }); 10 | 11 | tsTest("Complex types are assignable using a property binding", t => { 12 | const { diagnostics } = getDiagnostics('html``'); 13 | hasNoDiagnostics(t, diagnostics); 14 | }); 15 | 16 | tsTest("Primitives are not assignable to complex type using an attribute binding", t => { 17 | const { diagnostics } = getDiagnostics([makeElement({ properties: ["complex = {foo: string}"] }), 'html``']); 18 | hasDiagnostic(t, diagnostics, "no-complex-attribute-binding"); 19 | }); 20 | 21 | tsTest("Complex types are assignable using property binding", t => { 22 | const { diagnostics } = getDiagnostics([ 23 | makeElement({ properties: ["complex = {foo: string}"] }), 24 | 'html``' 25 | ]); 26 | hasNoDiagnostics(t, diagnostics); 27 | }); 28 | 29 | tsTest("Don't check for the assignability of complex types in attribute bindings if the type is a custom lit directive", t => { 30 | const { diagnostics } = getDiagnostics( 31 | 'type Part = {}; type ifExists = (val: any) => (part: Part) => void; html``' 32 | ); 33 | hasNoDiagnostics(t, diagnostics); 34 | }); 35 | 36 | tsTest("Ignore element expressions", t => { 37 | const { diagnostics } = getDiagnostics("html``", { rules: { "no-incompatible-type-binding": false } }); 38 | hasNoDiagnostics(t, diagnostics); 39 | }); 40 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/test/rules/no-invalid-boolean-binding.ts: -------------------------------------------------------------------------------- 1 | import { getDiagnostics } from "../helpers/analyze.js"; 2 | import { hasDiagnostic, hasNoDiagnostics } from "../helpers/assert.js"; 3 | import { tsTest } from "../helpers/ts-test.js"; 4 | 5 | tsTest.skip("Emits 'no-invalid-boolean-binding' diagnostic when a boolean binding is used on a non-boolean type", t => { 6 | const { diagnostics } = getDiagnostics('html``'); 7 | hasDiagnostic(t, diagnostics, "no-invalid-boolean-binding"); 8 | }); 9 | 10 | tsTest.skip("Emits no 'no-invalid-boolean-binding' diagnostic when the rule is turned off", t => { 11 | const { diagnostics } = getDiagnostics('html``', { rules: { "no-invalid-boolean-binding": "off" } }); 12 | hasNoDiagnostics(t, diagnostics); 13 | }); 14 | 15 | tsTest.skip("Emits no 'no-invalid-boolean-binding' diagnostic when a boolean binding is used on a boolean type", t => { 16 | const { diagnostics } = getDiagnostics('html``'); 17 | hasNoDiagnostics(t, diagnostics); 18 | }); 19 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/test/rules/no-legacy-attribute.ts: -------------------------------------------------------------------------------- 1 | import { getDiagnostics } from "../helpers/analyze.js"; 2 | import { hasDiagnostic, hasNoDiagnostics } from "../helpers/assert.js"; 3 | import { tsTest } from "../helpers/ts-test.js"; 4 | 5 | tsTest("Don't report legacy attributes when 'no-legacy-attribute' is turned off", t => { 6 | const { diagnostics } = getDiagnostics("html``", { rules: { "no-legacy-attribute": false } }); 7 | hasNoDiagnostics(t, diagnostics); 8 | }); 9 | 10 | tsTest("Report legacy attributes on known element", t => { 11 | const { diagnostics } = getDiagnostics("html``", { rules: { "no-legacy-attribute": true } }); 12 | hasDiagnostic(t, diagnostics, "no-legacy-attribute"); 13 | }); 14 | 15 | tsTest("Report legacy attribute values on known element", t => { 16 | const { diagnostics } = getDiagnostics('html``', { rules: { "no-legacy-attribute": true } }); 17 | hasDiagnostic(t, diagnostics, "no-legacy-attribute"); 18 | }); 19 | 20 | tsTest("Don't report non-legacy boolean attributes", t => { 21 | const { diagnostics } = getDiagnostics("html``", { rules: { "no-legacy-attribute": true } }); 22 | hasNoDiagnostics(t, diagnostics); 23 | }); 24 | 25 | tsTest("Don't report non-legacy attributes", t => { 26 | const { diagnostics } = getDiagnostics("html``", { rules: { "no-legacy-attribute": true } }); 27 | hasNoDiagnostics(t, diagnostics); 28 | }); 29 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/test/rules/no-missing-element-type-definition.ts: -------------------------------------------------------------------------------- 1 | import { getDiagnostics } from "../helpers/analyze.js"; 2 | import { hasDiagnostic, hasNoDiagnostics } from "../helpers/assert.js"; 3 | import { tsTest } from "../helpers/ts-test.js"; 4 | 5 | tsTest("'no-missing-element-type-definition' reports diagnostic when element is not in HTMLElementTagNameMap", t => { 6 | const { diagnostics } = getDiagnostics( 7 | ` 8 | class MyElement extends HTMLElement { }; 9 | customElements.define("my-element", MyElement) 10 | `, 11 | { 12 | rules: { "no-missing-element-type-definition": true } 13 | } 14 | ); 15 | 16 | hasDiagnostic(t, diagnostics, "no-missing-element-type-definition"); 17 | }); 18 | 19 | tsTest("'no-missing-element-type-definition' reports no diagnostic when element is not in HTMLElementTagNameMap", t => { 20 | const { diagnostics } = getDiagnostics( 21 | ` 22 | class MyElement extends HTMLElement { }; 23 | customElements.define("my-element", MyElement) 24 | declare global { 25 | interface HTMLElementTagNameMap { 26 | "my-element": MyElement 27 | } 28 | } 29 | `, 30 | { 31 | rules: { "no-missing-element-type-definition": true } 32 | } 33 | ); 34 | 35 | hasNoDiagnostics(t, diagnostics); 36 | }); 37 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/test/rules/no-nullable-attribute-binding.ts: -------------------------------------------------------------------------------- 1 | import { getDiagnostics } from "../helpers/analyze.js"; 2 | import { hasDiagnostic, hasNoDiagnostics } from "../helpers/assert.js"; 3 | import { makeElement } from "../helpers/generate-test-file.js"; 4 | import { tsTest } from "../helpers/ts-test.js"; 5 | 6 | tsTest("Cannot assign 'undefined' in attribute binding", t => { 7 | const { diagnostics } = getDiagnostics('html``'); 8 | hasDiagnostic(t, diagnostics, "no-nullable-attribute-binding"); 9 | }); 10 | 11 | tsTest("Can assign 'undefined' in property binding", t => { 12 | const { diagnostics } = getDiagnostics([ 13 | makeElement({ slots: ["foo: number | undefined"] }), 14 | 'html``' 15 | ]); 16 | hasNoDiagnostics(t, diagnostics); 17 | }); 18 | 19 | tsTest("Cannot assign 'null' in attribute binding", t => { 20 | const { diagnostics } = getDiagnostics('html``'); 21 | hasDiagnostic(t, diagnostics, "no-nullable-attribute-binding"); 22 | }); 23 | 24 | tsTest("Can assign 'null' in property binding", t => { 25 | const { diagnostics } = getDiagnostics('html``'); 26 | hasNoDiagnostics(t, diagnostics); 27 | }); 28 | 29 | tsTest("Message for 'null' in attribute detects null type correctly", t => { 30 | const { diagnostics } = getDiagnostics('html``'); 31 | hasDiagnostic(t, diagnostics, "no-nullable-attribute-binding"); 32 | 33 | t.true(diagnostics[0].message.includes("can end up binding the string 'null'")); 34 | }); 35 | 36 | tsTest("Message for 'undefined' in attribute detects undefined type correctly", t => { 37 | const { diagnostics } = getDiagnostics('html``'); 38 | hasDiagnostic(t, diagnostics, "no-nullable-attribute-binding"); 39 | 40 | t.true(diagnostics[0].message.includes("can end up binding the string 'undefined'")); 41 | }); 42 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/test/rules/no-property-visibility-mismatch.ts: -------------------------------------------------------------------------------- 1 | import { getDiagnostics } from "../helpers/analyze.js"; 2 | import { hasDiagnostic, hasNoDiagnostics } from "../helpers/assert.js"; 3 | import { tsTest } from "../helpers/ts-test.js"; 4 | import { TestFile } from "../helpers/compile-files.js"; 5 | 6 | function makeTestElement({ properties }: { properties?: Array<{ visibility: string; name: string; internal: boolean }> }): TestFile { 7 | return { 8 | fileName: "my-element.ts", 9 | text: ` 10 | class MyElement extends HTMElement { 11 | ${(properties || []) 12 | .map(({ name, visibility, internal }) => `@${internal ? "internalProperty" : "property"}() ${visibility} ${name}: any;`) 13 | .join("\n")} 14 | }; 15 | customElements.define("my-element", MyElement); 16 | ` 17 | }; 18 | } 19 | 20 | tsTest("Report public @internalProperty properties", t => { 21 | const { diagnostics } = getDiagnostics( 22 | makeTestElement({ 23 | properties: [{ name: "foo", visibility: "public", internal: true }] 24 | }), 25 | { 26 | rules: { "no-property-visibility-mismatch": true } 27 | } 28 | ); 29 | hasDiagnostic(t, diagnostics, "no-property-visibility-mismatch"); 30 | }); 31 | 32 | tsTest("Report private @property properties", t => { 33 | const { diagnostics } = getDiagnostics( 34 | makeTestElement({ 35 | properties: [{ name: "foo", visibility: "private", internal: false }] 36 | }), 37 | { 38 | rules: { "no-property-visibility-mismatch": true } 39 | } 40 | ); 41 | hasDiagnostic(t, diagnostics, "no-property-visibility-mismatch"); 42 | }); 43 | 44 | tsTest("Don't report regular public properties", t => { 45 | const { diagnostics } = getDiagnostics( 46 | makeTestElement({ 47 | properties: [{ name: "foo", visibility: "public", internal: false }] 48 | }), 49 | { 50 | rules: { "no-property-visibility-mismatch": true } 51 | } 52 | ); 53 | hasNoDiagnostics(t, diagnostics); 54 | }); 55 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/test/rules/no-unclosed-tag.ts: -------------------------------------------------------------------------------- 1 | import { getDiagnostics } from "../helpers/analyze.js"; 2 | import { hasDiagnostic, hasNoDiagnostics } from "../helpers/assert.js"; 3 | import { tsTest } from "../helpers/ts-test.js"; 4 | 5 | tsTest("Report unclosed tags", t => { 6 | const { diagnostics } = getDiagnostics("html`
`", { rules: { "no-unclosed-tag": true } }); 7 | hasDiagnostic(t, diagnostics, "no-unclosed-tag"); 8 | }); 9 | 10 | tsTest("Don't report self closed tags", t => { 11 | const { diagnostics } = getDiagnostics("html``", { rules: { "no-unclosed-tag": true } }); 12 | hasNoDiagnostics(t, diagnostics); 13 | }); 14 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/test/rules/no-unintended-mixed-binding.ts: -------------------------------------------------------------------------------- 1 | import { getDiagnostics } from "../helpers/analyze.js"; 2 | import { hasDiagnostic, hasNoDiagnostics } from "../helpers/assert.js"; 3 | import { tsTest } from "../helpers/ts-test.js"; 4 | 5 | tsTest('Report mixed binding with expression and "', t => { 6 | const { diagnostics } = getDiagnostics('html``'); 7 | hasDiagnostic(t, diagnostics, "no-unintended-mixed-binding"); 8 | }); 9 | 10 | tsTest("Report mixed binding with expression and '", t => { 11 | const { diagnostics } = getDiagnostics("html``"); 12 | hasDiagnostic(t, diagnostics, "no-unintended-mixed-binding"); 13 | }); 14 | 15 | tsTest("Report mixed binding with expression and }", t => { 16 | const { diagnostics } = getDiagnostics("html``"); 17 | hasDiagnostic(t, diagnostics, "no-unintended-mixed-binding"); 18 | }); 19 | 20 | tsTest("Report mixed binding with expression and /", t => { 21 | const { diagnostics } = getDiagnostics("html``"); 22 | hasDiagnostic(t, diagnostics, "no-unintended-mixed-binding"); 23 | }); 24 | 25 | tsTest("Don't report mixed binding with expression and %", t => { 26 | const { diagnostics } = getDiagnostics("html``"); 27 | hasNoDiagnostics(t, diagnostics); 28 | }); 29 | 30 | tsTest("Don't report mixed event listener binding directly followed by /", t => { 31 | const { diagnostics } = getDiagnostics("html``"); 32 | hasNoDiagnostics(t, diagnostics); 33 | }); 34 | 35 | tsTest("Report mixed binding with expression and } inside quotes", t => { 36 | const { diagnostics } = getDiagnostics('html``'); 37 | hasDiagnostic(t, diagnostics, "no-unintended-mixed-binding"); 38 | }); 39 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/test/rules/no-unknown-attribute.ts: -------------------------------------------------------------------------------- 1 | import { getDiagnostics } from "../helpers/analyze.js"; 2 | import { hasDiagnostic, hasNoDiagnostics } from "../helpers/assert.js"; 3 | import { tsTest } from "../helpers/ts-test.js"; 4 | 5 | tsTest("Don't report unknown attributes when 'no-unknown-attribute' is turned off", t => { 6 | const { diagnostics } = getDiagnostics("html``", { rules: { "no-unknown-attribute": false } }); 7 | hasNoDiagnostics(t, diagnostics); 8 | }); 9 | 10 | tsTest("Report unknown attributes on known element", t => { 11 | const { diagnostics } = getDiagnostics("html``", { rules: { "no-unknown-attribute": true } }); 12 | hasDiagnostic(t, diagnostics, "no-unknown-attribute"); 13 | }); 14 | 15 | tsTest("Don't report unknown attributes", t => { 16 | const { diagnostics } = getDiagnostics("html``", { rules: { "no-unknown-attribute": true } }); 17 | hasNoDiagnostics(t, diagnostics); 18 | }); 19 | 20 | tsTest("Don't report unknown attributes on unknown element", t => { 21 | const { diagnostics } = getDiagnostics("html``", { 22 | rules: { "no-unknown-attribute": true, "no-unknown-tag-name": false } 23 | }); 24 | hasNoDiagnostics(t, diagnostics); 25 | }); 26 | 27 | tsTest("Don't report unknown data- attributes", t => { 28 | const { diagnostics } = getDiagnostics("html``", { rules: { "no-unknown-attribute": true } }); 29 | hasNoDiagnostics(t, diagnostics); 30 | }); 31 | 32 | tsTest("Don't report element expressions", t => { 33 | const { diagnostics } = getDiagnostics("html``", { rules: { "no-unknown-attribute": true } }); 34 | hasNoDiagnostics(t, diagnostics); 35 | }); 36 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/test/rules/no-unknown-event.ts: -------------------------------------------------------------------------------- 1 | import { getDiagnostics } from "../helpers/analyze.js"; 2 | import { hasDiagnostic, hasNoDiagnostics } from "../helpers/assert.js"; 3 | import { tsTest } from "../helpers/ts-test.js"; 4 | 5 | tsTest("Don't report unknown events when 'no-unknown-event' is turned off", t => { 6 | const { diagnostics } = getDiagnostics("html``", { rules: { "no-unknown-event": false } }); 7 | hasNoDiagnostics(t, diagnostics); 8 | }); 9 | 10 | tsTest("Report unknown events on known element", t => { 11 | const { diagnostics } = getDiagnostics("html``", { rules: { "no-unknown-event": true } }); 12 | hasDiagnostic(t, diagnostics, "no-unknown-event"); 13 | }); 14 | 15 | tsTest("Don't report known events", t => { 16 | const { diagnostics } = getDiagnostics("html``", { rules: { "no-unknown-event": true } }); 17 | hasNoDiagnostics(t, diagnostics); 18 | }); 19 | 20 | tsTest("Don't report unknown events on unknown element", t => { 21 | const { diagnostics } = getDiagnostics("html``", { 22 | rules: { "no-unknown-event": true, "no-unknown-tag-name": false } 23 | }); 24 | hasNoDiagnostics(t, diagnostics); 25 | }); 26 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/test/rules/no-unknown-property.ts: -------------------------------------------------------------------------------- 1 | import { getDiagnostics } from "../helpers/analyze.js"; 2 | import { hasDiagnostic, hasNoDiagnostics } from "../helpers/assert.js"; 3 | import { makeElement } from "../helpers/generate-test-file.js"; 4 | import { tsTest } from "../helpers/ts-test.js"; 5 | 6 | tsTest("Don't report unknown properties when 'no-unknown-property' is turned off", t => { 7 | const { diagnostics } = getDiagnostics("html``", { rules: { "no-unknown-property": false } }); 8 | hasNoDiagnostics(t, diagnostics); 9 | }); 10 | 11 | tsTest("Report unknown properties on known element", t => { 12 | const { diagnostics } = getDiagnostics("html``", { rules: { "no-unknown-property": true } }); 13 | hasDiagnostic(t, diagnostics, "no-unknown-property"); 14 | }); 15 | 16 | tsTest("Don't report known properties", t => { 17 | const { diagnostics } = getDiagnostics([makeElement({ properties: ["foo: string"] }), "html``"], { 18 | rules: { "no-unknown-property": true } 19 | }); 20 | hasNoDiagnostics(t, diagnostics); 21 | }); 22 | 23 | tsTest("Don't report unknown properties on unknown element", t => { 24 | const { diagnostics } = getDiagnostics("html``", { 25 | rules: { "no-unknown-property": true, "no-unknown-tag-name": false } 26 | }); 27 | hasNoDiagnostics(t, diagnostics); 28 | }); 29 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/test/rules/no-unknown-slot.ts: -------------------------------------------------------------------------------- 1 | import { getDiagnostics } from "../helpers/analyze.js"; 2 | import { hasDiagnostic, hasNoDiagnostics } from "../helpers/assert.js"; 3 | import { makeElement } from "../helpers/generate-test-file.js"; 4 | import { tsTest } from "../helpers/ts-test.js"; 5 | 6 | tsTest("Report unknown slot name", t => { 7 | const { diagnostics } = getDiagnostics([makeElement({ slots: ["foo"] }), "html`
`"], { 8 | rules: { "no-unknown-slot": true } 9 | }); 10 | hasDiagnostic(t, diagnostics, "no-unknown-slot"); 11 | }); 12 | 13 | tsTest("Don't report known slot name", t => { 14 | const { diagnostics } = getDiagnostics([makeElement({ slots: ["foo"] }), "html`
`"], { 15 | rules: { "no-unknown-slot": true } 16 | }); 17 | hasNoDiagnostics(t, diagnostics); 18 | }); 19 | 20 | tsTest("Don't report known, unnamed slot name", t => { 21 | const { diagnostics } = getDiagnostics([makeElement({ slots: [""] }), "html`
`"], { 22 | rules: { "no-unknown-slot": true } 23 | }); 24 | hasNoDiagnostics(t, diagnostics); 25 | }); 26 | 27 | tsTest("Report missing slot attribute", t => { 28 | const { diagnostics } = getDiagnostics([makeElement({ slots: ["foo"] }), "html`
`"], { 29 | rules: { "no-unknown-slot": true } 30 | }); 31 | hasDiagnostic(t, diagnostics, "no-unknown-slot"); 32 | }); 33 | -------------------------------------------------------------------------------- /packages/lit-analyzer/src/test/rules/no-unknown-tag-name.ts: -------------------------------------------------------------------------------- 1 | import { getDiagnostics } from "../helpers/analyze.js"; 2 | import { hasDiagnostic, hasNoDiagnostics } from "../helpers/assert.js"; 3 | import { makeElement } from "../helpers/generate-test-file.js"; 4 | import { tsTest } from "../helpers/ts-test.js"; 5 | 6 | tsTest("Report unknown custom elements", t => { 7 | const { diagnostics } = getDiagnostics("html``", { rules: { "no-unknown-tag-name": true } }); 8 | hasDiagnostic(t, diagnostics, "no-unknown-tag-name"); 9 | }); 10 | 11 | tsTest("Don't report known built in elements", t => { 12 | const { diagnostics } = getDiagnostics("html`
`", { rules: { "no-unknown-tag-name": true } }); 13 | hasNoDiagnostics(t, diagnostics); 14 | }); 15 | 16 | tsTest("Report unknown built in elements", t => { 17 | const { diagnostics } = getDiagnostics("html``", { rules: { "no-unknown-tag-name": true } }); 18 | hasDiagnostic(t, diagnostics, "no-unknown-tag-name"); 19 | }); 20 | 21 | tsTest("Don't report known custom elements found in other file", t => { 22 | const { diagnostics } = getDiagnostics([makeElement({}), "html``"], { rules: { "no-unknown-tag-name": true } }); 23 | hasNoDiagnostics(t, diagnostics); 24 | }); 25 | 26 | tsTest("Don't report known custom element", t => { 27 | const { diagnostics } = getDiagnostics( 28 | "class MyElement extends HTMLElement {}; customElements.define('my-element', MyElement); html``", 29 | { 30 | rules: { "no-unknown-tag-name": true } 31 | } 32 | ); 33 | hasNoDiagnostics(t, diagnostics); 34 | }); 35 | -------------------------------------------------------------------------------- /packages/lit-analyzer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["./src"], 4 | "exclude": [], 5 | "compilerOptions": { 6 | "rootDir": "./src", 7 | "outDir": "./", 8 | "resolveJsonModule": true, 9 | "tsBuildInfoFile": "./.tsbuildinfo" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/ts-lit-plugin/index.js: -------------------------------------------------------------------------------- 1 | // A TypeScript compiler plugin must do a CJS style default export, but 2 | // we can't express that in proper ESM, so this hand-written JS 3 | // file bridges the difference. 4 | 5 | module.exports = require("./lib/index").init; 6 | -------------------------------------------------------------------------------- /packages/ts-lit-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ts-lit-plugin", 3 | "version": "2.0.2", 4 | "description": "Typescript plugin that adds type checking and code completion to lit-html", 5 | "author": "runem", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/runem/lit-analyzer.git" 10 | }, 11 | "keywords": [ 12 | "lit-html", 13 | "lit", 14 | "lit-element", 15 | "javascript", 16 | "typescript", 17 | "web components", 18 | "web", 19 | "components", 20 | "tagged", 21 | "template" 22 | ], 23 | "scripts": { 24 | "watch": "tsc --watch", 25 | "build": "wireit", 26 | "test": "wireit", 27 | "readme": "readme generate -i readme.blueprint.md -c readme.config.json" 28 | }, 29 | "wireit": { 30 | "build": { 31 | "dependencies": [ 32 | "../lit-analyzer:build" 33 | ], 34 | "command": "tsc --build --pretty", 35 | "files": [ 36 | "src/**/*", 37 | "tsconfig.json" 38 | ], 39 | "output": [ 40 | "lib", 41 | "./tsbuildinfo" 42 | ], 43 | "clean": "if-file-deleted" 44 | }, 45 | "test": { 46 | "dependencies": [ 47 | "build" 48 | ] 49 | } 50 | }, 51 | "main": "index.js", 52 | "files": [ 53 | "/lib/", 54 | "/html-documentation/" 55 | ], 56 | "dependencies": { 57 | "lit-analyzer": "^2.0.1", 58 | "web-component-analyzer": "^2.0.0" 59 | }, 60 | "devDependencies": { 61 | "@types/node": "^14.0.13", 62 | "esbuild": "^0.14.34", 63 | "rimraf": "^3.0.2", 64 | "typescript": "~5.2.2", 65 | "wireit": "^0.1.1" 66 | }, 67 | "contributors": [ 68 | { 69 | "name": "Rune Mehlsen", 70 | "url": "https://twitter.com/runemehlsen", 71 | "img": "https://avatars2.githubusercontent.com/u/5372940?s=460&v=4" 72 | }, 73 | { 74 | "name": "Andreas Mehlsen", 75 | "url": "https://twitter.com/andreasmehlsen", 76 | "img": "https://avatars1.githubusercontent.com/u/6267397?s=460&v=4" 77 | }, 78 | { 79 | "name": "You?", 80 | "img": "https://joeschmoe.io/api/v1/random", 81 | "url": "https://github.com/runem/lit-analyzer/blob/master/CONTRIBUTING.md" 82 | } 83 | ] 84 | } 85 | -------------------------------------------------------------------------------- /packages/ts-lit-plugin/readme.blueprint.md: -------------------------------------------------------------------------------- 1 | {{ template:title }} 2 | {{ template:description }} 3 | {{ template:badges }} 4 | 5 | {{ load:./readme/header.md }} 6 | 7 | {{ load:./readme/install.md }} 8 | {{ load:./readme/config.md }} 9 | {{ load:./../../docs/readme/rules.md }} 10 | {{ load:./../../docs/readme/jsdoc.md }} 11 | 12 | {{ template:contributors }} 13 | {{ template:license }} 14 | -------------------------------------------------------------------------------- /packages/ts-lit-plugin/readme.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "line": "rainbow", 3 | "ids": { 4 | "github": "runem/lit-analyzer", 5 | "npm": "ts-lit-plugin" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/ts-lit-plugin/readme/config.md: -------------------------------------------------------------------------------- 1 | ## Configuration 2 | 3 | You can configure this plugin through your `tsconfig.json`. 4 | 5 | ### Example 6 | 7 | 8 | ```json 9 | { 10 | "compilerOptions": { 11 | "plugins": [ 12 | { 13 | "name": "ts-lit-plugin", 14 | "strict": true, 15 | "rules": { 16 | "no-unknown-tag-name": "off", 17 | "no-unknown-event": "warn" 18 | } 19 | } 20 | ] 21 | } 22 | } 23 | ``` 24 | 25 | ### Available options 26 | 27 | {{ load:./../../docs/readme/config-table.md }} 28 | -------------------------------------------------------------------------------- /packages/ts-lit-plugin/readme/header.md: -------------------------------------------------------------------------------- 1 |

2 | Lit plugin GIF 3 |

4 | -------------------------------------------------------------------------------- /packages/ts-lit-plugin/readme/install.md: -------------------------------------------------------------------------------- 1 | ## Installation 2 | 3 | First, install the plugin: 4 | 5 | 6 | ```bash 7 | npm install ts-lit-plugin -D 8 | ``` 9 | 10 | Then add a `plugins` section to your [`tsconfig.json`](http://www.typescriptlang.org/docs/handbook/tsconfig-json.html): 11 | 12 | 13 | ```json 14 | { 15 | "compilerOptions": { 16 | "plugins": [ 17 | { 18 | "name": "ts-lit-plugin" 19 | } 20 | ] 21 | } 22 | } 23 | ``` 24 | 25 | Finally, restart you Typescript Language Service, and you should start getting diagnostics from `ts-lit-plugin`. 26 | 27 | **Note:** 28 | 29 | - If you use Visual Studio Code you can also install the [lit-plugin](https://marketplace.visualstudio.com/items?itemName=runem.lit-plugin) extension. 30 | - If you would rather use a CLI, you can install the [lit-analyzer](https://github.com/runem/lit-analyzer/blob/master/packages/lit-analyzer). 31 | -------------------------------------------------------------------------------- /packages/ts-lit-plugin/src/bazel-plugin.ts: -------------------------------------------------------------------------------- 1 | import { DefaultLitAnalyzerContext, LitAnalyzer, LitAnalyzerConfig, LitAnalyzerContext, makeConfig } from "lit-analyzer"; 2 | import ts, { Diagnostic } from "typescript"; 3 | import { translateDiagnostics } from "./ts-lit-plugin/translate/translate-diagnostics.js"; 4 | 5 | // See https://github.com/bazelbuild/rules_typescript/blob/master/internal/tsc_wrapped/plugin_api.ts 6 | interface DiagnosticPlugin { 7 | readonly name: string; 8 | getDiagnostics(sourceFile: ts.SourceFile): Readonly[]; 9 | } 10 | 11 | /** 12 | * Implements bazel's DiagnosticPlugin interface, so that we can run 13 | * the ts-lit-plugin checks as part of bazel compilation. 14 | */ 15 | export class Plugin implements DiagnosticPlugin { 16 | public readonly name = "lit"; 17 | 18 | private readonly context: LitAnalyzerContext; 19 | private readonly analyzer: LitAnalyzer; 20 | 21 | constructor(program: ts.Program, config: LitAnalyzerConfig) { 22 | this.name = "lit"; 23 | const context = new DefaultLitAnalyzerContext({ 24 | getProgram() { 25 | return program; 26 | } 27 | }); 28 | context.updateConfig(makeConfig(config)); 29 | this.context = context; 30 | this.analyzer = new LitAnalyzer(context); 31 | } 32 | 33 | getDiagnostics(sourceFile: ts.SourceFile): Diagnostic[] { 34 | const litDiagnostics = this.analyzer.getDiagnosticsInFile(sourceFile); 35 | 36 | const diagnostics = translateDiagnostics(litDiagnostics, sourceFile, this.context); 37 | for (const diagnostic of diagnostics) { 38 | if (diagnostic.category === ts.DiagnosticCategory.Warning) { 39 | // In bazel something is either an error that breaks the build, or 40 | // we don't want to report it at all. 41 | diagnostic.category = ts.DiagnosticCategory.Error; 42 | } 43 | } 44 | return diagnostics; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/ts-lit-plugin/src/ts-lit-plugin/lit-plugin-context.ts: -------------------------------------------------------------------------------- 1 | import { DefaultLitAnalyzerContext, LitAnalyzerConfig } from "lit-analyzer"; 2 | import { logger } from "../logger.js"; 3 | 4 | export class LitPluginContext extends DefaultLitAnalyzerContext { 5 | logger = logger; 6 | 7 | public updateConfig(config: LitAnalyzerConfig): void { 8 | const hasChangedLogging = config.logging !== "off" && (this.config.logging !== config.logging || this.config.cwd !== config.cwd); 9 | 10 | // Setup logging 11 | this.logger.cwd = config.cwd; 12 | 13 | super.updateConfig(config); 14 | 15 | if (hasChangedLogging) { 16 | this.logger.resetLogs(); 17 | } 18 | 19 | logger.debug("Updating the config", config); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/ts-lit-plugin/src/ts-lit-plugin/translate/translate-code-fixes.ts: -------------------------------------------------------------------------------- 1 | import { LitCodeFix, LitCodeFixAction } from "lit-analyzer"; 2 | import { CodeFixAction, FileTextChanges, SourceFile } from "typescript"; 3 | import { translateRange } from "./translate-range.js"; 4 | 5 | export function translateCodeFixes(codeFixes: LitCodeFix[], file: SourceFile): CodeFixAction[] { 6 | return codeFixes.map(codeFix => translateCodeFix(file, codeFix)); 7 | } 8 | 9 | export function translateCodeFix(file: SourceFile, codeFix: LitCodeFix): CodeFixAction { 10 | return { 11 | fixName: codeFix.name, 12 | description: codeFix.message, 13 | changes: codeFix.actions.map(action => translateCodeFixAction(file, action)) 14 | }; 15 | } 16 | 17 | function translateCodeFixAction(file: SourceFile, action: LitCodeFixAction): FileTextChanges { 18 | return { 19 | fileName: file.fileName, 20 | textChanges: [ 21 | { 22 | span: translateRange(action.range), 23 | newText: action.newText 24 | } 25 | ] 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /packages/ts-lit-plugin/src/ts-lit-plugin/translate/translate-completion-details.ts: -------------------------------------------------------------------------------- 1 | import { LitCompletionDetails } from "lit-analyzer"; 2 | import { CompletionEntryDetails } from "typescript"; 3 | import { LitPluginContext } from "../lit-plugin-context.js"; 4 | 5 | export function translateCompletionDetails(completionDetails: LitCompletionDetails, context: LitPluginContext): CompletionEntryDetails { 6 | return { 7 | name: completionDetails.name, 8 | kind: context.ts.ScriptElementKind.label, 9 | kindModifiers: "", 10 | displayParts: [ 11 | { 12 | text: completionDetails.primaryInfo, 13 | kind: "text" 14 | } 15 | ], 16 | documentation: 17 | completionDetails.secondaryInfo == null 18 | ? [] 19 | : [ 20 | { 21 | kind: "text", 22 | text: completionDetails.secondaryInfo 23 | } 24 | ] 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /packages/ts-lit-plugin/src/ts-lit-plugin/translate/translate-completions.ts: -------------------------------------------------------------------------------- 1 | import { LitCompletion } from "lit-analyzer"; 2 | import { CompletionEntry, CompletionInfo } from "typescript"; 3 | import { translateRange } from "./translate-range.js"; 4 | import { translateTargetKind } from "./translate-target-kind.js"; 5 | 6 | export function translateCompletions(completions: LitCompletion[]): CompletionInfo | undefined { 7 | const entries = completions.map(completion => translateCompletion(completion)); 8 | 9 | if (entries != null && entries.length > 0) { 10 | return { 11 | isGlobalCompletion: false, 12 | isMemberCompletion: false, 13 | isNewIdentifierLocation: false, 14 | entries 15 | }; 16 | } 17 | 18 | return undefined; 19 | } 20 | 21 | function translateCompletion(completion: LitCompletion): CompletionEntry { 22 | const { importance, kind, insert, name, range } = completion; 23 | 24 | return { 25 | name, 26 | kind: translateTargetKind(kind), 27 | kindModifiers: completion.kindModifiers, 28 | sortText: completion.sortText != null ? completion.sortText : importance === "high" ? "0" : importance === "medium" ? "1" : "2", 29 | insertText: insert, 30 | ...(range != null ? { replacementSpan: translateRange(range) } : {}) 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /packages/ts-lit-plugin/src/ts-lit-plugin/translate/translate-definition.ts: -------------------------------------------------------------------------------- 1 | import { LitDefinition, LitDefinitionTarget } from "lit-analyzer"; 2 | import { DefinitionInfo, DefinitionInfoAndBoundSpan } from "typescript"; 3 | import { tsModule } from "../../ts-module.js"; 4 | import { translateRange } from "./translate-range.js"; 5 | 6 | export function translateDefinition(definition: LitDefinition): DefinitionInfoAndBoundSpan { 7 | return { 8 | definitions: definition.targets.map(translateDefinitionInfo), 9 | textSpan: translateRange(definition.fromRange) 10 | }; 11 | } 12 | 13 | function translateDefinitionInfo(target: LitDefinitionTarget): DefinitionInfo { 14 | let targetStart: number; 15 | let targetEnd: number; 16 | let targetFileName: string; 17 | let targetName: string; 18 | 19 | switch (target.kind) { 20 | case "range": 21 | targetStart = target.range.start; 22 | targetEnd = target.range.end; 23 | targetFileName = target.sourceFile.fileName; 24 | targetName = target.name || ""; 25 | break; 26 | 27 | case "node": { 28 | const node = target.node; 29 | targetStart = node.getStart(); 30 | targetEnd = node.getEnd(); 31 | targetFileName = node.getSourceFile().fileName; 32 | targetName = target.name || (tsModule.ts.isIdentifier(node) ? node.getText() : ""); 33 | break; 34 | } 35 | } 36 | 37 | return { 38 | name: targetName, 39 | textSpan: { 40 | start: targetStart, 41 | length: targetEnd - targetStart 42 | }, 43 | fileName: targetFileName, 44 | containerName: targetFileName, 45 | kind: tsModule.ts.ScriptElementKind.memberVariableElement, 46 | containerKind: tsModule.ts.ScriptElementKind.functionElement 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /packages/ts-lit-plugin/src/ts-lit-plugin/translate/translate-diagnostics.ts: -------------------------------------------------------------------------------- 1 | import { LitAnalyzerContext, LitDiagnostic } from "lit-analyzer"; 2 | import { DiagnosticMessageChain, DiagnosticWithLocation, SourceFile } from "typescript"; 3 | import { translateRange } from "./translate-range.js"; 4 | 5 | export function translateDiagnostics(reports: LitDiagnostic[], file: SourceFile, context: LitAnalyzerContext): DiagnosticWithLocation[] { 6 | return reports.map(report => translateDiagnostic(report, file, context)); 7 | } 8 | 9 | /** 10 | * Convert a diagnostic into a "message" that can be shown to the user. 11 | * @param diagnostic 12 | */ 13 | function getMessageTextFromDiagnostic(diagnostic: LitDiagnostic): string { 14 | return `${diagnostic.message}${diagnostic.fixMessage == null ? "" : ` ${diagnostic.fixMessage}`}`; 15 | } 16 | 17 | function translateDiagnostic(diagnostic: LitDiagnostic, file: SourceFile, context: LitAnalyzerContext): DiagnosticWithLocation { 18 | const span = translateRange(diagnostic.location); 19 | 20 | const category = diagnostic.severity === "error" ? context.ts.DiagnosticCategory.Error : context.ts.DiagnosticCategory.Warning; 21 | const code = diagnostic.code ?? 0; 22 | const messageText: string | DiagnosticMessageChain = 23 | !context.config.dontShowSuggestions && diagnostic.suggestion 24 | ? { 25 | messageText: getMessageTextFromDiagnostic(diagnostic), 26 | code, 27 | category, 28 | next: [ 29 | { 30 | messageText: diagnostic.suggestion, 31 | code: 0, 32 | category: context.ts.DiagnosticCategory.Suggestion 33 | } 34 | ] 35 | } 36 | : getMessageTextFromDiagnostic(diagnostic); 37 | 38 | if (Number(context.ts.versionMajorMinor) < 3.6 && typeof messageText !== "string") { 39 | // The format of DiagnosticMessageChain#next changed in 3.6 to be an array. 40 | // This check for backwards compatibility 41 | if (messageText.next != null && Array.isArray(messageText.next)) { 42 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 43 | messageText.next = messageText.next[0] as any; 44 | } 45 | } 46 | 47 | return { 48 | ...span, 49 | file, 50 | messageText, 51 | category, 52 | code, 53 | source: diagnostic.source == null ? undefined : `lit-plugin(${diagnostic.source})` 54 | }; 55 | } 56 | -------------------------------------------------------------------------------- /packages/ts-lit-plugin/src/ts-lit-plugin/translate/translate-format-edits.ts: -------------------------------------------------------------------------------- 1 | import { LitFormatEdit } from "lit-analyzer"; 2 | import * as ts from "typescript"; 3 | import { translateRange } from "./translate-range.js"; 4 | 5 | export function translateFormatEdits(formatEdits: LitFormatEdit[]): ts.TextChange[] { 6 | return formatEdits.map(formatEdit => translateFormatEdit(formatEdit)); 7 | } 8 | 9 | function translateFormatEdit(formatEdit: LitFormatEdit): ts.TextChange { 10 | return { 11 | newText: formatEdit.newText, 12 | span: translateRange(formatEdit.range) 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /packages/ts-lit-plugin/src/ts-lit-plugin/translate/translate-outlining-spans.ts: -------------------------------------------------------------------------------- 1 | import { LitOutliningSpan } from "lit-analyzer"; 2 | import { translateRange } from "./translate-range.js"; 3 | import type * as ts from "typescript"; 4 | 5 | export function translateOutliningSpans(outliningSpans: LitOutliningSpan[]): ts.OutliningSpan[] { 6 | return outliningSpans.map(outliningSpan => translateOutliningSpan(outliningSpan)); 7 | } 8 | 9 | function translateOutliningSpan(outliningSpan: LitOutliningSpan): ts.OutliningSpan { 10 | const span = translateRange(outliningSpan.location); 11 | 12 | return { 13 | autoCollapse: outliningSpan.autoCollapse || false, 14 | textSpan: span, 15 | hintSpan: span, 16 | kind: outliningSpan.kind as unknown as ts.OutliningSpanKind, 17 | bannerText: outliningSpan.bannerText 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /packages/ts-lit-plugin/src/ts-lit-plugin/translate/translate-quick-info.ts: -------------------------------------------------------------------------------- 1 | import { LitQuickInfo } from "lit-analyzer"; 2 | import { QuickInfo } from "typescript"; 3 | import { tsModule } from "../../ts-module.js"; 4 | import { translateRange } from "./translate-range.js"; 5 | 6 | export function translateQuickInfo(quickInfo: LitQuickInfo): QuickInfo { 7 | return { 8 | kind: tsModule.ts.ScriptElementKind.label, 9 | kindModifiers: "", 10 | textSpan: translateRange(quickInfo.range), 11 | displayParts: [ 12 | { 13 | text: quickInfo.primaryInfo, 14 | kind: "text" 15 | } 16 | ], 17 | documentation: 18 | quickInfo.secondaryInfo == null 19 | ? [] 20 | : [ 21 | { 22 | kind: "text", 23 | text: quickInfo.secondaryInfo 24 | } 25 | ] 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /packages/ts-lit-plugin/src/ts-lit-plugin/translate/translate-range.ts: -------------------------------------------------------------------------------- 1 | import { Range } from "lit-analyzer"; 2 | import { TextSpan } from "typescript"; 3 | 4 | export function translateRange(range: Range): TextSpan { 5 | return { 6 | start: range.start, 7 | length: range.end - range.start 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /packages/ts-lit-plugin/src/ts-lit-plugin/translate/translate-rename-info.ts: -------------------------------------------------------------------------------- 1 | import { LitRenameInfo } from "lit-analyzer"; 2 | import { RenameInfo } from "typescript"; 3 | import { translateTargetKind } from "./translate-target-kind.js"; 4 | import { translateRange } from "./translate-range.js"; 5 | 6 | export function translateRenameInfo({ displayName, fullDisplayName, kind, range }: LitRenameInfo): RenameInfo { 7 | const triggerSpan = translateRange(range); 8 | 9 | return { 10 | canRename: true, 11 | kind: translateTargetKind(kind), 12 | kindModifiers: "", 13 | displayName, 14 | fullDisplayName, 15 | triggerSpan 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /packages/ts-lit-plugin/src/ts-lit-plugin/translate/translate-rename-locations.ts: -------------------------------------------------------------------------------- 1 | import { LitRenameLocation } from "lit-analyzer"; 2 | import { translateRange } from "./translate-range.js"; 3 | import { RenameLocation } from "typescript"; 4 | 5 | export function translateRenameLocations(renameLocations: LitRenameLocation[]): RenameLocation[] { 6 | return renameLocations.map(renameLocation => translateRenameLocation(renameLocation)); 7 | } 8 | 9 | function translateRenameLocation({ fileName, prefixText, suffixText, range }: LitRenameLocation): RenameLocation { 10 | const textSpan = translateRange(range); 11 | 12 | return { 13 | textSpan, 14 | fileName, 15 | prefixText, 16 | suffixText 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /packages/ts-lit-plugin/src/ts-lit-plugin/translate/translate-target-kind.ts: -------------------------------------------------------------------------------- 1 | import { LitTargetKind } from "lit-analyzer"; 2 | import { ScriptElementKind } from "typescript"; 3 | import { tsModule } from "../../ts-module.js"; 4 | 5 | export function translateTargetKind(kind: LitTargetKind): ScriptElementKind { 6 | switch (kind) { 7 | case "memberFunctionElement": 8 | return tsModule.ts.ScriptElementKind.memberFunctionElement; 9 | case "functionElement": 10 | return tsModule.ts.ScriptElementKind.functionElement; 11 | case "constructorImplementationElement": 12 | return tsModule.ts.ScriptElementKind.constructorImplementationElement; 13 | case "variableElement": 14 | return tsModule.ts.ScriptElementKind.variableElement; 15 | case "classElement": 16 | return tsModule.ts.ScriptElementKind.classElement; 17 | case "interfaceElement": 18 | return tsModule.ts.ScriptElementKind.interfaceElement; 19 | case "moduleElement": 20 | return tsModule.ts.ScriptElementKind.moduleElement; 21 | case "memberVariableElement": 22 | case "member": 23 | return tsModule.ts.ScriptElementKind.memberVariableElement; 24 | case "constElement": 25 | return tsModule.ts.ScriptElementKind.constElement; 26 | case "enumElement": 27 | return tsModule.ts.ScriptElementKind.enumElement; 28 | case "keyword": 29 | return tsModule.ts.ScriptElementKind.keyword; 30 | case "alias": 31 | return tsModule.ts.ScriptElementKind.alias; 32 | case "label": 33 | return tsModule.ts.ScriptElementKind.label; 34 | default: 35 | return tsModule.ts.ScriptElementKind.unknown; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/ts-lit-plugin/src/ts-module.ts: -------------------------------------------------------------------------------- 1 | import * as tsModuleType from "typescript"; 2 | 3 | export const tsModule: { ts: typeof tsModuleType } = { ts: tsModuleType }; 4 | 5 | export function setTypescriptModule(newModule: typeof tsModuleType): void { 6 | tsModule.ts = newModule; 7 | } 8 | -------------------------------------------------------------------------------- /packages/ts-lit-plugin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["./src"], 4 | "compilerOptions": { 5 | "outDir": "./lib", 6 | "tsBuildInfoFile": "./.tsbuildinfo" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/vscode-lit-plugin/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "runtimeExecutable": "${execPath}", 13 | "args": ["--extensionDevelopmentPath=${workspaceFolder}"], 14 | "outFiles": ["${workspaceFolder}/out/**/*.js"], 15 | "preLaunchTask": "npm: watch" 16 | }, 17 | { 18 | "name": "Extension Tests", 19 | "type": "extensionHost", 20 | "request": "launch", 21 | "runtimeExecutable": "${execPath}", 22 | "args": ["--extensionDevelopmentPath=${workspaceFolder}", "--extensionTestsPath=${workspaceFolder}/out/test"], 23 | "outFiles": ["${workspaceFolder}/out/test/**/*.js"], 24 | "preLaunchTask": "npm: watch" 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /packages/vscode-lit-plugin/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "off" 11 | } 12 | -------------------------------------------------------------------------------- /packages/vscode-lit-plugin/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | //"problemMatcher": "$tsc", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | }, 18 | "problemMatcher": { 19 | "owner": "typescript", 20 | "fileLocation": "relative", 21 | "pattern": { 22 | "regexp": "^([^\\s].*)\\((\\d+|\\d+,\\d+|\\d+,\\d+,\\d+,\\d+)\\):\\s+(error|warning|info)\\s+(TS\\d+)\\s*:\\s*(.*)$", 23 | "file": 1, 24 | "location": 2, 25 | "severity": 3, 26 | "code": 4, 27 | "message": 5 28 | }, 29 | "background": { 30 | "activeOnStart": true, 31 | "beginsPattern": "^.*Starting compilation in watch mode.*$", 32 | "endsPattern": "^.*Found 0 errors. Watching for file changes.*$" 33 | } 34 | } 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /packages/vscode-lit-plugin/LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2018 Rune Mehlsen runemehlsen@gmail.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE 10 | -------------------------------------------------------------------------------- /packages/vscode-lit-plugin/copy-to-built.js: -------------------------------------------------------------------------------- 1 | const { copy, mkdirp, writeFile } = require("fs-extra"); 2 | 3 | /** 4 | * Copy files into the ./built directory. 5 | * 6 | * This is the directory that actually has the final filesystem layout for 7 | * the extension, and to keep the vsix file small we want to only include 8 | * those files that are needed. 9 | * 10 | * Note that ./built/bundle.js is generated directly by esbuild.script.js and 11 | * not copied by this script. 12 | */ 13 | async function main() { 14 | // We don't bundle the typescript compiler into ./built/bundle.js, so we need 15 | // a copy of it. 16 | await mkdirp("./node_modules/typescript/lib"); 17 | await copy("./node_modules/typescript/package.json", "./built/node_modules/typescript/package.json"); 18 | await copy("./node_modules/typescript/lib/typescript.js", "./built/node_modules/typescript/lib/typescript.js"); 19 | await copy("./node_modules/typescript/lib/tsserverlibrary.js", "./built/node_modules/typescript/lib/tsserverlibrary.js"); 20 | 21 | // For the TS compiler plugin, it must be in node modules because that's 22 | // hard coded by the TS compiler's custom module resolution logic. 23 | await mkdirp("./built/node_modules/ts-lit-plugin"); 24 | const tsPluginPackageJson = require("../ts-lit-plugin/package.json"); 25 | // We're only using the bundled version, so the plugin doesn't need any 26 | // dependencies. 27 | tsPluginPackageJson.dependencies = {}; 28 | await writeFile("./built/node_modules/ts-lit-plugin/package.json", JSON.stringify(tsPluginPackageJson, null, 2)); 29 | await copy("../ts-lit-plugin/index.js", "./built/node_modules/ts-lit-plugin/index.js"); 30 | 31 | const pluginPackageJson = require("./package.json"); 32 | // vsce is _very_ picky about the directories in node_modules matching the 33 | // extension's package.json, so we need an entry for ts-lit-plugin or it 34 | // will think that it's extraneous. 35 | pluginPackageJson.dependencies["ts-lit-plugin"] = "*"; 36 | await writeFile("./built/package.json", JSON.stringify(pluginPackageJson, null, 2)); 37 | 38 | // Copy static files used by the extension. 39 | await copy("./LICENSE.md", "./built/LICENSE.md"); 40 | await copy("./README.md", "./built/README.md"); 41 | await copy("./docs", "./built/docs"); 42 | await copy("./syntaxes", "./built/syntaxes"); 43 | await copy("./schemas", "./built/schemas"); 44 | } 45 | 46 | main().catch(e => { 47 | // eslint-disable-next-line no-console 48 | console.error(e); 49 | process.exitCode = 1; 50 | }); 51 | -------------------------------------------------------------------------------- /packages/vscode-lit-plugin/docs/assets/lit-plugin.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runem/lit-analyzer/b0e79a9b369dd48203e846e7c2796c8aa0a371dd/packages/vscode-lit-plugin/docs/assets/lit-plugin.gif -------------------------------------------------------------------------------- /packages/vscode-lit-plugin/docs/assets/lit-plugin@128w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runem/lit-analyzer/b0e79a9b369dd48203e846e7c2796c8aa0a371dd/packages/vscode-lit-plugin/docs/assets/lit-plugin@128w.png -------------------------------------------------------------------------------- /packages/vscode-lit-plugin/docs/assets/lit-plugin@256w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/runem/lit-analyzer/b0e79a9b369dd48203e846e7c2796c8aa0a371dd/packages/vscode-lit-plugin/docs/assets/lit-plugin@256w.png -------------------------------------------------------------------------------- /packages/vscode-lit-plugin/esbuild.script.mjs: -------------------------------------------------------------------------------- 1 | import * as esbuild from "esbuild"; 2 | 3 | await esbuild.build({ 4 | entryPoints: ["src/extension.ts"], 5 | bundle: true, 6 | outfile: "built/bundle.js", 7 | platform: "node", 8 | minify: true, 9 | target: "es2017", 10 | format: "cjs", 11 | color: true, 12 | external: ["vscode", "typescript"], 13 | mainFields: ["module", "main"] 14 | }); 15 | 16 | await esbuild.build({ 17 | entryPoints: ["../ts-lit-plugin/src/index.ts"], 18 | bundle: true, 19 | outfile: "built/node_modules/ts-lit-plugin/lib/index.js", 20 | platform: "node", 21 | external: ["typescript"], 22 | minify: true, 23 | target: "es2017", 24 | format: "cjs", 25 | color: true, 26 | mainFields: ["module", "main"] 27 | }); 28 | -------------------------------------------------------------------------------- /packages/vscode-lit-plugin/readme.blueprint.md: -------------------------------------------------------------------------------- 1 | {{ load:./readme/header.md }} 2 | 3 | {{ load:./readme/install.md }} 4 | {{ load:./../../docs/readme/rules.md }} 5 | {{ load:./readme/config.md }} 6 | {{ load:./readme/other.md }} 7 | {{ load:./../../docs/readme/jsdoc.md }} 8 | {{ load:./readme/feature-comparison.md }} 9 | {{ load:./readme/how.md }} 10 | 11 | {{ template:contributors }} 12 | {{ template:license }} 13 | -------------------------------------------------------------------------------- /packages/vscode-lit-plugin/readme.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "logo": { 3 | "src": "https://user-images.githubusercontent.com/5372940/62078619-4d436880-b24d-11e9-92e0-5fcc43635b7c.png", 4 | "width": 200 5 | }, 6 | "line": "rainbow", 7 | "badges": [ 8 | { 9 | "alt": "Published at vscode marketplace", 10 | "url": "https://marketplace.visualstudio.com/items?itemName=runem.lit-plugin", 11 | "img": "https://vsmarketplacebadge.apphb.com/version/runem.lit-plugin.svg" 12 | }, 13 | { 14 | "alt": "Downloads per Month", 15 | "img": "https://vsmarketplacebadge.apphb.com/downloads-short/runem.lit-plugin.svg?label=vscode-lit-plugin", 16 | "url": "https://marketplace.visualstudio.com/items?itemName=runem.lit-plugin" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /packages/vscode-lit-plugin/readme/config.md: -------------------------------------------------------------------------------- 1 | ## Configuration 2 | 3 | You can configure this plugin by going to `VS Code Settings` > `Extension` > `lit-plugin`. 4 | 5 | **Note:** You can also configure the plugin using a `tsconfig.json` file (see [ts-lit-plugin](https://github.com/runem/lit-analyzer/blob/master/packages/ts-lit-plugin)). 6 | 7 | ### Available options 8 | 9 | {{ load:./../../docs/readme/config-table.md }} 10 | -------------------------------------------------------------------------------- /packages/vscode-lit-plugin/readme/feature-comparison.md: -------------------------------------------------------------------------------- 1 | ## Feature comparison 2 | 3 | This plugin is similar to [vscode-lit-html](https://github.com/mjbvz/vscode-lit-html) on many points. The power of `vscode-lit-html` is that it covers all the basic functionality of HTML in tagged templates, so it's a plugin that can be easily used with other libraries than `lit-html`. However `vscode-lit-plugin` (this one) aims to be a specialized plugin for working with `lit-element / lit-html`, so for example it supports `css` and discovers web components out of the box. 4 | 5 | Below is a comparison table of the two plugins: 6 | 7 | 8 | | Feature | [vscode-lit-html](https://github.com/mjbvz/vscode-lit-html) | [vscode-lit-plugin](https://github.com/runem/vscode-lit-plugin) | 9 | |-------------------------|------------|------------| 10 | | CSS support | ❌ | ✅ | 11 | | Goto definition | ❌ | ✅ | 12 | | Check missing imports | ❌ | ✅ | 13 | | Auto discover web components | ❌ | ✅ | 14 | | Template type checking | ❌ | ✅ | 15 | | Report unknown tag names | ❌ | ✅ | 16 | | Report unknown attrs | ❌ | ✅ | 17 | | Report unknown props | ❌ | ✅ | 18 | | Report unknown events | ❌ | ✅ | 19 | | Report unknown slots | ❌ | ✅ | 20 | | Support for vscode custom data format | ❌| ✅ | 21 | | Refactor tag names | ❌ | ✅ | 22 | | Refactor attr names | ❌ | ❌ | 23 | | Auto close tags | ✅ | ✅ | 24 | | Syntax Highlighting | ✅ | ✅ | 25 | | Completions | ✅ | ✅ | 26 | | Quick info on hover | ✅ | ✅ | 27 | | Code folding | ✅ | ⚠️ (disabled until problem with calling 'program.getSourceFile' is fixed) | 28 | | Formatting | ✅ | ⚠️ (disabled until problem with nested templates is fixed) | 29 | -------------------------------------------------------------------------------- /packages/vscode-lit-plugin/readme/header.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | {{ template:logo }} 4 | 5 | {{ template:description }} 6 | 7 | [![](https://vsmarketplacebadge.apphb.com/version-short/runem.lit-plugin.svg)](https://marketplace.visualstudio.com/items?itemName=runem.lit-plugin) 8 | [![](https://vsmarketplacebadge.apphb.com/downloads-short/runem.lit-plugin.svg)](https://marketplace.visualstudio.com/items?itemName=runem.lit-plugin) 9 | [![](https://vsmarketplacebadge.apphb.com/rating-short/runem.lit-plugin.svg)](https://marketplace.visualstudio.com/items?itemName=runem.lit-plugin) 10 | MIT License 11 | Dependencies 12 | Contributors 13 | 14 | Lit plugin GIF 15 | 16 |
17 | -------------------------------------------------------------------------------- /packages/vscode-lit-plugin/readme/how.md: -------------------------------------------------------------------------------- 1 | ## How does this plugin work? 2 | 3 | All features are provided by these three libraries: 4 | 5 | - **[ts-lit-plugin](https://github.com/runem/lit-analyzer)**: The typescript plugin that powers the logic through the typescript language service (code completion, type checking, eg.). Therefore issues regarding anything but syntax highlighting should be opened in `ts-lit-plugin` and not `vscode-lit-plugin`. 6 | - **[vscode-lit-html](https://github.com/mjbvz/vscode-lit-html)**: Provides highlighting for the html template tag. 7 | - **[vscode-styled-components](https://github.com/styled-components/vscode-styled-components)**: Provides highlighting for the css template tag. 8 | 9 | This library couples it all together and synchronizes relevant settings between vscode and `ts-lit-plugin`. 10 | -------------------------------------------------------------------------------- /packages/vscode-lit-plugin/readme/install.md: -------------------------------------------------------------------------------- 1 | ## Installation 2 | 3 | Simply search for [lit-plugin](https://marketplace.visualstudio.com/items?itemName=runem.lit-plugin) in the vscode marketplace and install the extension. 4 | 5 | **Note**: You can also run `code --install-extension runem.lit-plugin` to install it. 6 | -------------------------------------------------------------------------------- /packages/vscode-lit-plugin/src/test/fixtures/completions.ts: -------------------------------------------------------------------------------- 1 | // Pretending this is the Lit html function 2 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 3 | declare const html: any; 4 | 5 | /** An element to test autocomplete with. */ 6 | class CompleteMe extends HTMLElement { 7 | /** Docs for prop 1. */ 8 | prop1 = ""; 9 | /** Docs for prop 2. */ 10 | prop2 = ""; 11 | /** Docs for prop 3. */ 12 | prop3 = ""; 13 | } 14 | customElements.define("complete-me", CompleteMe); 15 | declare global { 16 | interface HTMLElementTagNameMap { 17 | "complete-me": CompleteMe; 18 | } 19 | } 20 | 21 | // These lines are used as a basis for testing completions, with hardcoded 22 | // line and character offsets in the test file. So if you change this file, 23 | // you'll likely need to update those offsets in ../simple-test.ts 24 | html` 25 | 28 | `; 8 | -------------------------------------------------------------------------------- /packages/vscode-lit-plugin/src/test/fixtures/my-defined-element.ts: -------------------------------------------------------------------------------- 1 | export class MyDefinedElement extends HTMLElement {} 2 | 3 | customElements.define("my-defined-element", MyDefinedElement); 4 | 5 | declare global { 6 | interface HTMLElementTagNameMap { 7 | "my-defined-element": MyDefinedElement; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/vscode-lit-plugin/src/test/fixtures/my-other-element.ts: -------------------------------------------------------------------------------- 1 | export class MyOtherElement extends HTMLElement {} 2 | 3 | customElements.define("my-other-element", MyOtherElement); 4 | 5 | declare global { 6 | interface HTMLElementTagNameMap { 7 | "my-other-element": MyOtherElement; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/vscode-lit-plugin/src/test/fixtures/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/vscode-lit-plugin/src/test/scripts/mocha-driver.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import Mocha from "mocha"; 3 | import glob from "glob"; 4 | 5 | /** 6 | * Runs all tests in src/test that are named like *-test.ts with Mocha. 7 | * 8 | * Called by @vscode/test-electron's runTests function in ./test-runner 9 | * 10 | * Should resolve if the tests pass, reject if any fail. 11 | */ 12 | export async function run(): Promise { 13 | const mocha = new Mocha({ 14 | ui: "tdd", 15 | color: true, 16 | timeout: 60_000 17 | }); 18 | 19 | const testsRoot = path.join(__dirname, ".."); 20 | const files = glob.sync("**/*-test.js", { cwd: testsRoot }); 21 | for (const file of files) { 22 | mocha.addFile(path.resolve(testsRoot, file)); 23 | } 24 | const failures = await new Promise(resolve => { 25 | mocha.run(num => resolve(num)); 26 | }); 27 | if (failures > 0) { 28 | throw new Error(`${failures} tests failed.`); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/vscode-lit-plugin/src/test/scripts/test-runner.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // A script that launches vscode with our extension installed and 4 | // executes ./mocha-driver 5 | 6 | import * as path from "path"; 7 | 8 | import { runTests } from "@vscode/test-electron"; 9 | 10 | async function main() { 11 | try { 12 | if (process.argv.length !== 3) { 13 | throw new Error(`Usage: node ${process.argv[1]} `); 14 | } 15 | // When testing the packaged-and-then-unzipped extension, we'll be handed the path to it. 16 | const extensionPath = path.resolve(process.argv[2]); 17 | const extensionTestsPath = path.resolve(__dirname, "./mocha-driver"); 18 | 19 | const fixturesDir = path.join(__dirname, "..", "..", "..", "src", "test", "fixtures"); 20 | // Download VS Code, unzip it and run the integration test 21 | await runTests({ extensionDevelopmentPath: extensionPath, extensionTestsPath, launchArgs: [fixturesDir] }); 22 | 23 | const inCI = !!process.env.CI; 24 | // For reasons unknown, the test runner sometimes fails to free some 25 | // resource after testing is done when running locally. 26 | // Note that at this point, the test has completed successfully. 27 | if (!inCI) { 28 | setTimeout(function () { 29 | // eslint-disable-next-line no-console 30 | console.log(`[tests completed successfully, but some resource leak is preventing the test runner from exiting, so manually exiting]`); 31 | process.exit(0); 32 | }, 1_000).unref(); 33 | } 34 | } catch (err) { 35 | // eslint-disable-next-line no-console 36 | console.error(err); 37 | process.exit(1); 38 | } 39 | } 40 | 41 | main(); 42 | -------------------------------------------------------------------------------- /packages/vscode-lit-plugin/syntaxes/vscode-lit-html/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Microsoft Corporation 2 | 3 | All rights reserved. 4 | 5 | MIT License 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation 8 | files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, 9 | modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 15 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 16 | BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT 17 | OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /packages/vscode-lit-plugin/syntaxes/vscode-lit-html/README.md: -------------------------------------------------------------------------------- 1 | These JSON files are vendored in from https://github.com/mjbvz/vscode-lit-html/tree/5688ed883c4d5bf36888ae06e4e72a1c2a79d18a/syntaxes and are licensed under the license in ./LICENSE 2 | -------------------------------------------------------------------------------- /packages/vscode-lit-plugin/syntaxes/vscode-lit-html/lit-html-string-injection.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileTypes": [], 3 | "injectionSelector": "L:meta.embedded.block.html meta.tag", 4 | "patterns": [ 5 | { 6 | "include": "source.ts#template-substitution-element" 7 | } 8 | ], 9 | "scopeName": "inline.lit-html.string.injection" 10 | } 11 | -------------------------------------------------------------------------------- /packages/vscode-lit-plugin/syntaxes/vscode-lit-html/lit-html-style-injection.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileTypes": [], 3 | "injectionSelector": "L:meta.embedded.block.html meta.property-value.css", 4 | "patterns": [ 5 | { 6 | "include": "source.ts#template-substitution-element" 7 | } 8 | ], 9 | "scopeName": "inline.lit-html.style.injection" 10 | } 11 | -------------------------------------------------------------------------------- /packages/vscode-lit-plugin/syntaxes/vscode-lit-html/lit-html-svg.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileTypes": [], 3 | "injectionSelector": "L:source.js -comment -(string -meta.embedded), L:source.jsx -comment -(string -meta.embedded), L:source.js.jsx -comment -(string -meta.embedded), L:source.ts -comment -(string -meta.embedded), L:source.tsx -comment -(string -meta.embedded)", 4 | "injections": { 5 | "L:source": { 6 | "patterns": [ 7 | { 8 | "match": "<", 9 | "name": "invalid.illegal.bad-angle-bracket.html" 10 | } 11 | ] 12 | } 13 | }, 14 | "patterns": [ 15 | { 16 | "name": "string.js.taggedTemplate.svg", 17 | "contentName": "meta.embedded.block.svg", 18 | "begin": "(?x)(\\b(?:\\w+\\.)*(?:svg)\\s*)(`)", 19 | "beginCaptures": { 20 | "1": { 21 | "name": "entity.name.function.tagged-template.js" 22 | }, 23 | "2": { 24 | "name": "punctuation.definition.string.template.begin.js" 25 | } 26 | }, 27 | 28 | "end": "(`)", 29 | "endCaptures": { 30 | "0": { 31 | "name": "string.js" 32 | }, 33 | "1": { 34 | "name": "punctuation.definition.string.template.end.js" 35 | } 36 | }, 37 | "patterns": [ 38 | { 39 | "include": "source.ts#template-substitution-element" 40 | }, 41 | { 42 | "include": "text.xml" 43 | } 44 | ] 45 | } 46 | ], 47 | "scopeName": "inline.lit-html-svg" 48 | } 49 | -------------------------------------------------------------------------------- /packages/vscode-lit-plugin/syntaxes/vscode-lit-html/lit-html.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileTypes": [], 3 | "injectionSelector": "L:source.js -comment -(string -meta.embedded), L:source.jsx -comment -(string -meta.embedded), L:source.js.jsx -comment -(string -meta.embedded), L:source.ts -comment -(string -meta.embedded), L:source.tsx -comment -(string -meta.embedded)", 4 | "injections": { 5 | "L:source": { 6 | "patterns": [ 7 | { 8 | "match": "<", 9 | "name": "invalid.illegal.bad-angle-bracket.html" 10 | } 11 | ] 12 | } 13 | }, 14 | "patterns": [ 15 | { 16 | "name": "string.js.taggedTemplate", 17 | "contentName": "meta.embedded.block.html", 18 | "begin": "(?x)(\\b(?:\\w+\\.)*(?:html|raw)\\s*)(`)", 19 | "beginCaptures": { 20 | "1": { 21 | "name": "entity.name.function.tagged-template.js" 22 | }, 23 | "2": { 24 | "name": "punctuation.definition.string.template.begin.js" 25 | } 26 | }, 27 | "end": "(`)", 28 | "endCaptures": { 29 | "0": { 30 | "name": "string.js" 31 | }, 32 | "1": { 33 | "name": "punctuation.definition.string.template.end.js" 34 | } 35 | }, 36 | "patterns": [ 37 | { 38 | "include": "source.ts#template-substitution-element" 39 | }, 40 | { 41 | "include": "text.html.basic" 42 | } 43 | ] 44 | } 45 | ], 46 | "scopeName": "inline.lit-html" 47 | } 48 | -------------------------------------------------------------------------------- /packages/vscode-lit-plugin/syntaxes/vscode-styled-components/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2016 Graham Clark, Julien Poissonnier, 2018 GitHub, Tobias Zimmermann 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /packages/vscode-lit-plugin/syntaxes/vscode-styled-components/README.md: -------------------------------------------------------------------------------- 1 | These JSON files are vendored in from https://github.com/styled-components/vscode-styled-components/tree/cfc992d93b16ddd0bc77b47c469d94e5892b2656/syntaxes 2 | and are licensed under the license in ./LICENSE 3 | -------------------------------------------------------------------------------- /packages/vscode-lit-plugin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["./src"], 4 | "exclude": ["node_modules", "./src/test/fixtures"], 5 | "compilerOptions": { 6 | "module": "commonjs", 7 | "target": "es6", 8 | "outDir": "out", 9 | "lib": ["es2019"], 10 | "sourceMap": true, 11 | "rootDir": "src", 12 | "tsBuildInfoFile": "./.tsbuildinfo" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | useTabs: true, 3 | tabWidth: 2, 4 | singleQuote: false, 5 | printWidth: 150, 6 | bracketSpacing: true, 7 | arrowParens: "avoid", 8 | trailingComma: "none" 9 | }; 10 | -------------------------------------------------------------------------------- /readme.blueprint.md: -------------------------------------------------------------------------------- 1 | {{ template:title }} 2 | {{ template:description }} 3 | 4 | {{ template:badges }} 5 | 6 | This mono-repository consists of the following tools: 7 | 8 | - [**`vscode-lit-plugin`**](/packages/vscode-lit-plugin) VS Code plugin that adds syntax highlighting, type checking and code completion for lit-html. 9 | 10 | - [**`ts-lit-plugin`**](/packages/ts-lit-plugin) Typescript plugin that adds type checking and code completion to lit-html templates. 11 | 12 | - [**`lit-analyzer`**](/packages/lit-analyzer) CLI that analyzes lit-html templates in your code to validate html and type check bindings. 13 | 14 | ## Rules 15 | 16 | You can find a list of all rules [here](https://github.com/runem/lit-analyzer/blob/master/docs/readme/rules.md). 17 | 18 | ## Contributing 19 | 20 | If you are interested in contributing to this repository please read [`contributing.md`](/CONTRIBUTING.md) 21 | 22 | {{ template:contributors }} 23 | {{ template:license }} 24 | -------------------------------------------------------------------------------- /readme.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "line": "rainbow", 3 | "badges": [ 4 | { 5 | "alt": "Downloads per Month", 6 | "img": "https://vsmarketplacebadge.apphb.com/downloads-short/runem.lit-plugin.svg?label=vscode-lit-plugin", 7 | "url": "https://marketplace.visualstudio.com/items?itemName=runem.lit-plugin" 8 | }, 9 | { 10 | "alt": "Downloads per Month", 11 | "img": "https://img.shields.io/npm/dm/lit-analyzer.svg?label=lit-analyzer", 12 | "url": "https://www.npmjs.com/package/lit-analyzer" 13 | }, 14 | { 15 | "alt": "Downloads per Month", 16 | "img": "https://img.shields.io/npm/dm/ts-lit-plugin.svg?label=ts-lit-plugin", 17 | "url": "https://www.npmjs.com/package/ts-lit-plugin" 18 | }, 19 | { 20 | "alt": "Contributors", 21 | "img": "https://img.shields.io/github/contributors/runem/lit-analyzer", 22 | "url": "https://github.com/runem/lit-analyzer/graphs/contributors" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["./src"], 3 | "compilerOptions": { 4 | "target": "es5", 5 | "module": "commonjs", 6 | "lib": ["esnext"], 7 | "outDir": "./lib", 8 | "downlevelIteration": true, 9 | "strict": true, 10 | "esModuleInterop": true, 11 | "experimentalDecorators": true, 12 | "noUnusedLocals": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "noImplicitThis": true, 15 | "noImplicitAny": true, 16 | "noImplicitReturns": true, 17 | "skipLibCheck": true, 18 | "declaration": true, 19 | "declarationMap": true, 20 | "incremental": true 21 | } 22 | } 23 | --------------------------------------------------------------------------------