From 950d0e34e7aaa588d53a0146a71dafdd317f22c1 Mon Sep 17 00:00:00 2001 From: runem Date: Sun, 1 Mar 2020 02:02:17 +0100 Subject: [PATCH] Refactor rule modules and add component analyzer --- .../component-analyzer/component-analyzer.ts | 26 ++ .../analyze/default-lit-analyzer-context.ts | 32 ++- .../css/lit-css-document-analyzer.ts | 33 ++- .../css/lit-css-vscode-service.ts | 31 ++- .../code-fix/code-fixes-for-html-document.ts | 18 ++ .../code-fix/code-fixes-for-html-report.ts | 249 ------------------ .../html/completion/completions-at-offset.ts | 17 +- .../completions-for-html-attr-values.ts | 4 +- .../completion/completions-for-html-attrs.ts | 4 +- .../completion/completions-for-html-nodes.ts | 17 +- .../definition/definition-for-html-attr.ts | 10 +- .../definition/definition-for-html-node.ts | 8 +- .../html/diagnostic/validate-html-document.ts | 38 +-- .../validate-html-node-attr-assignment.ts | 18 -- .../diagnostic/validate-html-node-attr.ts | 18 -- .../html/diagnostic/validate-html-node.ts | 18 -- .../html/lit-html-document-analyzer.ts | 57 ++-- .../html/lit-html-vscode-service.ts | 16 +- .../quick-info/quick-info-for-html-attr.ts | 8 +- .../quick-info/quick-info-for-html-node.ts | 8 +- .../rename-locations-at-offset.ts | 7 +- .../rename-locations-for-tag-name.ts | 31 +-- .../src/analyze/lit-analyzer-config.ts | 24 +- .../src/analyze/lit-analyzer-context.ts | 14 +- .../lit-analyzer/src/analyze/lit-analyzer.ts | 244 ++++++----------- .../parse-documents-in-source-file.ts | 38 +-- .../html-document/html-document.ts | 20 +- .../html-document/parse-html-document.ts | 14 +- .../parse-html-node/parse-html-attribute.ts | 1 + .../parse-html-node/parse-html-context.ts | 2 + .../parse-html-node/parse-html-node.ts | 3 +- .../virtual-document/virtual-ast-document.ts | 20 +- .../virtual-document/virtual-document.ts | 10 +- .../src/analyze/rule-collection.ts | 140 ++++++++++ .../analyze/store/analyzer-document-store.ts | 5 +- .../default-analyzer-document-store.ts | 10 +- .../types/html-node/html-node-attr-types.ts | 4 +- .../types/html-node/html-node-types.ts | 4 +- .../src/analyze/types/lit-code-fix-action.ts | 23 +- .../src/analyze/types/lit-code-fix.ts | 40 +-- .../src/analyze/types/lit-completion.ts | 4 +- .../src/analyze/types/lit-definition.ts | 4 +- .../src/analyze/types/lit-diagnostic.ts | 190 +------------ .../src/analyze/types/lit-format-edit.ts | 4 +- .../src/analyze/types/lit-outlining-span.ts | 4 +- .../src/analyze/types/lit-quick-info.ts | 4 +- .../src/analyze/types/lit-range.ts | 17 -- .../src/analyze/types/lit-rename-info.ts | 4 +- .../src/analyze/types/lit-rename-location.ts | 4 +- .../lit-analyzer/src/analyze/types/range.ts | 22 ++ .../src/analyze/types/rule-module.ts | 21 -- .../src/analyze/types/rule/rule-diagnostic.ts | 10 + .../src/analyze/types/rule/rule-fix-action.ts | 57 ++++ .../src/analyze/types/rule/rule-fix.ts | 6 + .../analyze/types/rule/rule-module-context.ts | 26 ++ .../src/analyze/types/rule/rule-module.ts | 34 +++ .../lit-analyzer/src/analyze/types/rules.ts | 0 .../src/analyze/util/array-util.ts | 24 +- .../lit-analyzer/src/analyze/util/ast-util.ts | 2 +- .../src/analyze/util/general-util.ts | 32 --- .../util/get-position-context-in-document.ts | 7 +- .../src/analyze/util/lit-range-util.ts | 17 -- .../src/analyze/util/range-util.ts | 58 ++++ .../src/analyze/util/rule-diagnostic-util.ts | 19 ++ .../src/analyze/util/rule-fix-util.ts | 133 ++++++++++ .../lit-analyzer/src/analyze/util/str-util.ts | 8 + .../type/is-assignable-in-boolean-binding.ts | 49 ---- .../type/is-assignable-in-property-binding.ts | 39 --- .../lit-analyzer/src/cli/analyze-globs.ts | 4 +- packages/lit-analyzer/src/cli/cli.ts | 4 +- .../cli/format/code-diagnostic-formatter.ts | 18 +- .../cli/format/list-diagnostic-formatter.ts | 5 +- .../src/cli/format/markdown-formatter.ts | 5 +- packages/lit-analyzer/src/cli/format/util.ts | 15 -- packages/lit-analyzer/src/index.ts | 2 +- .../rules/no-boolean-in-attribute-binding.ts | 98 +++---- .../src/rules/no-complex-attribute-binding.ts | 91 ++++--- .../no-expressionless-property-binding.ts | 61 ++--- .../rules/no-incompatible-property-type.ts | 109 +++----- .../src/rules/no-incompatible-type-binding.ts | 30 ++- .../src/rules/no-invalid-attribute-name.ts | 30 +-- .../src/rules/no-invalid-directive-binding.ts | 80 ++---- .../src/rules/no-invalid-property.ts | 235 ----------------- .../src/rules/no-invalid-tag-name.ts | 34 +-- .../src/rules/no-missing-import.ts | 51 ++-- .../src/rules/no-noncallable-event-binding.ts | 34 +-- .../rules/no-nullable-attribute-binding.ts | 67 +++-- .../lit-analyzer/src/rules/no-unclosed-tag.ts | 29 +- .../src/rules/no-unintended-mixed-binding.ts | 29 +- .../src/rules/no-unknown-attribute.ts | 72 +++-- .../src/rules/no-unknown-event.ts | 51 ++-- .../src/rules/no-unknown-property.ts | 59 +++-- .../lit-analyzer/src/rules/no-unknown-slot.ts | 69 +++-- .../src/rules/no-unknown-tag-name.ts | 48 ++-- .../util/directive/get-directive.ts | 8 +- .../util/directive/is-lit-directive.ts | 0 .../util/type/extract-binding-types.ts | 14 +- ...ssignable-binding-under-security-system.ts | 55 ++-- .../is-assignable-in-attribute-binding.ts | 71 ++--- .../type/is-assignable-in-boolean-binding.ts | 47 ++++ .../type/is-assignable-in-property-binding.ts | 30 +++ .../util/type/is-assignable-to-type.ts | 8 +- .../util/type/remove-undefined-from-type.ts | 0 packages/lit-analyzer/test/helpers/assert.ts | 4 +- .../test/helpers/compile-files.ts | 9 +- .../rules/no-complex-attribute-binding.ts | 2 +- .../rules/no-nullable-attribute-binding.ts | 8 +- .../test/rules/security-system.ts | 8 +- .../translate/translate-code-fixes.ts | 56 +--- .../translate/translate-diagnostics.ts | 2 +- .../translate/translate-range.ts | 11 +- .../src/ts-lit-plugin/ts-lit-plugin.ts | 8 +- 112 files changed, 1635 insertions(+), 2119 deletions(-) create mode 100644 packages/lit-analyzer/src/analyze/component-analyzer/component-analyzer.ts create mode 100644 packages/lit-analyzer/src/analyze/document-analyzer/html/code-fix/code-fixes-for-html-document.ts delete mode 100644 packages/lit-analyzer/src/analyze/document-analyzer/html/code-fix/code-fixes-for-html-report.ts delete mode 100644 packages/lit-analyzer/src/analyze/document-analyzer/html/diagnostic/validate-html-node-attr-assignment.ts delete mode 100644 packages/lit-analyzer/src/analyze/document-analyzer/html/diagnostic/validate-html-node-attr.ts delete mode 100644 packages/lit-analyzer/src/analyze/document-analyzer/html/diagnostic/validate-html-node.ts create mode 100644 packages/lit-analyzer/src/analyze/rule-collection.ts delete mode 100644 packages/lit-analyzer/src/analyze/types/lit-range.ts delete mode 100644 packages/lit-analyzer/src/analyze/types/rule-module.ts create mode 100644 packages/lit-analyzer/src/analyze/types/rule/rule-diagnostic.ts create mode 100644 packages/lit-analyzer/src/analyze/types/rule/rule-fix-action.ts create mode 100644 packages/lit-analyzer/src/analyze/types/rule/rule-fix.ts create mode 100644 packages/lit-analyzer/src/analyze/types/rule/rule-module-context.ts create mode 100644 packages/lit-analyzer/src/analyze/types/rule/rule-module.ts delete mode 100644 packages/lit-analyzer/src/analyze/types/rules.ts delete mode 100644 packages/lit-analyzer/src/analyze/util/lit-range-util.ts create mode 100644 packages/lit-analyzer/src/analyze/util/range-util.ts create mode 100644 packages/lit-analyzer/src/analyze/util/rule-diagnostic-util.ts create mode 100644 packages/lit-analyzer/src/analyze/util/rule-fix-util.ts create mode 100644 packages/lit-analyzer/src/analyze/util/str-util.ts delete mode 100644 packages/lit-analyzer/src/analyze/util/type/is-assignable-in-boolean-binding.ts delete mode 100644 packages/lit-analyzer/src/analyze/util/type/is-assignable-in-property-binding.ts delete mode 100644 packages/lit-analyzer/src/rules/no-invalid-property.ts rename packages/lit-analyzer/src/{analyze => rules}/util/directive/get-directive.ts (92%) rename packages/lit-analyzer/src/{analyze => rules}/util/directive/is-lit-directive.ts (100%) rename packages/lit-analyzer/src/{analyze => rules}/util/type/extract-binding-types.ts (89%) rename packages/lit-analyzer/src/{analyze => rules}/util/type/is-assignable-binding-under-security-system.ts (63%) rename packages/lit-analyzer/src/{analyze => rules}/util/type/is-assignable-in-attribute-binding.ts (69%) create mode 100644 packages/lit-analyzer/src/rules/util/type/is-assignable-in-boolean-binding.ts create mode 100644 packages/lit-analyzer/src/rules/util/type/is-assignable-in-property-binding.ts rename packages/lit-analyzer/src/{analyze => rules}/util/type/is-assignable-to-type.ts (58%) rename packages/lit-analyzer/src/{analyze => rules}/util/type/remove-undefined-from-type.ts (100%) diff --git a/packages/lit-analyzer/src/analyze/component-analyzer/component-analyzer.ts b/packages/lit-analyzer/src/analyze/component-analyzer/component-analyzer.ts new file mode 100644 index 00000000..03ef7964 --- /dev/null +++ b/packages/lit-analyzer/src/analyze/component-analyzer/component-analyzer.ts @@ -0,0 +1,26 @@ +import { ComponentDefinition } from "web-component-analyzer"; +import { LitAnalyzerContext } from "../lit-analyzer-context"; +import { LitCodeFix } from "../types/lit-code-fix"; +import { LitDiagnostic } from "../types/lit-diagnostic"; +import { SourceFileRange } from "../types/range"; +import { arrayDefined, arrayFlat } from "../util/array-util"; +import { intersects } from "../util/range-util"; +import { convertRuleDiagnosticToLitDiagnostic } from "../util/rule-diagnostic-util"; +import { converRuleFixToLitCodeFix } from "../util/rule-fix-util"; + +export class ComponentAnalyzer { + getDiagnostics(definition: ComponentDefinition, context: LitAnalyzerContext): LitDiagnostic[] { + return context.rules.getDiagnosticsFromDefinition(definition, context).map(d => convertRuleDiagnosticToLitDiagnostic(d, context)); + } + + getCodeFixesAtOffsetRange(definition: ComponentDefinition, range: SourceFileRange, context: LitAnalyzerContext): LitCodeFix[] { + return arrayFlat( + arrayDefined( + context.rules + .getDiagnosticsFromDefinition(definition, context) + .filter(({ diagnostic }) => intersects(range, diagnostic.location)) + .map(({ diagnostic }) => diagnostic.fix?.()) + ) + ).map(ruleFix => converRuleFixToLitCodeFix(ruleFix)); + } +} diff --git a/packages/lit-analyzer/src/analyze/default-lit-analyzer-context.ts b/packages/lit-analyzer/src/analyze/default-lit-analyzer-context.ts index 0f1f3a8d..25b2983d 100644 --- a/packages/lit-analyzer/src/analyze/default-lit-analyzer-context.ts +++ b/packages/lit-analyzer/src/analyze/default-lit-analyzer-context.ts @@ -5,10 +5,10 @@ import { analyzeHTMLElement, analyzeSourceFile } from "web-component-analyzer"; import noBooleanInAttributeBindingRule from "../rules/no-boolean-in-attribute-binding"; import noComplexAttributeBindingRule from "../rules/no-complex-attribute-binding"; import noExpressionlessPropertyBindingRule from "../rules/no-expressionless-property-binding"; +import noIncompatiblePropertyType from "../rules/no-incompatible-property-type"; import noIncompatibleTypeBindingRule from "../rules/no-incompatible-type-binding"; import noInvalidAttributeName from "../rules/no-invalid-attribute-name"; import noInvalidDirectiveBindingRule from "../rules/no-invalid-directive-binding"; -import noIncompatiblePropertyType from "../rules/no-incompatible-property-type"; import noInvalidTagName from "../rules/no-invalid-tag-name"; import noMissingImport from "../rules/no-missing-import"; import noNoncallableEventBindingRule from "../rules/no-noncallable-event-binding"; @@ -27,16 +27,17 @@ import { LitAnalyzerContext, LitPluginContextHandler } from "./lit-analyzer-cont import { DefaultLitAnalyzerLogger, LitAnalyzerLoggerLevel } from "./lit-analyzer-logger"; import { convertAnalyzeResultToHtmlCollection, convertComponentDeclarationToHtmlTag } from "./parse/convert-component-definitions-to-html-collection"; import { parseDependencies } from "./parse/parse-dependencies/parse-dependencies"; +import { RuleCollection } from "./rule-collection"; import { DefaultAnalyzerDefinitionStore } from "./store/definition-store/default-analyzer-definition-store"; import { DefaultAnalyzerDependencyStore } from "./store/dependency-store/default-analyzer-dependency-store"; import { DefaultAnalyzerDocumentStore } from "./store/document-store/default-analyzer-document-store"; import { DefaultAnalyzerHtmlStore } from "./store/html-store/default-analyzer-html-store"; import { HtmlDataSourceKind } from "./store/html-store/html-data-source-merged"; -import { RuleModule } from "./types/rule-module"; +import { RuleModule } from "./types/rule/rule-module"; import { changedSourceFileIterator } from "./util/changed-source-file-iterator"; import { iterableFirst } from "./util/iterable-util"; -const rules: RuleModule[] = [ +const ALL_RULES: RuleModule[] = [ noExpressionlessPropertyBindingRule, noUnintendedMixedBindingRule, noUnknownSlotRule, @@ -78,14 +79,33 @@ export class DefaultLitAnalyzerContext implements LitAnalyzerContext { return this._config; } + private _currentFile: SourceFile | undefined; + get currentFile(): SourceFile { + if (this._currentFile == null) { + throw new Error("Current file is not set"); + } + + return this._currentFile; + } + readonly htmlStore = new DefaultAnalyzerHtmlStore(); readonly dependencyStore = new DefaultAnalyzerDependencyStore(); readonly documentStore = new DefaultAnalyzerDocumentStore(); readonly definitionStore = new DefaultAnalyzerDefinitionStore(); readonly logger = new DefaultLitAnalyzerLogger(); - get rules(): RuleModule[] { - return rules; + private _rules: RuleCollection | undefined; + get rules(): RuleCollection { + if (this._rules == null) { + this._rules = new RuleCollection(); + this._rules.push(...ALL_RULES); + } + + return this._rules; + } + + public setCurrentFile(file: SourceFile | undefined): void { + this._currentFile = file; } public updateConfig(config: LitAnalyzerConfig) { @@ -170,7 +190,7 @@ export class DefaultLitAnalyzerContext implements LitAnalyzerContext { config: { features: ["event", "member", "slot"], analyzeGlobalFeatures: true, - analyzeLibDom: false, + analyzeLibDom: true, analyzeLib: true, excludedDeclarationNames: ["HTMLElement"] } diff --git a/packages/lit-analyzer/src/analyze/document-analyzer/css/lit-css-document-analyzer.ts b/packages/lit-analyzer/src/analyze/document-analyzer/css/lit-css-document-analyzer.ts index f323084b..d6ee1952 100644 --- a/packages/lit-analyzer/src/analyze/document-analyzer/css/lit-css-document-analyzer.ts +++ b/packages/lit-analyzer/src/analyze/document-analyzer/css/lit-css-document-analyzer.ts @@ -1,18 +1,25 @@ +import { LitAnalyzerContext } from "../../lit-analyzer-context"; import { CssDocument } from "../../parse/document/text-document/css-document/css-document"; -import { getPositionContextInDocument } from "../../util/get-position-context-in-document"; -import { LitAnalyzerRequest } from "../../lit-analyzer-context"; import { LitCompletion } from "../../types/lit-completion"; import { LitCompletionDetails } from "../../types/lit-completion-details"; import { DefinitionKind, LitDefinition } from "../../types/lit-definition"; -import { LitCssDiagnostic } from "../../types/lit-diagnostic"; +import { LitDiagnostic } from "../../types/lit-diagnostic"; import { LitQuickInfo } from "../../types/lit-quick-info"; +import { DocumentOffset } from "../../types/range"; +import { getPositionContextInDocument } from "../../util/get-position-context-in-document"; +import { documentRangeToSFRange } from "../../util/range-util"; import { LitCssVscodeService } from "./lit-css-vscode-service"; export class LitCssDocumentAnalyzer { private vscodeCssService = new LitCssVscodeService(); private completionsCache: LitCompletion[] = []; - getCompletionDetailsAtOffset(document: CssDocument, offset: number, name: string, request: LitAnalyzerRequest): LitCompletionDetails | undefined { + getCompletionDetailsAtOffset( + document: CssDocument, + offset: DocumentOffset, + name: string, + context: LitAnalyzerContext + ): LitCompletionDetails | undefined { const completionWithName = this.completionsCache.find(completion => completion.name === name); if (completionWithName == null || completionWithName.documentation == null) return undefined; @@ -27,25 +34,25 @@ export class LitCssDocumentAnalyzer { }; } - getCompletionsAtOffset(document: CssDocument, offset: number, request: LitAnalyzerRequest): LitCompletion[] { - this.completionsCache = this.vscodeCssService.getCompletions(document, offset, request); + getCompletionsAtOffset(document: CssDocument, offset: DocumentOffset, context: LitAnalyzerContext): LitCompletion[] { + this.completionsCache = this.vscodeCssService.getCompletions(document, offset, context); return this.completionsCache; } - getQuickInfoAtOffset(document: CssDocument, offset: number, request: LitAnalyzerRequest): LitQuickInfo | undefined { - return this.vscodeCssService.getQuickInfo(document, offset, request); + getQuickInfoAtOffset(document: CssDocument, offset: DocumentOffset, context: LitAnalyzerContext): LitQuickInfo | undefined { + return this.vscodeCssService.getQuickInfo(document, offset, context); } - getDiagnostics(document: CssDocument, request: LitAnalyzerRequest): LitCssDiagnostic[] { - return this.vscodeCssService.getDiagnostics(document, request); + getDiagnostics(document: CssDocument, context: LitAnalyzerContext): LitDiagnostic[] { + return this.vscodeCssService.getDiagnostics(document, context); } - getDefinitionAtOffset(document: CssDocument, offset: number, request: LitAnalyzerRequest): LitDefinition | undefined { + getDefinitionAtOffset(document: CssDocument, offset: DocumentOffset, context: LitAnalyzerContext): LitDefinition | undefined { const positionContext = getPositionContextInDocument(document, offset); const tagNameMatch = positionContext.word.match(/^[a-zA-Z-1-9]+/); if (tagNameMatch == null) return undefined; const tagName = tagNameMatch[0]; - const definition = request.definitionStore.getDefinitionForTagName(tagName); + const definition = context.definitionStore.getDefinitionForTagName(tagName); if (definition != null) { const start = offset - positionContext.leftWord.length; @@ -53,7 +60,7 @@ export class LitCssDocumentAnalyzer { return { kind: DefinitionKind.COMPONENT, - fromRange: { document, start, end }, + fromRange: documentRangeToSFRange(document, { start, end }), target: definition.declaration() }; } diff --git a/packages/lit-analyzer/src/analyze/document-analyzer/css/lit-css-vscode-service.ts b/packages/lit-analyzer/src/analyze/document-analyzer/css/lit-css-vscode-service.ts index b938a7f8..0a3094fd 100644 --- a/packages/lit-analyzer/src/analyze/document-analyzer/css/lit-css-vscode-service.ts +++ b/packages/lit-analyzer/src/analyze/document-analyzer/css/lit-css-vscode-service.ts @@ -1,15 +1,17 @@ import * as vscode from "vscode-css-languageservice"; import { IAtDirectiveData, ICSSDataProvider, IPropertyData, IPseudoClassData, IPseudoElementData } from "vscode-css-languageservice"; import { isRuleDisabled } from "../../lit-analyzer-config"; -import { LitAnalyzerRequest } from "../../lit-analyzer-context"; +import { LitAnalyzerContext } from "../../lit-analyzer-context"; import { CssDocument } from "../../parse/document/text-document/css-document/css-document"; import { AnalyzerHtmlStore } from "../../store/analyzer-html-store"; import { LitCompletion } from "../../types/lit-completion"; -import { LitCssDiagnostic } from "../../types/lit-diagnostic"; +import { LitDiagnostic } from "../../types/lit-diagnostic"; import { LitQuickInfo } from "../../types/lit-quick-info"; import { LitTargetKind } from "../../types/lit-target-kind"; +import { DocumentOffset } from "../../types/range"; import { lazy } from "../../util/general-util"; import { iterableFilter, iterableMap } from "../../util/iterable-util"; +import { documentRangeToSFRange } from "../../util/range-util"; function makeVscTextDocument(cssDocument: CssDocument): vscode.TextDocument { return vscode.TextDocument.create("untitled://embedded.css", "css", 1, cssDocument.virtualDocument.text); @@ -26,7 +28,7 @@ export class LitCssVscodeService { return vscode.getSCSSLanguageService({ customDataProviders: [this.dataProvider.provider] }); } - getDiagnostics(document: CssDocument, context: LitAnalyzerRequest): LitCssDiagnostic[] { + getDiagnostics(document: CssDocument, context: LitAnalyzerContext): LitDiagnostic[] { this.dataProvider.update(context.htmlStore); const vscTextDocument = makeVscTextDocument(document); @@ -50,17 +52,18 @@ export class LitCssVscodeService { diagnostic => ({ severity: diagnostic.severity === vscode.DiagnosticSeverity.Error ? "error" : "warning", - location: { - document, + source: "no-invalid-css", + location: documentRangeToSFRange(document, { start: vscTextDocument.offsetAt(diagnostic.range.start), end: vscTextDocument.offsetAt(diagnostic.range.end) - }, - message: diagnostic.message - } as LitCssDiagnostic) + }), + message: diagnostic.message, + file: context.currentFile + } as LitDiagnostic) ); } - getQuickInfo(document: CssDocument, offset: number, context: LitAnalyzerRequest): LitQuickInfo | undefined { + getQuickInfo(document: CssDocument, offset: DocumentOffset, context: LitAnalyzerContext): LitQuickInfo | undefined { this.dataProvider.update(context.htmlStore); const vscTextDocument = makeVscTextDocument(document); @@ -88,16 +91,12 @@ export class LitCssVscodeService { return { primaryInfo: primaryInfo || "", secondaryInfo, - range: { - document, - start: vscTextDocument.offsetAt(hover.range.start), - end: vscTextDocument.offsetAt(hover.range.end) - } + range: documentRangeToSFRange(document, { start: vscTextDocument.offsetAt(hover.range.start), end: vscTextDocument.offsetAt(hover.range.end) }) }; } - getCompletions(document: CssDocument, offset: number, request: LitAnalyzerRequest): LitCompletion[] { - this.dataProvider.update(request.htmlStore); + getCompletions(document: CssDocument, offset: DocumentOffset, context: LitAnalyzerContext): LitCompletion[] { + this.dataProvider.update(context.htmlStore); const vscTextDocument = makeVscTextDocument(document); const vscStylesheet = this.makeVscStylesheet(vscTextDocument); diff --git a/packages/lit-analyzer/src/analyze/document-analyzer/html/code-fix/code-fixes-for-html-document.ts b/packages/lit-analyzer/src/analyze/document-analyzer/html/code-fix/code-fixes-for-html-document.ts new file mode 100644 index 00000000..b2374618 --- /dev/null +++ b/packages/lit-analyzer/src/analyze/document-analyzer/html/code-fix/code-fixes-for-html-document.ts @@ -0,0 +1,18 @@ +import { LitAnalyzerContext } from "../../../lit-analyzer-context"; +import { HtmlDocument } from "../../../parse/document/text-document/html-document/html-document"; +import { LitCodeFix } from "../../../types/lit-code-fix"; +import { DocumentRange } from "../../../types/range"; +import { arrayDefined, arrayFlat } from "../../../util/array-util"; +import { documentRangeToSFRange, intersects } from "../../../util/range-util"; +import { converRuleFixToLitCodeFix } from "../../../util/rule-fix-util"; + +export function codeFixesForHtmlDocument(htmlDocument: HtmlDocument, range: DocumentRange, context: LitAnalyzerContext): LitCodeFix[] { + return arrayFlat( + arrayDefined( + context.rules + .getDiagnosticsFromDocument(htmlDocument, context) + .filter(({ diagnostic }) => intersects(documentRangeToSFRange(htmlDocument, range), diagnostic.location)) + .map(({ diagnostic }) => diagnostic.fix?.()) + ) + ).map(ruleFix => converRuleFixToLitCodeFix(ruleFix)); +} diff --git a/packages/lit-analyzer/src/analyze/document-analyzer/html/code-fix/code-fixes-for-html-report.ts b/packages/lit-analyzer/src/analyze/document-analyzer/html/code-fix/code-fixes-for-html-report.ts deleted file mode 100644 index ebdec445..00000000 --- a/packages/lit-analyzer/src/analyze/document-analyzer/html/code-fix/code-fixes-for-html-report.ts +++ /dev/null @@ -1,249 +0,0 @@ -import { LIT_HTML_BOOLEAN_ATTRIBUTE_MODIFIER } from "../../../constants"; -import { LitAnalyzerRequest } from "../../../lit-analyzer-context"; -import { litAttributeModifierForTarget } from "../../../parse/parse-html-data/html-tag"; -import { HtmlNodeAttrKind } from "../../../types/html-node/html-node-attr-types"; -import { CodeFixKind, LitCodeFix } from "../../../types/lit-code-fix"; -import { CodeActionKind, LitCodeFixAction } from "../../../types/lit-code-fix-action"; -import { LitHtmlDiagnostic, LitHtmlDiagnosticKind } from "../../../types/lit-diagnostic"; -import { iterableFirst } from "../../../util/iterable-util"; - -export function codeFixesForHtmlReport(htmlReport: LitHtmlDiagnostic, { document, file, htmlStore }: LitAnalyzerRequest): LitCodeFix[] { - switch (htmlReport.kind) { - case LitHtmlDiagnosticKind.UNKNOWN_TARGET: { - const fixes: LitCodeFix[] = []; - - switch (htmlReport.htmlAttr.kind) { - case HtmlNodeAttrKind.BOOLEAN_ATTRIBUTE: - case HtmlNodeAttrKind.ATTRIBUTE: - fixes.push({ - kind: CodeFixKind.RENAME, - message: `Change attribute to 'data-${htmlReport.htmlAttr.name}'`, - htmlReport, - actions: [ - { - kind: CodeActionKind.TEXT_CHANGE, - change: { - range: { - document: document!, - start: htmlReport.htmlAttr.location.name.start, - end: htmlReport.htmlAttr.location.name.start - }, - newText: "data-" - } - } - ] - }); - break; - } - - if (htmlReport.suggestedTarget != null) { - const newText = `${litAttributeModifierForTarget(htmlReport.suggestedTarget)}${htmlReport.suggestedTarget.name}`; - fixes.push({ - kind: CodeFixKind.RENAME, - message: `Change ${htmlReport.htmlAttr.kind === HtmlNodeAttrKind.PROPERTY ? "property" : "attribute"} to '${newText}'`, - htmlReport, - actions: [ - { - kind: CodeActionKind.TEXT_CHANGE, - change: { - // Make a range that includes the modifier. - range: { - document: document!, - start: htmlReport.htmlAttr.location.start, - end: htmlReport.htmlAttr.location.name.end - }, - newText - } - } - ] - }); - } - - return fixes; - } - - case LitHtmlDiagnosticKind.UNKNOWN_TAG: { - if (htmlReport.suggestedName == null) break; - - const { endTag: endTagRange } = htmlReport.htmlNode.location; - - return [ - { - kind: CodeFixKind.RENAME, - message: `Change tag name to '${htmlReport.suggestedName}'`, - htmlReport, - actions: [ - { - kind: CodeActionKind.TEXT_CHANGE, - change: { - range: htmlReport.location, - newText: htmlReport.suggestedName - } - }, - ...(endTagRange == null - ? [] - : [ - { - kind: CodeActionKind.TEXT_CHANGE, - change: { - range: { - document: document!, - start: endTagRange.start + 2, - end: endTagRange.end - 1 - }, - newText: htmlReport.suggestedName - } - } as LitCodeFixAction - ]) - ] - } - ]; - } - - case LitHtmlDiagnosticKind.BOOL_MOD_ON_NON_BOOL: - case LitHtmlDiagnosticKind.PRIMITIVE_NOT_ASSIGNABLE_TO_COMPLEX: { - const { htmlAttr } = htmlReport; - - const existingModifierLength = htmlAttr.modifier ? htmlAttr.modifier.length : 0; - - const htmlAttrTarget = htmlStore.getHtmlAttrTarget(htmlAttr); - - const newModifier = htmlAttrTarget == null ? "." : htmlReport.kind === LitHtmlDiagnosticKind.BOOL_MOD_ON_NON_BOOL ? "" : "."; - - return [ - { - kind: CodeFixKind.CHANGE_LIT_MODIFIER, - message: newModifier.length === 0 ? `Remove '${htmlAttr.modifier || ""}' modifier` : `Use '${newModifier}' modifier instead`, - htmlReport, - actions: [ - { - kind: CodeActionKind.TEXT_CHANGE, - change: { - range: { - document: document!, - start: htmlAttr.location.name.start - existingModifierLength, - end: htmlAttr.location.name.start - }, - newText: newModifier - } - } - ] - } - ]; - } - - case LitHtmlDiagnosticKind.MISSING_IMPORT: - return [ - { - kind: CodeFixKind.IMPORT_COMPONENT, - message: `Import "${iterableFirst(htmlReport.definition.identifierNodes)?.getText() || "component"}" from module "${ - htmlReport.importPath - }"`, - htmlReport, - actions: [ - { - kind: CodeActionKind.IMPORT_COMPONENT, - importPath: htmlReport.importPath - } - ] - } - ]; - - case LitHtmlDiagnosticKind.MISSING_SLOT_ATTRIBUTE: - return [ - { - kind: CodeFixKind.ADD_TEXT, - message: `Add slot attribute.`, - htmlReport, - actions: [ - { - kind: CodeActionKind.TEXT_CHANGE, - change: { - range: { - document: document!, - start: htmlReport.htmlNode.location.name.end, - end: htmlReport.htmlNode.location.name.end - }, - newText: ` slot=""` - } - } - ] - } - ]; - - case LitHtmlDiagnosticKind.EXPRESSION_ONLY_ASSIGNABLE_WITH_BOOLEAN_BINDING: { - const newText = `${LIT_HTML_BOOLEAN_ATTRIBUTE_MODIFIER}${htmlReport.htmlAttr.name}`; - - return [ - { - kind: CodeFixKind.ADD_TEXT, - message: `Change to '${newText}'`, - htmlReport, - actions: [ - { - kind: CodeActionKind.TEXT_CHANGE, - change: { - range: { - document: document!, - ...htmlReport.htmlAttr.location.name - }, - newText - } - } - ] - } - ]; - } - - case LitHtmlDiagnosticKind.INVALID_ATTRIBUTE_EXPRESSION_TYPE_UNDEFINED: { - const { assignment } = htmlReport.htmlAttr; - return [ - { - kind: CodeFixKind.ADD_TEXT, - message: `Use the 'ifDefined' directive.`, - htmlReport, - actions: [ - { - kind: CodeActionKind.TEXT_CHANGE, - change: { - range: { - document: document!, - start: assignment.location.start + 2, // Offset 2 for '${' - end: assignment.location.end - 1 // Offset 1 for '}' - }, - newText: `ifDefined(${assignment.expression.getText()})` - } - } - ] - } - ]; - } - - case LitHtmlDiagnosticKind.INVALID_ATTRIBUTE_EXPRESSION_TYPE_NULL: { - const { assignment } = htmlReport.htmlAttr; - const newText = `ifDefined(${assignment.expression.getText()} === null ? undefined : ${assignment.expression.getText()})`; - return [ - { - kind: CodeFixKind.ADD_TEXT, - message: `Use '${newText}'`, - htmlReport, - actions: [ - { - kind: CodeActionKind.TEXT_CHANGE, - change: { - range: { - document: document!, - start: assignment.location.start + 2, // Offset 2 for '${' - end: assignment.location.end - 1 // Offset 1 for '}' - }, - newText - } - } - ] - } - ]; - } - } - - return []; -} diff --git a/packages/lit-analyzer/src/analyze/document-analyzer/html/completion/completions-at-offset.ts b/packages/lit-analyzer/src/analyze/document-analyzer/html/completion/completions-at-offset.ts index 11585d34..7c0ab0d1 100644 --- a/packages/lit-analyzer/src/analyze/document-analyzer/html/completion/completions-at-offset.ts +++ b/packages/lit-analyzer/src/analyze/document-analyzer/html/completion/completions-at-offset.ts @@ -1,13 +1,14 @@ -import { LitAnalyzerRequest } from "../../../lit-analyzer-context"; +import { LitAnalyzerContext } from "../../../lit-analyzer-context"; import { HtmlDocument } from "../../../parse/document/text-document/html-document/html-document"; import { LitCompletion } from "../../../types/lit-completion"; +import { DocumentOffset } from "../../../types/range"; import { getPositionContextInDocument } from "../../../util/get-position-context-in-document"; -import { rangeFromHtmlNodeAttr } from "../../../util/lit-range-util"; +import { rangeFromHtmlNodeAttr } from "../../../util/range-util"; import { completionsForHtmlAttrValues } from "./completions-for-html-attr-values"; import { completionsForHtmlAttrs } from "./completions-for-html-attrs"; import { completionsForHtmlNodes } from "./completions-for-html-nodes"; -export function completionsAtOffset(document: HtmlDocument, offset: number, request: LitAnalyzerRequest): LitCompletion[] { +export function completionsAtOffset(document: HtmlDocument, offset: DocumentOffset, context: LitAnalyzerContext): LitCompletion[] { const positionContext = getPositionContextInDocument(document, offset); const { beforeWord } = positionContext; @@ -20,19 +21,19 @@ export function completionsAtOffset(document: HtmlDocument, offset: number, requ // Get entries from the extensions if (intersectingAttr != null) { - const entries = completionsForHtmlAttrs(intersectingAttr.htmlNode, positionContext, request); + const entries = completionsForHtmlAttrs(intersectingAttr.htmlNode, positionContext, context); // Make sure that every entry overwrites the entire attribute name. return entries.map(entry => ({ ...entry, - range: rangeFromHtmlNodeAttr(request.document, intersectingAttr) + range: rangeFromHtmlNodeAttr(intersectingAttr) })); } else if (intersectingAttrAssignment != null) { - return completionsForHtmlAttrValues(intersectingAttrAssignment, positionContext, request); + return completionsForHtmlAttrValues(intersectingAttrAssignment, positionContext, context); } else if (intersectingAttrAreaNode != null) { - return completionsForHtmlAttrs(intersectingAttrAreaNode, positionContext, request); + return completionsForHtmlAttrs(intersectingAttrAreaNode, positionContext, context); } else if (beforeWord === "<" || beforeWord === "/") { - return completionsForHtmlNodes(intersectingClosestNode, positionContext, request); + return completionsForHtmlNodes(document, intersectingClosestNode, positionContext, context); } return []; diff --git a/packages/lit-analyzer/src/analyze/document-analyzer/html/completion/completions-for-html-attr-values.ts b/packages/lit-analyzer/src/analyze/document-analyzer/html/completion/completions-for-html-attr-values.ts index b39a94de..cf6acee0 100644 --- a/packages/lit-analyzer/src/analyze/document-analyzer/html/completion/completions-for-html-attr-values.ts +++ b/packages/lit-analyzer/src/analyze/document-analyzer/html/completion/completions-for-html-attr-values.ts @@ -1,5 +1,5 @@ import { isSimpleTypeLiteral, SimpleType, SimpleTypeKind } from "ts-simple-type"; -import { LitAnalyzerRequest } from "../../../lit-analyzer-context"; +import { LitAnalyzerContext } from "../../../lit-analyzer-context"; import { HtmlNodeAttrAssignmentKind } from "../../../types/html-node/html-node-attr-assignment-types"; import { HtmlNodeAttr, HtmlNodeAttrKind } from "../../../types/html-node/html-node-attr-types"; import { LitCompletion } from "../../../types/lit-completion"; @@ -8,7 +8,7 @@ import { DocumentPositionContext } from "../../../util/get-position-context-in-d export function completionsForHtmlAttrValues( htmlNodeAttr: HtmlNodeAttr, location: DocumentPositionContext, - { htmlStore }: LitAnalyzerRequest + { htmlStore }: LitAnalyzerContext ): LitCompletion[] { // There is not point in showing completions for event listener bindings if (htmlNodeAttr.kind === HtmlNodeAttrKind.EVENT_LISTENER) return []; diff --git a/packages/lit-analyzer/src/analyze/document-analyzer/html/completion/completions-for-html-attrs.ts b/packages/lit-analyzer/src/analyze/document-analyzer/html/completion/completions-for-html-attrs.ts index 513d2252..c119d92e 100644 --- a/packages/lit-analyzer/src/analyze/document-analyzer/html/completion/completions-for-html-attrs.ts +++ b/packages/lit-analyzer/src/analyze/document-analyzer/html/completion/completions-for-html-attrs.ts @@ -9,10 +9,10 @@ import { HtmlNode } from "../../../types/html-node/html-node-types"; import { DocumentPositionContext } from "../../../util/get-position-context-in-document"; import { iterableFilter, iterableMap } from "../../../util/iterable-util"; import { lazy } from "../../../util/general-util"; -import { LitAnalyzerRequest } from "../../../lit-analyzer-context"; +import { LitAnalyzerContext } from "../../../lit-analyzer-context"; import { LitCompletion } from "../../../types/lit-completion"; -export function completionsForHtmlAttrs(htmlNode: HtmlNode, location: DocumentPositionContext, { htmlStore }: LitAnalyzerRequest): LitCompletion[] { +export function completionsForHtmlAttrs(htmlNode: HtmlNode, location: DocumentPositionContext, { htmlStore }: LitAnalyzerContext): LitCompletion[] { const onTagName = htmlNode.tagName; // Code completions for ".[...]"; diff --git a/packages/lit-analyzer/src/analyze/document-analyzer/html/completion/completions-for-html-nodes.ts b/packages/lit-analyzer/src/analyze/document-analyzer/html/completion/completions-for-html-nodes.ts index 3bdb2525..6b85e346 100644 --- a/packages/lit-analyzer/src/analyze/document-analyzer/html/completion/completions-for-html-nodes.ts +++ b/packages/lit-analyzer/src/analyze/document-analyzer/html/completion/completions-for-html-nodes.ts @@ -1,15 +1,18 @@ -import { LitAnalyzerRequest } from "../../../lit-analyzer-context"; +import { LitAnalyzerContext } from "../../../lit-analyzer-context"; +import { HtmlDocument } from "../../../parse/document/text-document/html-document/html-document"; import { documentationForHtmlTag } from "../../../parse/parse-html-data/html-tag"; import { HtmlNode } from "../../../types/html-node/html-node-types"; import { LitCompletion } from "../../../types/lit-completion"; import { lazy } from "../../../util/general-util"; import { DocumentPositionContext } from "../../../util/get-position-context-in-document"; import { isCustomElementTagName } from "../../../util/is-valid-name"; +import { documentRangeToSFRange } from "../../../util/range-util"; export function completionsForHtmlNodes( + document: HtmlDocument, intersectingClosestNode: HtmlNode | undefined, { offset, leftWord, rightWord, beforeWord, afterWord }: DocumentPositionContext, - { document, htmlStore, logger }: LitAnalyzerRequest + { htmlStore }: LitAnalyzerContext ): LitCompletion[] { const isClosingTag = beforeWord === "/"; @@ -26,11 +29,10 @@ export function completionsForHtmlNodes( insert, kind: "enumElement", importance: "high", - range: { - document, + range: documentRangeToSFRange(document, { start: offset - leftWord.length - 2, end: offset + rightWord.length - }, + }), documentation: lazy(() => { const htmlTag = htmlStore.getHtmlTag(intersectingClosestNode); return htmlTag != null ? documentationForHtmlTag(htmlTag) : undefined; @@ -52,11 +54,10 @@ export function completionsForHtmlNodes( insert, kind: isBuiltIn ? "enumElement" : hasDeclaration ? "member" : "label", importance: isBuiltIn ? "low" : hasDeclaration ? "high" : "medium", - range: { - document, + range: documentRangeToSFRange(document, { start: offset - leftWord.length - (isClosingTag ? 2 : 0), end: offset + rightWord.length + (isClosingTag && afterWord === ">" ? 1 : 0) - }, + }), documentation: lazy(() => documentationForHtmlTag(htmlTag)) } as LitCompletion; }); diff --git a/packages/lit-analyzer/src/analyze/document-analyzer/html/definition/definition-for-html-attr.ts b/packages/lit-analyzer/src/analyze/document-analyzer/html/definition/definition-for-html-attr.ts index 3d959a27..cceb2b75 100644 --- a/packages/lit-analyzer/src/analyze/document-analyzer/html/definition/definition-for-html-attr.ts +++ b/packages/lit-analyzer/src/analyze/document-analyzer/html/definition/definition-for-html-attr.ts @@ -1,23 +1,23 @@ import { isHtmlEvent, isHtmlMember } from "../../../parse/parse-html-data/html-tag"; import { HtmlNodeAttr } from "../../../types/html-node/html-node-attr-types"; -import { LitAnalyzerRequest } from "../../../lit-analyzer-context"; +import { LitAnalyzerContext } from "../../../lit-analyzer-context"; import { DefinitionKind, LitDefinition } from "../../../types/lit-definition"; -import { rangeFromHtmlNodeAttr } from "../../../util/lit-range-util"; +import { rangeFromHtmlNodeAttr } from "../../../util/range-util"; -export function definitionForHtmlAttr(htmlAttr: HtmlNodeAttr, { htmlStore, document }: LitAnalyzerRequest): LitDefinition | undefined { +export function definitionForHtmlAttr(htmlAttr: HtmlNodeAttr, { htmlStore }: LitAnalyzerContext): LitDefinition | undefined { const target = htmlStore.getHtmlAttrTarget(htmlAttr); if (target == null) return undefined; if (isHtmlMember(target) && target.declaration != null) { return { kind: DefinitionKind.MEMBER, - fromRange: rangeFromHtmlNodeAttr(document, htmlAttr), + fromRange: rangeFromHtmlNodeAttr(htmlAttr), target: target.declaration }; } else if (isHtmlEvent(target) && target.declaration != null) { return { kind: DefinitionKind.EVENT, - fromRange: rangeFromHtmlNodeAttr(document, htmlAttr), + fromRange: rangeFromHtmlNodeAttr(htmlAttr), target: target.declaration }; } diff --git a/packages/lit-analyzer/src/analyze/document-analyzer/html/definition/definition-for-html-node.ts b/packages/lit-analyzer/src/analyze/document-analyzer/html/definition/definition-for-html-node.ts index c989cb06..abcaa19f 100644 --- a/packages/lit-analyzer/src/analyze/document-analyzer/html/definition/definition-for-html-node.ts +++ b/packages/lit-analyzer/src/analyze/document-analyzer/html/definition/definition-for-html-node.ts @@ -1,15 +1,15 @@ -import { LitAnalyzerRequest } from "../../../lit-analyzer-context"; +import { LitAnalyzerContext } from "../../../lit-analyzer-context"; import { HtmlNode } from "../../../types/html-node/html-node-types"; import { DefinitionKind, LitDefinition } from "../../../types/lit-definition"; -import { rangeFromHtmlNode } from "../../../util/lit-range-util"; +import { rangeFromHtmlNode } from "../../../util/range-util"; -export function definitionForHtmlNode(htmlNode: HtmlNode, { document, htmlStore }: LitAnalyzerRequest): LitDefinition | undefined { +export function definitionForHtmlNode(htmlNode: HtmlNode, { htmlStore }: LitAnalyzerContext): LitDefinition | undefined { const tag = htmlStore.getHtmlTag(htmlNode); if (tag == null || tag.declaration == null) return undefined; return { kind: DefinitionKind.COMPONENT, - fromRange: rangeFromHtmlNode(document, htmlNode), + fromRange: rangeFromHtmlNode(htmlNode), target: tag.declaration }; } diff --git a/packages/lit-analyzer/src/analyze/document-analyzer/html/diagnostic/validate-html-document.ts b/packages/lit-analyzer/src/analyze/document-analyzer/html/diagnostic/validate-html-document.ts index 1f1ad4ce..6865f669 100644 --- a/packages/lit-analyzer/src/analyze/document-analyzer/html/diagnostic/validate-html-document.ts +++ b/packages/lit-analyzer/src/analyze/document-analyzer/html/diagnostic/validate-html-document.ts @@ -1,36 +1,8 @@ -import { LitAnalyzerRequest } from "../../../lit-analyzer-context"; +import { LitAnalyzerContext } from "../../../lit-analyzer-context"; import { HtmlDocument } from "../../../parse/document/text-document/html-document/html-document"; -import { HtmlNodeAttr } from "../../../types/html-node/html-node-attr-types"; -import { HtmlNode } from "../../../types/html-node/html-node-types"; -import { LitHtmlDiagnostic } from "../../../types/lit-diagnostic"; -import { validateHtmlNode } from "./validate-html-node"; -import { validateHtmlAttr } from "./validate-html-node-attr"; -import { validateHtmlAttrAssignment } from "./validate-html-node-attr-assignment"; +import { LitDiagnostic } from "../../../types/lit-diagnostic"; +import { convertRuleDiagnosticToLitDiagnostic } from "../../../util/rule-diagnostic-util"; -export function validateHTMLDocument(htmlDocument: HtmlDocument, request: LitAnalyzerRequest): LitHtmlDiagnostic[] { - const reports: LitHtmlDiagnostic[] = []; - - const iterateNodes = (nodes: HtmlNode[]) => { - for (const childNode of nodes) { - reports.push(...validateHtmlNode(childNode, request)); - - const iterateAttrs = (attrs: HtmlNodeAttr[]) => { - for (const attr of attrs) { - reports.push(...validateHtmlAttr(attr, request)); - - if (attr.assignment != null) { - reports.push(...validateHtmlAttrAssignment(attr.assignment, request)); - } - } - }; - - iterateAttrs(childNode.attributes); - - iterateNodes(childNode.children); - } - }; - - iterateNodes(htmlDocument.rootNodes); - - return reports; +export function validateHTMLDocument(htmlDocument: HtmlDocument, context: LitAnalyzerContext): LitDiagnostic[] { + return context.rules.getDiagnosticsFromDocument(htmlDocument, context).map(d => convertRuleDiagnosticToLitDiagnostic(d, context)); } diff --git a/packages/lit-analyzer/src/analyze/document-analyzer/html/diagnostic/validate-html-node-attr-assignment.ts b/packages/lit-analyzer/src/analyze/document-analyzer/html/diagnostic/validate-html-node-attr-assignment.ts deleted file mode 100644 index a697a33a..00000000 --- a/packages/lit-analyzer/src/analyze/document-analyzer/html/diagnostic/validate-html-node-attr-assignment.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { isRuleEnabled } from "../../../lit-analyzer-config"; -import { LitAnalyzerRequest } from "../../../lit-analyzer-context"; -import { HtmlNodeAttrAssignment } from "../../../types/html-node/html-node-attr-assignment-types"; -import { LitHtmlDiagnostic } from "../../../types/lit-diagnostic"; - -export function validateHtmlAttrAssignment(assignment: HtmlNodeAttrAssignment, request: LitAnalyzerRequest): LitHtmlDiagnostic[] { - for (const rule of request.rules) { - if (isRuleEnabled(request.config, rule.name) && rule.visitHtmlAssignment != null) { - const result = rule.visitHtmlAssignment(assignment, request); - - if (result != null) { - return result; - } - } - } - - return []; -} diff --git a/packages/lit-analyzer/src/analyze/document-analyzer/html/diagnostic/validate-html-node-attr.ts b/packages/lit-analyzer/src/analyze/document-analyzer/html/diagnostic/validate-html-node-attr.ts deleted file mode 100644 index b1abf9de..00000000 --- a/packages/lit-analyzer/src/analyze/document-analyzer/html/diagnostic/validate-html-node-attr.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { isRuleEnabled } from "../../../lit-analyzer-config"; -import { LitAnalyzerRequest } from "../../../lit-analyzer-context"; -import { HtmlNodeAttr } from "../../../types/html-node/html-node-attr-types"; -import { LitHtmlDiagnostic } from "../../../types/lit-diagnostic"; - -export function validateHtmlAttr(htmlAttr: HtmlNodeAttr, request: LitAnalyzerRequest): LitHtmlDiagnostic[] { - for (const rule of request.rules) { - if (isRuleEnabled(request.config, rule.name) && rule.visitHtmlAttribute != null) { - const result = rule.visitHtmlAttribute(htmlAttr, request); - - if (result != null) { - return result; - } - } - } - - return []; -} diff --git a/packages/lit-analyzer/src/analyze/document-analyzer/html/diagnostic/validate-html-node.ts b/packages/lit-analyzer/src/analyze/document-analyzer/html/diagnostic/validate-html-node.ts deleted file mode 100644 index faa16a17..00000000 --- a/packages/lit-analyzer/src/analyze/document-analyzer/html/diagnostic/validate-html-node.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { isRuleEnabled } from "../../../lit-analyzer-config"; -import { LitAnalyzerRequest } from "../../../lit-analyzer-context"; -import { HtmlNode } from "../../../types/html-node/html-node-types"; -import { LitHtmlDiagnostic } from "../../../types/lit-diagnostic"; - -export function validateHtmlNode(htmlNode: HtmlNode, request: LitAnalyzerRequest): LitHtmlDiagnostic[] { - for (const rule of request.rules) { - if (isRuleEnabled(request.config, rule.name) && rule.visitHtmlNode != null) { - const result = rule.visitHtmlNode(htmlNode, request); - - if (result != null) { - return result; - } - } - } - - return []; -} diff --git a/packages/lit-analyzer/src/analyze/document-analyzer/html/lit-html-document-analyzer.ts b/packages/lit-analyzer/src/analyze/document-analyzer/html/lit-html-document-analyzer.ts index 8fc74fbd..5e8121dd 100644 --- a/packages/lit-analyzer/src/analyze/document-analyzer/html/lit-html-document-analyzer.ts +++ b/packages/lit-analyzer/src/analyze/document-analyzer/html/lit-html-document-analyzer.ts @@ -1,6 +1,5 @@ import { FormatCodeSettings } from "typescript"; -import { Range } from "../../types/range"; -import { LitAnalyzerRequest } from "../../lit-analyzer-context"; +import { LitAnalyzerContext } from "../../lit-analyzer-context"; import { HtmlDocument } from "../../parse/document/text-document/html-document/html-document"; import { isHTMLAttr } from "../../types/html-node/html-node-attr-types"; import { isHTMLNode } from "../../types/html-node/html-node-types"; @@ -9,16 +8,16 @@ import { LitCodeFix } from "../../types/lit-code-fix"; import { LitCompletion } from "../../types/lit-completion"; import { LitCompletionDetails } from "../../types/lit-completion-details"; import { LitDefinition } from "../../types/lit-definition"; -import { LitHtmlDiagnostic } from "../../types/lit-diagnostic"; +import { LitDiagnostic } from "../../types/lit-diagnostic"; import { LitFormatEdit } from "../../types/lit-format-edit"; import { LitOutliningSpan, LitOutliningSpanKind } from "../../types/lit-outlining-span"; import { LitQuickInfo } from "../../types/lit-quick-info"; import { LitRenameInfo } from "../../types/lit-rename-info"; import { LitRenameLocation } from "../../types/lit-rename-location"; -import { flatten } from "../../util/array-util"; +import { DocumentOffset, DocumentRange } from "../../types/range"; import { iterableDefined } from "../../util/iterable-util"; -import { intersects } from "../../util/general-util"; -import { codeFixesForHtmlReport } from "./code-fix/code-fixes-for-html-report"; +import { documentRangeToSFRange } from "../../util/range-util"; +import { codeFixesForHtmlDocument } from "./code-fix/code-fixes-for-html-document"; import { completionsAtOffset } from "./completion/completions-at-offset"; import { definitionForHtmlAttr } from "./definition/definition-for-html-attr"; import { definitionForHtmlNode } from "./definition/definition-for-html-node"; @@ -32,7 +31,12 @@ export class LitHtmlDocumentAnalyzer { private vscodeHtmlService = new LitHtmlVscodeService(); private completionsCache: LitCompletion[] = []; - getCompletionDetailsAtOffset(document: HtmlDocument, offset: number, name: string, request: LitAnalyzerRequest): LitCompletionDetails | undefined { + getCompletionDetailsAtOffset( + document: HtmlDocument, + offset: DocumentOffset, + name: string, + context: LitAnalyzerContext + ): LitCompletionDetails | undefined { const completionWithName = this.completionsCache.find(completion => completion.name === name); if (completionWithName == null || completionWithName.documentation == null) return undefined; @@ -47,40 +51,39 @@ export class LitHtmlDocumentAnalyzer { }; } - getCompletionsAtOffset(document: HtmlDocument, offset: number, request: LitAnalyzerRequest): LitCompletion[] { - this.completionsCache = completionsAtOffset(document, offset, request); - return completionsAtOffset(document, offset, request); + getCompletionsAtOffset(document: HtmlDocument, offset: DocumentOffset, context: LitAnalyzerContext): LitCompletion[] { + this.completionsCache = completionsAtOffset(document, offset, context); + return completionsAtOffset(document, offset, context); } - getDiagnostics(document: HtmlDocument, request: LitAnalyzerRequest): LitHtmlDiagnostic[] { - return validateHTMLDocument(document, request); + getDiagnostics(document: HtmlDocument, context: LitAnalyzerContext): LitDiagnostic[] { + return validateHTMLDocument(document, context); } - getClosingTagAtOffset(document: HtmlDocument, offset: number): LitClosingTagInfo | undefined { + getClosingTagAtOffset(document: HtmlDocument, offset: DocumentOffset): LitClosingTagInfo | undefined { return this.vscodeHtmlService.getClosingTagAtOffset(document, offset); } - getCodeFixesAtOffsetRange(document: HtmlDocument, offsetRange: Range, request: LitAnalyzerRequest): LitCodeFix[] { + getCodeFixesAtOffsetRange(document: HtmlDocument, offsetRange: DocumentRange, context: LitAnalyzerContext): LitCodeFix[] { const hit = document.htmlNodeOrAttrAtOffset(offsetRange); if (hit == null) return []; - const reports = validateHTMLDocument(document, request); - return flatten(reports.filter(report => intersects(offsetRange, report.location)).map(report => codeFixesForHtmlReport(report, request))); + return codeFixesForHtmlDocument(document, offsetRange, context); } - getDefinitionAtOffset(document: HtmlDocument, offset: number, request: LitAnalyzerRequest): LitDefinition | undefined { + getDefinitionAtOffset(document: HtmlDocument, offset: DocumentOffset, context: LitAnalyzerContext): LitDefinition | undefined { const hit = document.htmlNodeOrAttrAtOffset(offset); if (hit == null) return undefined; if (isHTMLNode(hit)) { - return definitionForHtmlNode(hit, request); + return definitionForHtmlNode(hit, context); } else if (isHTMLAttr(hit)) { - return definitionForHtmlAttr(hit, request); + return definitionForHtmlAttr(hit, context); } return; } - getRenameInfoAtOffset(document: HtmlDocument, offset: number, request: LitAnalyzerRequest): LitRenameInfo | undefined { + getRenameInfoAtOffset(document: HtmlDocument, offset: DocumentOffset, context: LitAnalyzerContext): LitRenameInfo | undefined { const hit = document.htmlNodeOrAttrAtOffset(offset); if (hit == null) return undefined; @@ -89,7 +92,7 @@ export class LitHtmlDocumentAnalyzer { kind: "memberVariableElement", fullDisplayName: hit.tagName, displayName: hit.tagName, - range: { document, ...hit.location.name }, + range: documentRangeToSFRange(document, { ...hit.location.name }), document, target: hit }; @@ -97,20 +100,20 @@ export class LitHtmlDocumentAnalyzer { return; } - getRenameLocationsAtOffset(document: HtmlDocument, offset: number, request: LitAnalyzerRequest): LitRenameLocation[] { - return renameLocationsAtOffset(document, offset, request); + getRenameLocationsAtOffset(document: HtmlDocument, offset: DocumentOffset, context: LitAnalyzerContext): LitRenameLocation[] { + return renameLocationsAtOffset(document, offset, context); } - getQuickInfoAtOffset(document: HtmlDocument, offset: number, request: LitAnalyzerRequest): LitQuickInfo | undefined { + getQuickInfoAtOffset(document: HtmlDocument, offset: DocumentOffset, context: LitAnalyzerContext): LitQuickInfo | undefined { const hit = document.htmlNodeOrAttrAtOffset(offset); if (hit == null) return undefined; if (isHTMLNode(hit)) { - return quickInfoForHtmlNode(hit, request); + return quickInfoForHtmlNode(hit, context); } if (isHTMLAttr(hit)) { - return quickInfoForHtmlAttr(hit, request); + return quickInfoForHtmlAttr(hit, context); } return; } @@ -136,7 +139,7 @@ export class LitHtmlDocumentAnalyzer { autoCollapse: false, bannerText: node.tagName, kind: LitOutliningSpanKind.Code, - location: { document, start: node.location.startTag.end, end: endIndex } + location: documentRangeToSFRange(document, { start: node.location.startTag.end, end: endIndex }) } as LitOutliningSpan; }) ); diff --git a/packages/lit-analyzer/src/analyze/document-analyzer/html/lit-html-vscode-service.ts b/packages/lit-analyzer/src/analyze/document-analyzer/html/lit-html-vscode-service.ts index cacafd2c..9d6ca63d 100644 --- a/packages/lit-analyzer/src/analyze/document-analyzer/html/lit-html-vscode-service.ts +++ b/packages/lit-analyzer/src/analyze/document-analyzer/html/lit-html-vscode-service.ts @@ -4,6 +4,8 @@ import { HtmlDocument } from "../../parse/document/text-document/html-document/h import { textPartsToRanges } from "../../parse/document/virtual-document/virtual-document"; import { LitClosingTagInfo } from "../../types/lit-closing-tag-info"; import { LitFormatEdit } from "../../types/lit-format-edit"; +import { DocumentOffset } from "../../types/range"; +import { documentRangeToSFRange, makeDocumentRange } from "../../util/range-util"; const htmlService = vscode.getLanguageService(); @@ -16,7 +18,7 @@ function makeVscHtmlDocument(vscTextDocument: vscode.TextDocument) { } export class LitHtmlVscodeService { - getClosingTagAtOffset(document: HtmlDocument, offset: number): LitClosingTagInfo | undefined { + getClosingTagAtOffset(document: HtmlDocument, offset: DocumentOffset): LitClosingTagInfo | undefined { const vscTextDocument = makeVscTextDocument(document); const vscHtmlDocument = makeVscHtmlDocument(vscTextDocument); const htmlLSPosition = vscTextDocument.positionAt(offset); @@ -31,10 +33,12 @@ export class LitHtmlVscodeService { } format(document: HtmlDocument, settings: ts.FormatCodeSettings): LitFormatEdit[] { - const parts = document.virtualDocument.getPartsAtOffsetRange({ - start: 0, - end: document.virtualDocument.location.end - document.virtualDocument.location.start - }); + const parts = document.virtualDocument.getPartsAtDocumentRange( + makeDocumentRange({ + start: 0, + end: document.virtualDocument.location.end - document.virtualDocument.location.start + }) + ); const ranges = textPartsToRanges(parts); const originalHtml = parts.map(p => (typeof p === "string" ? p : `[#${"#".repeat(p.getText().length)}]`)).join(""); @@ -64,7 +68,7 @@ export class LitHtmlVscodeService { return splitted.map((newText, i) => { const range = ranges[i]; - return { range: { document, ...range }, newText }; + return { range: documentRangeToSFRange(document, range), newText }; }); } } diff --git a/packages/lit-analyzer/src/analyze/document-analyzer/html/quick-info/quick-info-for-html-attr.ts b/packages/lit-analyzer/src/analyze/document-analyzer/html/quick-info/quick-info-for-html-attr.ts index 51711ed9..9c740d97 100644 --- a/packages/lit-analyzer/src/analyze/document-analyzer/html/quick-info/quick-info-for-html-attr.ts +++ b/packages/lit-analyzer/src/analyze/document-analyzer/html/quick-info/quick-info-for-html-attr.ts @@ -1,15 +1,15 @@ -import { LitAnalyzerRequest } from "../../../lit-analyzer-context"; +import { LitAnalyzerContext } from "../../../lit-analyzer-context"; import { descriptionForTarget, targetKindAndTypeText } from "../../../parse/parse-html-data/html-tag"; import { HtmlNodeAttr } from "../../../types/html-node/html-node-attr-types"; import { LitQuickInfo } from "../../../types/lit-quick-info"; -import { rangeFromHtmlNodeAttr } from "../../../util/lit-range-util"; +import { rangeFromHtmlNodeAttr } from "../../../util/range-util"; -export function quickInfoForHtmlAttr(htmlAttr: HtmlNodeAttr, { document, htmlStore }: LitAnalyzerRequest): LitQuickInfo | undefined { +export function quickInfoForHtmlAttr(htmlAttr: HtmlNodeAttr, { htmlStore }: LitAnalyzerContext): LitQuickInfo | undefined { const target = htmlStore.getHtmlAttrTarget(htmlAttr); if (target == null) return undefined; return { - range: rangeFromHtmlNodeAttr(document, htmlAttr), + range: rangeFromHtmlNodeAttr(htmlAttr), primaryInfo: targetKindAndTypeText(target, { modifier: htmlAttr.modifier }), secondaryInfo: descriptionForTarget(target, { markdown: true }) }; diff --git a/packages/lit-analyzer/src/analyze/document-analyzer/html/quick-info/quick-info-for-html-node.ts b/packages/lit-analyzer/src/analyze/document-analyzer/html/quick-info/quick-info-for-html-node.ts index 173f585b..f4c2bda4 100644 --- a/packages/lit-analyzer/src/analyze/document-analyzer/html/quick-info/quick-info-for-html-node.ts +++ b/packages/lit-analyzer/src/analyze/document-analyzer/html/quick-info/quick-info-for-html-node.ts @@ -1,15 +1,15 @@ -import { LitAnalyzerRequest } from "../../../lit-analyzer-context"; +import { LitAnalyzerContext } from "../../../lit-analyzer-context"; import { documentationForHtmlTag } from "../../../parse/parse-html-data/html-tag"; import { HtmlNode } from "../../../types/html-node/html-node-types"; import { LitQuickInfo } from "../../../types/lit-quick-info"; -import { rangeFromHtmlNode } from "../../../util/lit-range-util"; +import { rangeFromHtmlNode } from "../../../util/range-util"; -export function quickInfoForHtmlNode(htmlNode: HtmlNode, { htmlStore, document }: LitAnalyzerRequest): LitQuickInfo | undefined { +export function quickInfoForHtmlNode(htmlNode: HtmlNode, { htmlStore }: LitAnalyzerContext): LitQuickInfo | undefined { const htmlTag = htmlStore.getHtmlTag(htmlNode); if (htmlTag == null) return undefined; return { - range: rangeFromHtmlNode(document, htmlNode), + range: rangeFromHtmlNode(htmlNode), primaryInfo: `<${htmlNode.tagName}>`, secondaryInfo: documentationForHtmlTag(htmlTag, { markdown: true }) }; diff --git a/packages/lit-analyzer/src/analyze/document-analyzer/html/rename-locations/rename-locations-at-offset.ts b/packages/lit-analyzer/src/analyze/document-analyzer/html/rename-locations/rename-locations-at-offset.ts index 7728f0d2..2841f576 100644 --- a/packages/lit-analyzer/src/analyze/document-analyzer/html/rename-locations/rename-locations-at-offset.ts +++ b/packages/lit-analyzer/src/analyze/document-analyzer/html/rename-locations/rename-locations-at-offset.ts @@ -1,15 +1,16 @@ -import { LitAnalyzerRequest } from "../../../lit-analyzer-context"; +import { LitAnalyzerContext } from "../../../lit-analyzer-context"; import { HtmlDocument } from "../../../parse/document/text-document/html-document/html-document"; import { isHTMLNode } from "../../../types/html-node/html-node-types"; import { LitRenameLocation } from "../../../types/lit-rename-location"; +import { DocumentOffset } from "../../../types/range"; import { renameLocationsForTagName } from "./rename-locations-for-tag-name"; -export function renameLocationsAtOffset(document: HtmlDocument, offset: number, request: LitAnalyzerRequest): LitRenameLocation[] { +export function renameLocationsAtOffset(document: HtmlDocument, offset: DocumentOffset, context: LitAnalyzerContext): LitRenameLocation[] { const hit = document.htmlNodeOrAttrAtOffset(offset); if (hit == null) return []; if (isHTMLNode(hit)) { - return renameLocationsForTagName(hit.tagName, request); + return renameLocationsForTagName(hit.tagName, context); } return []; diff --git a/packages/lit-analyzer/src/analyze/document-analyzer/html/rename-locations/rename-locations-for-tag-name.ts b/packages/lit-analyzer/src/analyze/document-analyzer/html/rename-locations/rename-locations-for-tag-name.ts index cafe0f41..01ebc069 100644 --- a/packages/lit-analyzer/src/analyze/document-analyzer/html/rename-locations/rename-locations-for-tag-name.ts +++ b/packages/lit-analyzer/src/analyze/document-analyzer/html/rename-locations/rename-locations-for-tag-name.ts @@ -1,16 +1,17 @@ import { JSDocUnknownTag } from "typescript"; -import { LitAnalyzerRequest } from "../../../lit-analyzer-context"; +import { LitAnalyzerContext } from "../../../lit-analyzer-context"; import { HtmlDocument } from "../../../parse/document/text-document/html-document/html-document"; import { HtmlNode } from "../../../types/html-node/html-node-types"; import { LitRenameLocation } from "../../../types/lit-rename-location"; import { findChild } from "../../../util/ast-util"; import { iterableFirst } from "../../../util/iterable-util"; +import { documentRangeToSFRange, makeSourceFileRange } from "../../../util/range-util"; -export function renameLocationsForTagName(tagName: string, request: LitAnalyzerRequest): LitRenameLocation[] { +export function renameLocationsForTagName(tagName: string, context: LitAnalyzerContext): LitRenameLocation[] { const locations: LitRenameLocation[] = []; - for (const sourceFile of request.program.getSourceFiles()) { - const documents = request.documentStore.getDocumentsInFile(sourceFile, request.config); + for (const sourceFile of context.program.getSourceFiles()) { + const documents = context.documentStore.getDocumentsInFile(sourceFile, context.config); for (const document of documents) { if (document instanceof HtmlDocument) { @@ -27,7 +28,7 @@ export function renameLocationsForTagName(tagName: string, request: LitAnalyzerR } } - const definition = request.definitionStore.getDefinitionForTagName(tagName); + const definition = context.definitionStore.getDefinitionForTagName(tagName); if (definition != null) { // TODO const definitionNode = iterableFirst(definition.tagNameNodes); @@ -35,16 +36,16 @@ export function renameLocationsForTagName(tagName: string, request: LitAnalyzerR if (definitionNode != null) { const fileName = definitionNode.getSourceFile().fileName; - if (request.ts.isCallLikeExpression(definitionNode)) { - const stringLiteralNode = findChild(definitionNode, child => request.ts.isStringLiteralLike(child) && child.text === tagName); + if (context.ts.isCallLikeExpression(definitionNode)) { + const stringLiteralNode = findChild(definitionNode, child => context.ts.isStringLiteralLike(child) && child.text === tagName); if (stringLiteralNode != null) { locations.push({ fileName, - range: { file: request.file, start: stringLiteralNode.getStart() + 1, end: stringLiteralNode.getEnd() - 1 } + range: makeSourceFileRange({ start: stringLiteralNode.getStart() + 1, end: stringLiteralNode.getEnd() - 1 }) }); } - } else if (definitionNode.kind === request.ts.SyntaxKind.JSDocTag) { + } else if (definitionNode.kind === context.ts.SyntaxKind.JSDocTag) { const jsDocTagNode = definitionNode as JSDocUnknownTag; if (jsDocTagNode.comment != null) { @@ -52,16 +53,16 @@ export function renameLocationsForTagName(tagName: string, request: LitAnalyzerR locations.push({ fileName, - range: { file: request.file, start, end: start + jsDocTagNode.comment.length } + range: makeSourceFileRange({ start, end: start + jsDocTagNode.comment.length }) }); } - } else if (request.ts.isInterfaceDeclaration(definitionNode)) { - const stringLiteralNode = findChild(definitionNode, child => request.ts.isStringLiteralLike(child) && child.text === tagName); + } else if (context.ts.isInterfaceDeclaration(definitionNode)) { + const stringLiteralNode = findChild(definitionNode, child => context.ts.isStringLiteralLike(child) && child.text === tagName); if (stringLiteralNode != null) { locations.push({ fileName, - range: { file: request.file, start: stringLiteralNode.getStart() + 1, end: stringLiteralNode.getEnd() - 1 } + range: makeSourceFileRange({ start: stringLiteralNode.getStart() + 1, end: stringLiteralNode.getEnd() - 1 }) }); } } @@ -80,14 +81,14 @@ interface VisitHtmlNodeContext { function visitHtmlNode(node: HtmlNode, context: VisitHtmlNodeContext) { if (node.tagName === context.tagName) { context.emitRenameLocation({ - range: { document: context.document, ...node.location.name }, + range: documentRangeToSFRange(context.document, node.location.name), fileName: context.document.virtualDocument.fileName }); if (node.location.endTag != null) { const { start, end } = node.location.endTag; context.emitRenameLocation({ - range: { document: context.document, start: start + 2, end: end - 1 }, + range: documentRangeToSFRange(context.document, { start: start + 2, end: end - 1 }), fileName: context.document.virtualDocument.fileName }); } diff --git a/packages/lit-analyzer/src/analyze/lit-analyzer-config.ts b/packages/lit-analyzer/src/analyze/lit-analyzer-config.ts index f1b89a3c..2165406b 100644 --- a/packages/lit-analyzer/src/analyze/lit-analyzer-config.ts +++ b/packages/lit-analyzer/src/analyze/lit-analyzer-config.ts @@ -1,7 +1,7 @@ import { HtmlData } from "./parse/parse-html-data/html-data-tag"; import { LitDiagnosticSeverity } from "./types/lit-diagnostic"; -export type LitAnalyzerRuleName = +export type LitAnalyzerRuleId = | "no-unknown-tag-name" | "no-missing-import" | "no-unclosed-tag" @@ -23,7 +23,7 @@ export type LitAnalyzerRuleName = | "no-invalid-tag-name" | "no-invalid-css"; -export const ALL_RULE_NAMES: LitAnalyzerRuleName[] = [ +export const ALL_RULE_NAMES: LitAnalyzerRuleId[] = [ "no-unknown-tag-name", "no-missing-import", "no-unclosed-tag", @@ -48,7 +48,7 @@ export const ALL_RULE_NAMES: LitAnalyzerRuleName[] = [ export type LitAnalyzerRuleSeverity = "off" | "warn" | "warning" | "error" | 0 | 1 | 2 | true | false; -export type LitAnalyzerRules = Partial>; +export type LitAnalyzerRules = Partial>; const DEFAULT_RULES_NOSTRICT: Required = { "no-unknown-tag-name": "off", @@ -96,23 +96,23 @@ const DEFAULT_RULES_STRICT: Required = { "no-invalid-css": "error" }; -export function ruleSeverity(rules: LitAnalyzerConfig | LitAnalyzerRules, ruleName: LitAnalyzerRuleName): LitAnalyzerRuleSeverity { - if ("rules" in rules) return ruleSeverity(rules.rules, ruleName); +export function ruleSeverity(rules: LitAnalyzerConfig | LitAnalyzerRules, ruleId: LitAnalyzerRuleId): LitAnalyzerRuleSeverity { + if ("rules" in rules) return ruleSeverity(rules.rules, ruleId); - const ruleConfig = rules[ruleName] || "off"; + const ruleConfig = rules[ruleId] || "off"; return Array.isArray(ruleConfig) ? ruleConfig[0] : ruleConfig; } -export function isRuleDisabled(config: LitAnalyzerConfig, ruleName: LitAnalyzerRuleName): boolean { - return ["off", 0, false].includes(ruleSeverity(config, ruleName)); +export function isRuleDisabled(config: LitAnalyzerConfig, ruleId: LitAnalyzerRuleId): boolean { + return ["off", 0, false].includes(ruleSeverity(config, ruleId)); } -export function isRuleEnabled(config: LitAnalyzerConfig, ruleName: LitAnalyzerRuleName): boolean { - return !isRuleDisabled(config, ruleName); +export function isRuleEnabled(config: LitAnalyzerConfig, ruleId: LitAnalyzerRuleId): boolean { + return !isRuleDisabled(config, ruleId); } -export function litDiagnosticRuleSeverity(config: LitAnalyzerConfig, ruleName: LitAnalyzerRuleName): LitDiagnosticSeverity { - switch (ruleSeverity(config, ruleName)) { +export function litDiagnosticRuleSeverity(config: LitAnalyzerConfig, ruleId: LitAnalyzerRuleId): LitDiagnosticSeverity { + switch (ruleSeverity(config, ruleId)) { case "off": case false: case 0: diff --git a/packages/lit-analyzer/src/analyze/lit-analyzer-context.ts b/packages/lit-analyzer/src/analyze/lit-analyzer-context.ts index f2e5e338..2d3e0229 100644 --- a/packages/lit-analyzer/src/analyze/lit-analyzer-context.ts +++ b/packages/lit-analyzer/src/analyze/lit-analyzer-context.ts @@ -1,10 +1,9 @@ import * as tsMod from "typescript"; -import * as tsServer from "typescript/lib/tsserverlibrary"; import { Program, SourceFile } from "typescript"; -import { RuleModule } from "./types/rule-module"; +import * as tsServer from "typescript/lib/tsserverlibrary"; import { LitAnalyzerConfig } from "./lit-analyzer-config"; import { LitAnalyzerLogger } from "./lit-analyzer-logger"; -import { TextDocument } from "./parse/document/text-document/text-document"; +import { RuleCollection } from "./rule-collection"; import { AnalyzerDefinitionStore } from "./store/analyzer-definition-store"; import { AnalyzerDependencyStore } from "./store/analyzer-dependency-store"; import { AnalyzerDocumentStore } from "./store/analyzer-document-store"; @@ -20,15 +19,14 @@ export interface LitAnalyzerContext { readonly documentStore: AnalyzerDocumentStore; readonly definitionStore: AnalyzerDefinitionStore; readonly logger: LitAnalyzerLogger; - readonly rules: RuleModule[]; + readonly rules: RuleCollection; + readonly currentFile: SourceFile; + updateConfig(config: LitAnalyzerConfig): void; updateDependencies(file: SourceFile): void; updateComponents(file: SourceFile): void; -} -export interface LitAnalyzerRequest extends LitAnalyzerContext { - file: SourceFile; - document?: TextDocument; + setCurrentFile(file: SourceFile | undefined): void; } export interface LitPluginContextHandler { diff --git a/packages/lit-analyzer/src/analyze/lit-analyzer.ts b/packages/lit-analyzer/src/analyze/lit-analyzer.ts index 6cadb2e8..cec49666 100644 --- a/packages/lit-analyzer/src/analyze/lit-analyzer.ts +++ b/packages/lit-analyzer/src/analyze/lit-analyzer.ts @@ -1,10 +1,10 @@ import { setTypescriptModule as setTypescriptModuleTsSimpleType } from "ts-simple-type"; import { SourceFile } from "typescript"; +import { ComponentAnalyzer } from "./component-analyzer/component-analyzer"; import { LitCssDocumentAnalyzer } from "./document-analyzer/css/lit-css-document-analyzer"; import { LitHtmlDocumentAnalyzer } from "./document-analyzer/html/lit-html-document-analyzer"; import { renameLocationsForTagName } from "./document-analyzer/html/rename-locations/rename-locations-for-tag-name"; -import { isRuleEnabled } from "./lit-analyzer-config"; -import { LitAnalyzerContext, LitAnalyzerRequest } from "./lit-analyzer-context"; +import { LitAnalyzerContext } from "./lit-analyzer-context"; import { CssDocument } from "./parse/document/text-document/css-document/css-document"; import { HtmlDocument } from "./parse/document/text-document/html-document/html-document"; import { TextDocument } from "./parse/document/text-document/text-document"; @@ -20,14 +20,16 @@ import { LitOutliningSpan } from "./types/lit-outlining-span"; import { LitQuickInfo } from "./types/lit-quick-info"; import { LitRenameInfo } from "./types/lit-rename-info"; import { LitRenameLocation } from "./types/lit-rename-location"; -import { Range } from "./types/range"; -import { flatten } from "./util/array-util"; +import { DocumentOffset, Range, SourceFilePosition } from "./types/range"; +import { arrayFlat } from "./util/array-util"; import { getNodeAtPosition, nodeIntersects } from "./util/ast-util"; import { iterableFirst } from "./util/iterable-util"; +import { makeSourceFileRange, sfRangeToDocumentRange } from "./util/range-util"; export class LitAnalyzer { private litHtmlDocumentAnalyzer = new LitHtmlDocumentAnalyzer(); private litCssDocumentAnalyzer = new LitCssDocumentAnalyzer(); + private componentAnalyzer = new ComponentAnalyzer(); constructor(private context: LitAnalyzerContext) { // Set the Typescript module @@ -37,11 +39,13 @@ export class LitAnalyzer { } getOutliningSpansInFile(file: SourceFile): LitOutliningSpan[] { + this.context.setCurrentFile(file); + const documents = this.getDocumentsInFile(file); this.context.updateComponents(file); - return flatten( + return arrayFlat( documents.map(document => { if (document instanceof CssDocument) { return []; @@ -54,47 +58,47 @@ export class LitAnalyzer { ); } - getDefinitionAtPosition(file: SourceFile, position: number): LitDefinition | undefined { + getDefinitionAtPosition(file: SourceFile, position: SourceFilePosition): LitDefinition | undefined { + this.context.setCurrentFile(file); + const { document, offset } = this.getDocumentAndOffsetAtPosition(file, position); if (document == null) return undefined; this.context.updateComponents(file); - const request = this.makeRequest({ file, document }); - if (document instanceof CssDocument) { - return this.litCssDocumentAnalyzer.getDefinitionAtOffset(document, offset, request); + return this.litCssDocumentAnalyzer.getDefinitionAtOffset(document, offset, this.context); } else if (document instanceof HtmlDocument) { - return this.litHtmlDocumentAnalyzer.getDefinitionAtOffset(document, offset, request); + return this.litHtmlDocumentAnalyzer.getDefinitionAtOffset(document, offset, this.context); } return; } - getQuickInfoAtPosition(file: SourceFile, position: number): LitQuickInfo | undefined { + getQuickInfoAtPosition(file: SourceFile, position: SourceFilePosition): LitQuickInfo | undefined { + this.context.setCurrentFile(file); + const { document, offset } = this.getDocumentAndOffsetAtPosition(file, position); if (document == null) return undefined; this.context.updateComponents(file); - const request = this.makeRequest({ file, document }); - if (document instanceof CssDocument) { - return this.litCssDocumentAnalyzer.getQuickInfoAtOffset(document, offset, request); + return this.litCssDocumentAnalyzer.getQuickInfoAtOffset(document, offset, this.context); } else if (document instanceof HtmlDocument) { - return this.litHtmlDocumentAnalyzer.getQuickInfoAtOffset(document, offset, request); + return this.litHtmlDocumentAnalyzer.getQuickInfoAtOffset(document, offset, this.context); } return; } - getRenameInfoAtPosition(file: SourceFile, position: number): LitRenameInfo | undefined { + getRenameInfoAtPosition(file: SourceFile, position: SourceFilePosition): LitRenameInfo | undefined { + this.context.setCurrentFile(file); + const { document, offset } = this.getDocumentAndOffsetAtPosition(file, position); if (document != null) { - const request = this.makeRequest({ file, document }); - if (document instanceof CssDocument) { return undefined; } else if (document instanceof HtmlDocument) { - return this.litHtmlDocumentAnalyzer.getRenameInfoAtOffset(document, offset, request); + return this.litHtmlDocumentAnalyzer.getRenameInfoAtOffset(document, offset, this.context); } } else { const nodeUnderCursor = getNodeAtPosition(file, position); @@ -108,7 +112,7 @@ export class LitAnalyzer { return { fullDisplayName: tagName, displayName: tagName, - range: { file, start: nodeUnderCursor.getStart() + 1, end: nodeUnderCursor.getEnd() - 1 }, + range: makeSourceFileRange({ start: nodeUnderCursor.getStart() + 1, end: nodeUnderCursor.getEnd() - 1 }), kind: "label", target: definition }; @@ -118,27 +122,29 @@ export class LitAnalyzer { return; } - getRenameLocationsAtPosition(file: SourceFile, position: number): LitRenameLocation[] { + getRenameLocationsAtPosition(file: SourceFile, position: SourceFilePosition): LitRenameLocation[] { + this.context.setCurrentFile(file); + const renameInfo = this.getRenameInfoAtPosition(file, position); if (renameInfo == null) return []; if ("document" in renameInfo) { const document = renameInfo.document; - const offset = document.virtualDocument.scPositionToOffset(position); - const request = this.makeRequest({ file, document }); + const offset = document.virtualDocument.sfPositionToDocumentOffset(position); if (document instanceof CssDocument) { return []; } else { - return this.litHtmlDocumentAnalyzer.getRenameLocationsAtOffset(document, offset, request); + return this.litHtmlDocumentAnalyzer.getRenameLocationsAtOffset(document, offset, this.context); } } else { - const request = this.makeRequest({ file }); - return renameLocationsForTagName(renameInfo.target.tagName, request); + return renameLocationsForTagName(renameInfo.target.tagName, this.context); } } - getClosingTagAtPosition(file: SourceFile, position: number): LitClosingTagInfo | undefined { + getClosingTagAtPosition(file: SourceFile, position: SourceFilePosition): LitClosingTagInfo | undefined { + this.context.setCurrentFile(file); + const { document, offset } = this.getDocumentAndOffsetAtPosition(file, position); if (document == null) return undefined; @@ -150,122 +156,95 @@ export class LitAnalyzer { return; } - getCompletionDetailsAtPosition(file: SourceFile, position: number, name: string): LitCompletionDetails | undefined { + getCompletionDetailsAtPosition(file: SourceFile, position: SourceFilePosition, name: string): LitCompletionDetails | undefined { + this.context.setCurrentFile(file); + const { document, offset } = this.getDocumentAndOffsetAtPosition(file, position); if (document == null) return undefined; - const request = this.makeRequest({ file, document }); - if (document instanceof CssDocument) { - return this.litCssDocumentAnalyzer.getCompletionDetailsAtOffset(document, offset, name, request); + return this.litCssDocumentAnalyzer.getCompletionDetailsAtOffset(document, offset, name, this.context); } else if (document instanceof HtmlDocument) { - return this.litHtmlDocumentAnalyzer.getCompletionDetailsAtOffset(document, offset, name, request); + return this.litHtmlDocumentAnalyzer.getCompletionDetailsAtOffset(document, offset, name, this.context); } return; } - getCompletionsAtPosition(file: SourceFile, position: number): LitCompletion[] | undefined { + getCompletionsAtPosition(file: SourceFile, position: SourceFilePosition): LitCompletion[] | undefined { + this.context.setCurrentFile(file); + const { document, offset } = this.getDocumentAndOffsetAtPosition(file, position); if (document == null) return undefined; this.context.updateComponents(file); - const request = this.makeRequest({ file, document }); - if (document instanceof CssDocument) { - return this.litCssDocumentAnalyzer.getCompletionsAtOffset(document, offset, request); + return this.litCssDocumentAnalyzer.getCompletionsAtOffset(document, offset, this.context); } else if (document instanceof HtmlDocument) { - return this.litHtmlDocumentAnalyzer.getCompletionsAtOffset(document, offset, request); + return this.litHtmlDocumentAnalyzer.getCompletionsAtOffset(document, offset, this.context); } return; } getDiagnosticsInFile(file: SourceFile): LitDiagnostic[] { - const diagnostics: LitDiagnostic[] = []; - - const documents = this.getDocumentsInFile(file); + this.context.setCurrentFile(file); this.context.updateComponents(file); this.context.updateDependencies(file); - const analysisResult = this.context.definitionStore.getAnalysisResultForFile(file); - if (analysisResult != null) { - const request = this.makeRequest({ file }); - - for (const definition of analysisResult.componentDefinitions) { - const declaration = definition.declaration(); - - for (const member of declaration.members) { - for (const rule of this.context.rules) { - if (isRuleEnabled(this.context.config, rule.name)) { - const result = rule.visitComponentMember?.(member, request); - if (result != null) { - diagnostics.push(...result); - } - } - } - } + const documents = this.getDocumentsInFile(file); - for (const rule of this.context.rules) { - if (isRuleEnabled(this.context.config, rule.name)) { - if (rule.visitComponentDefinition != null) { - const result = rule.visitComponentDefinition(definition, request); - if (result != null) { - diagnostics.push(...result); - } - } - - if (rule.visitComponentDeclaration != null) { - const result = rule.visitComponentDeclaration(declaration, request); - - if (result != null) { - diagnostics.push(...result); - } - } - } - } - } + const diagnostics: LitDiagnostic[] = []; + + // Get diagnostics for components in this file + const definitions = this.context.definitionStore.getDefinitionsWithDeclarationInFile(file); + for (const definition of definitions) { + diagnostics.push(...this.componentAnalyzer.getDiagnostics(definition, this.context)); } + // Get diagnostics for documents in this file for (const document of documents) { - const request = this.makeRequest({ document, file }); - if (document instanceof CssDocument) { - diagnostics.push(...this.litCssDocumentAnalyzer.getDiagnostics(document, request)); + diagnostics.push(...this.litCssDocumentAnalyzer.getDiagnostics(document, this.context)); } else if (document instanceof HtmlDocument) { - diagnostics.push(...this.litHtmlDocumentAnalyzer.getDiagnostics(document, request)); + diagnostics.push(...this.litHtmlDocumentAnalyzer.getDiagnostics(document, this.context)); } } return diagnostics; } - getCodeFixesAtPositionRange(file: SourceFile, positionRange: Range): LitCodeFix[] { - const { document } = this.getDocumentAndOffsetAtPosition(file, positionRange.start); - if (document == null) return []; + getCodeFixesAtPositionRange(file: SourceFile, sourceFileRange: Range): LitCodeFix[] { + this.context.setCurrentFile(file); + + const { document } = this.getDocumentAndOffsetAtPosition(file, sourceFileRange.start); this.context.updateComponents(file); this.context.updateDependencies(file); - const offsetRange: Range = { - start: document.virtualDocument.scPositionToOffset(positionRange.start), - end: document.virtualDocument.scPositionToOffset(positionRange.end) - }; - - const request = this.makeRequest({ file, document }); - + // Return fixes for intersecting document if (document instanceof HtmlDocument) { - return this.litHtmlDocumentAnalyzer.getCodeFixesAtOffsetRange(document, offsetRange, request); + return this.litHtmlDocumentAnalyzer.getCodeFixesAtOffsetRange(document, sfRangeToDocumentRange(document, sourceFileRange), this.context); + } + + // Else, return fixes for components in this file + else { + const definitions = this.context.definitionStore.getDefinitionsWithDeclarationInFile(file); + for (const definition of definitions) { + return this.componentAnalyzer.getCodeFixesAtOffsetRange(definition, makeSourceFileRange(sourceFileRange), this.context); + } } return []; } getFormatEditsInFile(file: SourceFile, settings: ts.FormatCodeSettings): LitFormatEdit[] { + this.context.setCurrentFile(file); + const documents = this.getDocumentsInFile(file); - return flatten( + return arrayFlat( documents.map(document => { if (document instanceof CssDocument) { return []; @@ -278,86 +257,15 @@ export class LitAnalyzer { ); } - /*private sendRequest>>( funcName: "getCompletionsAtOffset", { file, document }: { file: SourceFile; document: TextDocument }, offset: number); - private sendRequest>>( funcName: FuncName, { file, document }: { file: SourceFile; document: TextDocument }, arg1: number ) { - const request = this.makeRequest({ file, document }); - - const func = (() => { - if (document instanceof CssDocument) { - //return this.litCssDocumentAnalyzer[funcName]; - } else if (document instanceof HtmlDocument) { - return this.litHtmlDocumentAnalyzer[funcName]; - } - })() as LitDocumentAnalyzer[FuncName]; - - if (func == null) return undefined; - - switch (funcName) { - case "getCompletionsAtOffset": - return func(document, arg1, request); - } - }*/ - - /*private sendRequest< - FuncName extends keyof LitDocumentAnalyzer, - Params extends Parameters>, - Arg = Params extends { length: infer L } ? (L extends 1 ? never : (L extends 2 ? never : Params[1])) : never - >(funcName: FuncName, { file, document }: { file: SourceFile; document: TextDocument }, arg1: Arg) { - const request = this.makeRequest({ file, document }); - - const func = (() => { - if (document instanceof CssDocument) { - return this.litCssDocumentAnalyzer[funcName]; - } else if (document instanceof HtmlDocument) { - return this.litHtmlDocumentAnalyzer[funcName]; - } - })(); - - if (arg1 == null) { - } - }*/ - - private makeRequest(options: { document?: TextDocument; file: SourceFile }): LitAnalyzerRequest { - const { - project, - htmlStore, - dependencyStore, - definitionStore, - config, - updateDependencies, - updateComponents, - ts, - program, - documentStore, - logger, - updateConfig, - rules - } = this.context; - - return { - htmlStore, - dependencyStore, - definitionStore, - config, - updateDependencies, - updateComponents, - ts, - project, - program, - documentStore, - logger, - updateConfig, - rules, - ...options - }; - } - - private getDocumentAndOffsetAtPosition(sourceFile: SourceFile, position: number): { document: TextDocument | undefined; offset: number } { + private getDocumentAndOffsetAtPosition( + sourceFile: SourceFile, + position: SourceFilePosition + ): { document: TextDocument | undefined; offset: DocumentOffset } { const document = this.context.documentStore.getDocumentAtPosition(sourceFile, position, this.context.config); return { document, - offset: document != null ? document.virtualDocument.scPositionToOffset(position) : -1 + offset: document != null ? document.virtualDocument.sfPositionToDocumentOffset(position) : -1 }; } diff --git a/packages/lit-analyzer/src/analyze/parse/document/parse-documents-in-source-file.ts b/packages/lit-analyzer/src/analyze/parse/document/parse-documents-in-source-file.ts index a2275d52..3cb090bb 100644 --- a/packages/lit-analyzer/src/analyze/parse/document/parse-documents-in-source-file.ts +++ b/packages/lit-analyzer/src/analyze/parse/document/parse-documents-in-source-file.ts @@ -1,7 +1,8 @@ import { SourceFile, TaggedTemplateExpression } from "typescript"; import { HtmlNodeKind, IHtmlNodeStyleTag } from "../../types/html-node/html-node-types"; -import { flatten } from "../../util/array-util"; -import { intersects } from "../../util/general-util"; +import { SourceFilePosition } from "../../types/range"; +import { arrayFlat } from "../../util/array-util"; +import { documentRangeToSFRange, intersects, makeDocumentRange } from "../../util/range-util"; import { findTaggedTemplates } from "../tagged-template/find-tagged-templates"; import { CssDocument } from "./text-document/css-document/css-document"; import { HtmlDocument } from "./text-document/html-document/html-document"; @@ -15,11 +16,15 @@ export interface ParseDocumentOptions { } export function parseDocumentsInSourceFile(sourceFile: SourceFile, options: ParseDocumentOptions): TextDocument[]; -export function parseDocumentsInSourceFile(sourceFile: SourceFile, options: ParseDocumentOptions, position: number): TextDocument | undefined; export function parseDocumentsInSourceFile( sourceFile: SourceFile, options: ParseDocumentOptions, - position?: number + position: SourceFilePosition +): TextDocument | undefined; +export function parseDocumentsInSourceFile( + sourceFile: SourceFile, + options: ParseDocumentOptions, + position?: SourceFilePosition ): TextDocument[] | TextDocument | undefined { // Parse html tags in the relevant source file const templateTags = [...options.cssTags, ...options.htmlTags]; @@ -37,7 +42,7 @@ export function parseDocumentsInSourceFile( if (result == null) return undefined; if (Array.isArray(result)) { - return flatten( + return arrayFlat( result.map(document => { const res = unpackHtmlDocument(document, position); return [document, ...(res == null ? [] : Array.isArray(res) ? res : [res])]; @@ -62,9 +67,9 @@ function taggedTemplateToDocument(taggedTemplate: TaggedTemplateExpression, { cs } } -function unpackHtmlDocument(textDocument: TextDocument, position: number): TextDocument | undefined; -function unpackHtmlDocument(textDocument: TextDocument, position?: number): TextDocument | TextDocument[]; -function unpackHtmlDocument(textDocument: TextDocument, position?: number): TextDocument[] | TextDocument | undefined { +function unpackHtmlDocument(textDocument: TextDocument, position: SourceFilePosition): TextDocument | undefined; +function unpackHtmlDocument(textDocument: TextDocument, position?: SourceFilePosition): TextDocument | TextDocument[]; +function unpackHtmlDocument(textDocument: TextDocument, position?: SourceFilePosition): TextDocument[] | TextDocument | undefined { const documents: TextDocument[] = []; if (textDocument instanceof HtmlDocument) { @@ -76,7 +81,7 @@ function unpackHtmlDocument(textDocument: TextDocument, position?: number): Text documents.push(nestedDocument); } } else if ( - intersects(textDocument.virtualDocument.scPositionToOffset(position), { + intersects(textDocument.virtualDocument.sfPositionToDocumentOffset(position), { start: rootNode.location.startTag.end, end: rootNode.location.endTag.start }) @@ -95,17 +100,16 @@ function unpackHtmlDocument(textDocument: TextDocument, position?: number): Text function styleHtmlNodeToCssDocument(htmlDocument: HtmlDocument, styleNode: IHtmlNodeStyleTag): CssDocument | undefined { if (styleNode.location.endTag == null) return undefined; - const cssDocumentParts = htmlDocument.virtualDocument.getPartsAtOffsetRange({ - start: styleNode.location.startTag.end, - end: styleNode.location.endTag.start - }); + const cssDocumentParts = htmlDocument.virtualDocument.getPartsAtDocumentRange( + makeDocumentRange({ + start: styleNode.location.startTag.end, + end: styleNode.location.endTag.start + }) + ); const cssVirtualDocument = new VirtualAstCssDocument( cssDocumentParts, - { - start: htmlDocument.virtualDocument.offsetToSCPosition(styleNode.location.startTag.end), - end: htmlDocument.virtualDocument.offsetToSCPosition(styleNode.location.endTag.start) - }, + documentRangeToSFRange(htmlDocument, styleNode.location.startTag), htmlDocument.virtualDocument.fileName ); diff --git a/packages/lit-analyzer/src/analyze/parse/document/text-document/html-document/html-document.ts b/packages/lit-analyzer/src/analyze/parse/document/text-document/html-document/html-document.ts index 3d3779b7..66933193 100644 --- a/packages/lit-analyzer/src/analyze/parse/document/text-document/html-document/html-document.ts +++ b/packages/lit-analyzer/src/analyze/parse/document/text-document/html-document/html-document.ts @@ -1,16 +1,16 @@ -import { Range } from "../../../../types/range"; -import { intersects } from "../../../../util/general-util"; -import { VirtualDocument } from "../../virtual-document/virtual-document"; -import { TextDocument } from "../text-document"; import { HtmlNodeAttr } from "../../../../types/html-node/html-node-attr-types"; import { HtmlNode } from "../../../../types/html-node/html-node-types"; +import { DocumentOffset, DocumentRange } from "../../../../types/range"; +import { intersects } from "../../../../util/range-util"; +import { VirtualDocument } from "../../virtual-document/virtual-document"; +import { TextDocument } from "../text-document"; export class HtmlDocument extends TextDocument { constructor(virtualDocument: VirtualDocument, public rootNodes: HtmlNode[]) { super(virtualDocument); } - htmlAttrAreaAtOffset(offset: number | Range): HtmlNode | undefined { + htmlAttrAreaAtOffset(offset: DocumentOffset | DocumentRange): HtmlNode | undefined { return this.mapFindOne(node => { if (offset > node.location.name.end && intersects(offset, node.location.startTag)) { // Check if the position intersects any attributes. Break if so. @@ -26,23 +26,23 @@ export class HtmlDocument extends TextDocument { }); } - htmlAttrAssignmentAtOffset(offset: number | Range): HtmlNodeAttr | undefined { + htmlAttrAssignmentAtOffset(offset: DocumentOffset | DocumentRange): HtmlNodeAttr | undefined { return this.findAttr(attr => attr.assignment != null && attr.assignment.location != null ? intersects(offset, attr.assignment.location) : false ); } - htmlAttrNameAtOffset(offset: number | Range): HtmlNodeAttr | undefined { + htmlAttrNameAtOffset(offset: DocumentOffset | DocumentRange): HtmlNodeAttr | undefined { return this.findAttr(attr => intersects(offset, attr.location.name)); } - htmlNodeNameAtOffset(offset: number | Range): HtmlNode | undefined { + htmlNodeNameAtOffset(offset: DocumentOffset | DocumentRange): HtmlNode | undefined { return this.findNode( node => intersects(offset, node.location.name) || (node.location.endTag != null && intersects(offset, node.location.endTag)) ); } - htmlNodeOrAttrAtOffset(offset: number | Range): HtmlNode | HtmlNodeAttr | undefined { + htmlNodeOrAttrAtOffset(offset: DocumentOffset | DocumentRange): HtmlNode | HtmlNodeAttr | undefined { const htmlNode = this.htmlNodeNameAtOffset(offset); if (htmlNode != null) return htmlNode; @@ -56,7 +56,7 @@ export class HtmlDocument extends TextDocument { * This method can be used to find out which tag to close in the HTML. * @param offset */ - htmlNodeClosestToOffset(offset: number): HtmlNode | undefined { + htmlNodeClosestToOffset(offset: DocumentOffset): HtmlNode | undefined { let closestNode: HtmlNode | undefined = undefined; // Use 'findNode' to iterate nodes. Keep track of the closest node. diff --git a/packages/lit-analyzer/src/analyze/parse/document/text-document/html-document/parse-html-document.ts b/packages/lit-analyzer/src/analyze/parse/document/text-document/html-document/parse-html-document.ts index 56330df7..9b3cbcb0 100644 --- a/packages/lit-analyzer/src/analyze/parse/document/text-document/html-document/parse-html-document.ts +++ b/packages/lit-analyzer/src/analyze/parse/document/text-document/html-document/parse-html-document.ts @@ -1,9 +1,9 @@ import { Expression, TaggedTemplateExpression } from "typescript"; -import { Range } from "../../../../types/range"; +import { DocumentRange } from "../../../../types/range"; import { VirtualAstHtmlDocument } from "../../virtual-document/virtual-html-document"; import { HtmlDocument } from "./html-document"; -import { parseHtmlNodes } from "./parse-html-node/parse-html-node"; import { ParseHtmlContext } from "./parse-html-node/parse-html-context"; +import { parseHtmlNodes } from "./parse-html-node/parse-html-node"; import { parseHtml } from "./parse-html-p5/parse-html"; export function parseHtmlDocuments(nodes: TaggedTemplateExpression[]): HtmlDocument[] { @@ -14,15 +14,17 @@ export function parseHtmlDocument(node: TaggedTemplateExpression): HtmlDocument const virtualDocument = new VirtualAstHtmlDocument(node); const html = virtualDocument.text; const htmlAst = parseHtml(html); + const document = new HtmlDocument(virtualDocument, []); const context: ParseHtmlContext = { html, - getPartsAtOffsetRange(range: Range): (Expression | string)[] { - return virtualDocument.getPartsAtOffsetRange(range); + document, + getPartsAtOffsetRange(range: DocumentRange): (Expression | string)[] { + return virtualDocument.getPartsAtDocumentRange(range); } }; - const childNodes = parseHtmlNodes(htmlAst.childNodes, undefined, context); + document.rootNodes = parseHtmlNodes(htmlAst.childNodes, undefined, context); - return new HtmlDocument(virtualDocument, childNodes); + return document; } diff --git a/packages/lit-analyzer/src/analyze/parse/document/text-document/html-document/parse-html-node/parse-html-attribute.ts b/packages/lit-analyzer/src/analyze/parse/document/text-document/html-document/parse-html-node/parse-html-attribute.ts index 86f1a79d..27d0ae8e 100644 --- a/packages/lit-analyzer/src/analyze/parse/document/text-document/html-document/parse-html-node/parse-html-attribute.ts +++ b/packages/lit-analyzer/src/analyze/parse/document/text-document/html-document/parse-html-node/parse-html-attribute.ts @@ -47,6 +47,7 @@ export function parseHtmlNodeAttr(p5Node: IP5TagNode, p5Attr: IP5NodeAttr, conte const htmlAttrBase: IHtmlNodeAttrBase = { name: name.toLowerCase(), // Parse5 lowercases all attributes names. Therefore ".myAttr" becomes ".myattr" + document: context.document, modifier, htmlNode, location diff --git a/packages/lit-analyzer/src/analyze/parse/document/text-document/html-document/parse-html-node/parse-html-context.ts b/packages/lit-analyzer/src/analyze/parse/document/text-document/html-document/parse-html-node/parse-html-context.ts index d3c03bd5..3f3c0427 100644 --- a/packages/lit-analyzer/src/analyze/parse/document/text-document/html-document/parse-html-node/parse-html-context.ts +++ b/packages/lit-analyzer/src/analyze/parse/document/text-document/html-document/parse-html-node/parse-html-context.ts @@ -1,7 +1,9 @@ import { Expression } from "typescript"; import { Range } from "../../../../../types/range"; +import { HtmlDocument } from "../html-document"; export interface ParseHtmlContext { html: string; + document: HtmlDocument; getPartsAtOffsetRange(range: Range): (string | Expression)[]; } diff --git a/packages/lit-analyzer/src/analyze/parse/document/text-document/html-document/parse-html-node/parse-html-node.ts b/packages/lit-analyzer/src/analyze/parse/document/text-document/html-document/parse-html-node/parse-html-node.ts index 7b38e5bd..293fc967 100644 --- a/packages/lit-analyzer/src/analyze/parse/document/text-document/html-document/parse-html-node/parse-html-node.ts +++ b/packages/lit-analyzer/src/analyze/parse/document/text-document/html-document/parse-html-node/parse-html-node.ts @@ -1,7 +1,7 @@ import { TS_IGNORE_FLAG } from "../../../../../constants"; import { HtmlNode, HtmlNodeKind, IHtmlNodeBase, IHtmlNodeSourceCodeLocation } from "../../../../../types/html-node/html-node-types"; import { isCommentNode, isTagNode } from "../parse-html-p5/parse-html"; -import { IP5TagNode, P5Node, getSourceLocation } from "../parse-html-p5/parse-html-types"; +import { getSourceLocation, IP5TagNode, P5Node } from "../parse-html-p5/parse-html-types"; import { parseHtmlNodeAttrs } from "./parse-html-attribute"; import { ParseHtmlContext } from "./parse-html-context"; @@ -53,6 +53,7 @@ export function parseHtmlNode(p5Node: IP5TagNode, parent: HtmlNode | undefined, attributes: [], location: makeHtmlNodeLocation(p5Node, context), children: [], + document: context.document, parent }; diff --git a/packages/lit-analyzer/src/analyze/parse/document/virtual-document/virtual-ast-document.ts b/packages/lit-analyzer/src/analyze/parse/document/virtual-document/virtual-ast-document.ts index efbd592f..bce868f0 100644 --- a/packages/lit-analyzer/src/analyze/parse/document/virtual-document/virtual-ast-document.ts +++ b/packages/lit-analyzer/src/analyze/parse/document/virtual-document/virtual-ast-document.ts @@ -1,7 +1,7 @@ import { Expression, Node, TaggedTemplateExpression } from "typescript"; import { tsModule } from "../../../ts-module"; -import { Range } from "../../../types/range"; -import { intersects } from "../../../util/general-util"; +import { DocumentOffset, DocumentRange, Range, SourceFilePosition, SourceFileRange } from "../../../types/range"; +import { intersects, makeSourceFileRange } from "../../../util/range-util"; import { VirtualDocument } from "./virtual-document"; function getPartLength(part: Node): number { @@ -11,7 +11,7 @@ function getPartLength(part: Node): number { export class VirtualAstDocument implements VirtualDocument { readonly fileName: string; - readonly location: Range; + readonly location: SourceFileRange; private readonly parts: (Expression | string)[]; private _text?: string; @@ -40,7 +40,7 @@ export class VirtualAstDocument implements VirtualDocument { return this._text; } - getPartsAtOffsetRange(range: Range): (Expression | string)[] { + getPartsAtDocumentRange(range: DocumentRange): (Expression | string)[] { if (range == null) { return this.parts; } @@ -91,17 +91,17 @@ export class VirtualAstDocument implements VirtualDocument { return resultParts; } - scPositionToOffset(position: number): number { + sfPositionToDocumentOffset(position: SourceFilePosition): DocumentOffset { return position - this.location.start; } - offsetToSCPosition(offset: number): number { + documentOffsetToSFPosition(offset: DocumentOffset): SourceFilePosition { return this.location.start + offset; } - constructor(parts: (Expression | string)[], location: Range, fileName: string); + constructor(parts: (Expression | string)[], location: SourceFileRange, fileName: string); constructor(astNode: TaggedTemplateExpression); - constructor(astNodeOrParts: TaggedTemplateExpression | (Expression | string)[], location?: Range, fileName?: string) { + constructor(astNodeOrParts: TaggedTemplateExpression | (Expression | string)[], location?: SourceFileRange, fileName?: string) { if (Array.isArray(astNodeOrParts)) { this.parts = astNodeOrParts.map((p, i) => typeof p === "string" ? `${i !== 0 ? "}" : ""}${p}${i !== astNodeOrParts.length - 1 ? "${" : ""}` : p @@ -120,10 +120,10 @@ export class VirtualAstDocument implements VirtualDocument { if (expressionPart != null) this.parts.push(expressionPart); }); - this.location = { + this.location = makeSourceFileRange({ start: astNodeOrParts.template.getStart() + 1, end: astNodeOrParts.template.getEnd() - 1 - }; + }); this.fileName = this.fileName = astNodeOrParts.getSourceFile().fileName; } diff --git a/packages/lit-analyzer/src/analyze/parse/document/virtual-document/virtual-document.ts b/packages/lit-analyzer/src/analyze/parse/document/virtual-document/virtual-document.ts index 8a903daa..238c061e 100644 --- a/packages/lit-analyzer/src/analyze/parse/document/virtual-document/virtual-document.ts +++ b/packages/lit-analyzer/src/analyze/parse/document/virtual-document/virtual-document.ts @@ -1,13 +1,13 @@ import { Expression } from "typescript"; -import { Range } from "../../../types/range"; +import { DocumentOffset, DocumentRange, Range, SourceFilePosition, SourceFileRange } from "../../../types/range"; export interface VirtualDocument { fileName: string; - location: Range; + location: SourceFileRange; text: string; - getPartsAtOffsetRange(range?: Range): (Expression | string)[]; - scPositionToOffset(position: number): number; - offsetToSCPosition(offset: number): number; + getPartsAtDocumentRange(range?: DocumentRange): (Expression | string)[]; + sfPositionToDocumentOffset(position: SourceFilePosition): DocumentOffset; + documentOffsetToSFPosition(offset: DocumentOffset): SourceFilePosition; } export function textPartsToRanges(parts: (Expression | string)[]): Range[] { diff --git a/packages/lit-analyzer/src/analyze/rule-collection.ts b/packages/lit-analyzer/src/analyze/rule-collection.ts new file mode 100644 index 00000000..64a03d6b --- /dev/null +++ b/packages/lit-analyzer/src/analyze/rule-collection.ts @@ -0,0 +1,140 @@ +import { ComponentDefinition } from "web-component-analyzer"; +import { isRuleEnabled, LitAnalyzerRuleId } from "./lit-analyzer-config"; +import { LitAnalyzerContext } from "./lit-analyzer-context"; +import { HtmlDocument } from "./parse/document/text-document/html-document/html-document"; +import { HtmlNodeAttr } from "./types/html-node/html-node-attr-types"; +import { HtmlNode } from "./types/html-node/html-node-types"; +import { RuleDiagnostic } from "./types/rule/rule-diagnostic"; +import { RuleModule, RuleModuleImplementation } from "./types/rule/rule-module"; +import { RuleModuleContext } from "./types/rule/rule-module-context"; +import { iterableFirst } from "./util/iterable-util"; + +export interface ReportedRuleDiagnostic { + source: LitAnalyzerRuleId; + diagnostic: RuleDiagnostic; +} + +export class RuleCollection { + private rules: RuleModule[] = []; + + push(...rule: RuleModule[]) { + this.rules.push(...rule); + + // Sort rules by most important first + this.rules.sort((ruleA, ruleB) => (getPriorityValue(ruleA) > getPriorityValue(ruleB) ? -1 : 1)); + } + + private invokeRule( + functionName: VisitFunctionName, + parameter: Parameters>[0], + report: (diagnostic: ReportedRuleDiagnostic) => void, + baseContext: LitAnalyzerContext + ): void { + let shouldBreak = false; + + const { config, htmlStore, program, definitionStore, dependencyStore, documentStore, logger, ts } = baseContext; + + let currentRuleId: LitAnalyzerRuleId | undefined = undefined; + + const context: RuleModuleContext = { + config, + htmlStore, + program, + definitionStore, + dependencyStore, + documentStore, + logger, + ts, + file: baseContext.currentFile, + report(diagnostic: RuleDiagnostic): void { + if (currentRuleId != null) { + report({ diagnostic, source: currentRuleId }); + } + shouldBreak = true; + }, + break(): void { + shouldBreak = true; + } + }; + + for (const rule of this.rules) { + if (isRuleEnabled(context.config, rule.id)) { + const func = rule[functionName]; + if (func != null) { + currentRuleId = rule.id; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + func(parameter as any, context); + } + } + + if (shouldBreak) { + break; + } + } + } + + getDiagnosticsFromDefinition(definition: ComponentDefinition, baseContext: LitAnalyzerContext): ReportedRuleDiagnostic[] { + const file = baseContext.currentFile; + + const diagnostics: ReportedRuleDiagnostic[] = []; + + if (iterableFirst(definition.tagNameNodes)?.getSourceFile() === file) { + this.invokeRule("visitComponentDefinition", definition, d => diagnostics.push(d), baseContext); + } + + const declaration = definition.declaration(); + this.invokeRule("visitComponentDeclaration", declaration, d => diagnostics.push(d), baseContext); + + for (const member of declaration.members) { + if (member.node.getSourceFile() === file) { + this.invokeRule("visitComponentMember", member, d => diagnostics.push(d), baseContext); + } + } + + return diagnostics; + } + + getDiagnosticsFromDocument(htmlDocument: HtmlDocument, baseContext: LitAnalyzerContext): ReportedRuleDiagnostic[] { + const diagnostics: ReportedRuleDiagnostic[] = []; + + const iterateNodes = (nodes: HtmlNode[]) => { + for (const childNode of nodes) { + this.invokeRule("visitHtmlNode", childNode, d => diagnostics.push(d), baseContext); + + const iterateAttrs = (attrs: HtmlNodeAttr[]) => { + for (const attr of attrs) { + this.invokeRule("visitHtmlAttribute", attr, d => diagnostics.push(d), baseContext); + + if (attr.assignment != null) { + this.invokeRule("visitHtmlAssignment", attr.assignment, d => diagnostics.push(d), baseContext); + } + } + }; + + iterateAttrs(childNode.attributes); + + iterateNodes(childNode.children); + } + }; + + iterateNodes(htmlDocument.rootNodes); + + return diagnostics; + } +} + +function getPriorityValue(rule: RuleModule): number { + if (rule.meta?.priority != null) { + switch (rule.meta?.priority) { + case "low": + return 0; + case "medium": + return 1; + case "high": + return 2; + } + } + + return 0; +} diff --git a/packages/lit-analyzer/src/analyze/store/analyzer-document-store.ts b/packages/lit-analyzer/src/analyze/store/analyzer-document-store.ts index 42aaa739..611d86e7 100644 --- a/packages/lit-analyzer/src/analyze/store/analyzer-document-store.ts +++ b/packages/lit-analyzer/src/analyze/store/analyzer-document-store.ts @@ -1,8 +1,9 @@ import { SourceFile } from "typescript"; -import { TextDocument } from "../parse/document/text-document/text-document"; import { LitAnalyzerConfig } from "../lit-analyzer-config"; +import { TextDocument } from "../parse/document/text-document/text-document"; +import { SourceFilePosition } from "../types/range"; export interface AnalyzerDocumentStore { - getDocumentAtPosition(sourceFile: SourceFile, position: number, options: LitAnalyzerConfig): TextDocument | undefined; + getDocumentAtPosition(sourceFile: SourceFile, position: SourceFilePosition, options: LitAnalyzerConfig): TextDocument | undefined; getDocumentsInFile(sourceFile: SourceFile, config: LitAnalyzerConfig): TextDocument[]; } diff --git a/packages/lit-analyzer/src/analyze/store/document-store/default-analyzer-document-store.ts b/packages/lit-analyzer/src/analyze/store/document-store/default-analyzer-document-store.ts index cb4011ec..c68761a8 100644 --- a/packages/lit-analyzer/src/analyze/store/document-store/default-analyzer-document-store.ts +++ b/packages/lit-analyzer/src/analyze/store/document-store/default-analyzer-document-store.ts @@ -1,10 +1,12 @@ +import { SourceFile } from "typescript"; +import { LitAnalyzerConfig } from "../../lit-analyzer-config"; import { parseDocumentsInSourceFile } from "../../parse/document/parse-documents-in-source-file"; import { TextDocument } from "../../parse/document/text-document/text-document"; -import { LitAnalyzerConfig } from "../../lit-analyzer-config"; -import { SourceFile } from "typescript"; +import { SourceFilePosition } from "../../types/range"; +import { AnalyzerDocumentStore } from "../analyzer-document-store"; -export class DefaultAnalyzerDocumentStore { - getDocumentAtPosition(sourceFile: SourceFile, position: number, options: LitAnalyzerConfig): TextDocument | undefined { +export class DefaultAnalyzerDocumentStore implements AnalyzerDocumentStore { + getDocumentAtPosition(sourceFile: SourceFile, position: SourceFilePosition, options: LitAnalyzerConfig): TextDocument | undefined { return parseDocumentsInSourceFile( sourceFile, { diff --git a/packages/lit-analyzer/src/analyze/types/html-node/html-node-attr-types.ts b/packages/lit-analyzer/src/analyze/types/html-node/html-node-attr-types.ts index 3846ad95..0f9bcd08 100644 --- a/packages/lit-analyzer/src/analyze/types/html-node/html-node-attr-types.ts +++ b/packages/lit-analyzer/src/analyze/types/html-node/html-node-attr-types.ts @@ -1,7 +1,8 @@ import { LitHtmlAttributeModifier } from "../../constants"; +import { HtmlDocument } from "../../parse/document/text-document/html-document/html-document"; +import { Range } from "../range"; import { HtmlNodeAttrAssignment } from "./html-node-attr-assignment-types"; import { HtmlNode } from "./html-node-types"; -import { Range } from "../range"; export enum HtmlNodeAttrKind { EVENT_LISTENER = "EVENT_LISTENER", @@ -20,6 +21,7 @@ export interface IHtmlNodeAttrBase { location: IHtmlNodeAttrSourceCodeLocation; assignment?: HtmlNodeAttrAssignment; htmlNode: HtmlNode; + document: HtmlDocument; } export interface IHtmlNodeAttrEventListener extends IHtmlNodeAttrBase { diff --git a/packages/lit-analyzer/src/analyze/types/html-node/html-node-types.ts b/packages/lit-analyzer/src/analyze/types/html-node/html-node-types.ts index fe15d5f7..c7373c60 100644 --- a/packages/lit-analyzer/src/analyze/types/html-node/html-node-types.ts +++ b/packages/lit-analyzer/src/analyze/types/html-node/html-node-types.ts @@ -1,5 +1,6 @@ -import { HtmlNodeAttr } from "./html-node-attr-types"; +import { HtmlDocument } from "../../parse/document/text-document/html-document/html-document"; import { Range } from "../range"; +import { HtmlNodeAttr } from "./html-node-attr-types"; export interface IHtmlNodeSourceCodeLocation extends Range { name: Range; @@ -20,6 +21,7 @@ export interface IHtmlNodeBase { parent?: HtmlNode; children: HtmlNode[]; selfClosed: boolean; + document: HtmlDocument; } export interface IHtmlNode extends IHtmlNodeBase { diff --git a/packages/lit-analyzer/src/analyze/types/lit-code-fix-action.ts b/packages/lit-analyzer/src/analyze/types/lit-code-fix-action.ts index 4360a7fc..fc510621 100644 --- a/packages/lit-analyzer/src/analyze/types/lit-code-fix-action.ts +++ b/packages/lit-analyzer/src/analyze/types/lit-code-fix-action.ts @@ -1,23 +1,6 @@ -import { LitRange } from "./lit-range"; +import { SourceFileRange } from "./range"; -export interface TextChange { - range: LitRange; +export interface LitCodeFixAction { + range: SourceFileRange; newText: string; } - -export enum CodeActionKind { - TEXT_CHANGE = "TEXT_CHANGE", - IMPORT_COMPONENT = "IMPORT_COMPONENT" -} - -export interface CodeActionTextChange { - kind: CodeActionKind.TEXT_CHANGE; - change: TextChange; -} - -export interface CodeActionImportComponent { - kind: CodeActionKind.IMPORT_COMPONENT; - importPath: string; -} - -export type LitCodeFixAction = CodeActionTextChange | CodeActionImportComponent; diff --git a/packages/lit-analyzer/src/analyze/types/lit-code-fix.ts b/packages/lit-analyzer/src/analyze/types/lit-code-fix.ts index 0c5b781a..182fedf2 100644 --- a/packages/lit-analyzer/src/analyze/types/lit-code-fix.ts +++ b/packages/lit-analyzer/src/analyze/types/lit-code-fix.ts @@ -1,43 +1,7 @@ import { LitCodeFixAction } from "./lit-code-fix-action"; -import { - LitHtmlDiagnostic, - LitHtmlDiagnosticHtmlBoolMod, - LitHtmlDiagnosticMissingImport, - LitHtmlDiagnosticPrimitiveNotAssignableToComplex, - LitHtmlDiagnosticUnknownMember, - LitHtmlDiagnosticUnknownTag -} from "./lit-diagnostic"; -export enum CodeFixKind { - RENAME = "RENAME", - ADD_TEXT = "ADD_TEXT", - CHANGE_LIT_MODIFIER = "CHANGE_LIT_MODIFIER", - IMPORT_COMPONENT = "IMPORT_COMPONENT" -} - -export interface CodeFixBase { +export interface LitCodeFix { + name: string; message: string; - htmlReport: LitHtmlDiagnostic; actions: LitCodeFixAction[]; } - -export interface CodeFixAddText extends CodeFixBase { - kind: CodeFixKind.ADD_TEXT; -} - -export interface CodeFixRename extends CodeFixBase { - kind: CodeFixKind.RENAME; - htmlReport: LitHtmlDiagnosticUnknownMember | LitHtmlDiagnosticUnknownTag; -} - -export interface CodeFixChangeLitModifier extends CodeFixBase { - kind: CodeFixKind.CHANGE_LIT_MODIFIER; - htmlReport: LitHtmlDiagnosticHtmlBoolMod | LitHtmlDiagnosticPrimitiveNotAssignableToComplex; -} - -export interface CodeFixImportComponent extends CodeFixBase { - kind: CodeFixKind.IMPORT_COMPONENT; - htmlReport: LitHtmlDiagnosticMissingImport; -} - -export type LitCodeFix = CodeFixRename | CodeFixChangeLitModifier | CodeFixImportComponent | CodeFixAddText; diff --git a/packages/lit-analyzer/src/analyze/types/lit-completion.ts b/packages/lit-analyzer/src/analyze/types/lit-completion.ts index 1eb0fb84..de468850 100644 --- a/packages/lit-analyzer/src/analyze/types/lit-completion.ts +++ b/packages/lit-analyzer/src/analyze/types/lit-completion.ts @@ -1,12 +1,12 @@ import { LitTargetKind } from "./lit-target-kind"; -import { DocumentRange } from "./lit-range"; +import { SourceFileRange } from "./range"; export interface LitCompletion { name: string; kind: LitTargetKind; kindModifiers?: "color"; insert: string; - range?: DocumentRange; + range?: SourceFileRange; importance?: "high" | "medium" | "low"; documentation?(): string | undefined; } diff --git a/packages/lit-analyzer/src/analyze/types/lit-definition.ts b/packages/lit-analyzer/src/analyze/types/lit-definition.ts index a249523c..ca98e27f 100644 --- a/packages/lit-analyzer/src/analyze/types/lit-definition.ts +++ b/packages/lit-analyzer/src/analyze/types/lit-definition.ts @@ -1,5 +1,5 @@ import { ComponentDeclaration, ComponentEvent, ComponentMember } from "web-component-analyzer"; -import { DocumentRange } from "./lit-range"; +import { SourceFileRange } from "./range"; export enum DefinitionKind { COMPONENT = "COMPONENT", @@ -8,7 +8,7 @@ export enum DefinitionKind { } export interface DefinitionBase { - fromRange: DocumentRange; + fromRange: SourceFileRange; } export interface DefinitionComponent extends DefinitionBase { diff --git a/packages/lit-analyzer/src/analyze/types/lit-diagnostic.ts b/packages/lit-analyzer/src/analyze/types/lit-diagnostic.ts index 58939ba4..f0ece220 100644 --- a/packages/lit-analyzer/src/analyze/types/lit-diagnostic.ts +++ b/packages/lit-analyzer/src/analyze/types/lit-diagnostic.ts @@ -1,193 +1,15 @@ -import { SimpleType } from "ts-simple-type"; import { SourceFile } from "typescript"; -import { ComponentDefinition } from "web-component-analyzer"; -import { LitAnalyzerRuleName } from "../lit-analyzer-config"; -import { HtmlAttr, HtmlAttrTarget } from "../parse/parse-html-data/html-tag"; -import { IHtmlNodeAttrAssignmentExpression } from "./html-node/html-node-attr-assignment-types"; -import { HtmlNodeAttr, IHtmlNodeAttr } from "./html-node/html-node-attr-types"; -import { HtmlNode } from "./html-node/html-node-types"; -import { LitRange } from "./lit-range"; - -export enum LitHtmlDiagnosticKind { - MISSING_IMPORT = "MISSING_IMPORT", - MISSING_REQUIRED_ATTRS = "MISSING_REQUIRED_ATTRIBUTES", - UNKNOWN_TARGET = "UNKNOWN_TARGET", - UNKNOWN_TAG = "UNKNOWN_TAG", - TAG_NOT_CLOSED = "TAG_NOT_CLOSED", - BOOL_MOD_ON_NON_BOOL = "BOOL_MOD_ON_NON_BOOL", - PROPERTY_NEEDS_EXPRESSION = "PROPERTY_NEEDS_EXPRESSION", - NO_EVENT_LISTENER_FUNCTION = "NO_EVENT_LISTENER_FUNCTION", - PRIMITIVE_NOT_ASSIGNABLE_TO_COMPLEX = "PRIMITIVE_NOT_BINDING_IN_ATTRIBUTE_BINDING", - COMPLEX_NOT_BINDABLE_IN_ATTRIBUTE_BINDING = "COMPLEX_NOT_BINDABLE_IN_ATTRIBUTE_BINDING", - EXPRESSION_ONLY_ASSIGNABLE_WITH_BOOLEAN_BINDING = "EXPRESSION_ONLY_ASSIGNABLE_WITH_BOOLEAN_BINDING", - INVALID_ATTRIBUTE_EXPRESSION_TYPE_UNDEFINED = "INVALID_ATTRIBUTE_EXPRESSION_TYPE_UNDEFINED", - INVALID_ATTRIBUTE_EXPRESSION_TYPE_NULL = "INVALID_ATTRIBUTE_EXPRESSION_TYPE_NULL", - INVALID_ATTRIBUTE_EXPRESSION_TYPE = "INVALID_ATTRIBUTE_EXPRESSION_TYPE", - INVALID_SLOT_NAME = "INVALID_SLOT_NAME", - MISSING_SLOT_ATTRIBUTE = "MISSING_SLOT_ATTRIBUTE", - DIRECTIVE_NOT_ALLOWED_HERE = "DIRECTIVE_NOT_ALLOWED_HERE", - INVALID_MIXED_BINDING = "INVALID_MIXED_BINDING", - INVALID_TAG_NAME = "INVALID_TAG_NAME", - INVALID_ATTRIBUTE_NAME = "INVALID_ATTRIBUTE_NAME", - INVALID_PROPERTY_TYPE = "INVALID_PROPERTY_TYPE" -} +import { LitAnalyzerRuleId } from "../lit-analyzer-config"; +import { SourceFileRange } from "./range"; export type LitDiagnosticSeverity = "error" | "warning"; -export interface LitDiagnosticBase { - location: LitRange; +export interface LitDiagnostic { + location: SourceFileRange; message: string; - fix?: string; - source: LitAnalyzerRuleName; + fixMessage?: string; suggestion?: string; + source: LitAnalyzerRuleId; severity: LitDiagnosticSeverity; file: SourceFile; } - -export interface LitHtmlDiagnosticUnknownTag extends LitDiagnosticBase { - kind: LitHtmlDiagnosticKind.UNKNOWN_TAG; - suggestedName?: string; - htmlNode: HtmlNode; -} - -export interface LitHtmlDiagnosticUnknownMember extends LitDiagnosticBase { - kind: LitHtmlDiagnosticKind.UNKNOWN_TARGET; - htmlAttr: HtmlNodeAttr; - suggestedTarget?: HtmlAttrTarget; -} - -export interface LitHtmlDiagnosticMissingImport extends LitDiagnosticBase { - kind: LitHtmlDiagnosticKind.MISSING_IMPORT; - htmlNode: HtmlNode; - definition: ComponentDefinition; - importPath: string; -} - -export interface LitHtmlDiagnosticMissingProps extends LitDiagnosticBase { - kind: LitHtmlDiagnosticKind.MISSING_REQUIRED_ATTRS; - attrs: HtmlAttr[]; - htmlNode: HtmlNode; -} - -export interface LitHtmlDiagnosticTagNotClosed extends LitDiagnosticBase { - kind: LitHtmlDiagnosticKind.TAG_NOT_CLOSED; - htmlNode: HtmlNode; -} - -export interface LitHtmlDiagnosticHtmlBoolMod extends LitDiagnosticBase { - kind: LitHtmlDiagnosticKind.BOOL_MOD_ON_NON_BOOL; - htmlAttr: HtmlNodeAttr; - typeA: SimpleType; - typeB: SimpleType; -} - -export interface LitHtmlDiagnosticPrimitiveNotAssignableToComplex extends LitDiagnosticBase { - kind: LitHtmlDiagnosticKind.PRIMITIVE_NOT_ASSIGNABLE_TO_COMPLEX; - htmlAttr: HtmlNodeAttr; - typeA: SimpleType; - typeB: SimpleType; -} - -export interface LitHtmlDiagnosticHtmlNoEventListenerFunction extends LitDiagnosticBase { - kind: LitHtmlDiagnosticKind.NO_EVENT_LISTENER_FUNCTION; - typeB: SimpleType; -} - -export interface LitHtmlDiagnosticHtmlInvalidAttributeExpressionType extends LitDiagnosticBase { - kind: LitHtmlDiagnosticKind.INVALID_ATTRIBUTE_EXPRESSION_TYPE; - htmlAttr: HtmlNodeAttr; - typeA: SimpleType; - typeB: SimpleType; -} - -export interface LitHtmlDiagnosticHtmlInvalidAttributeExpressionTypeUndefined extends LitDiagnosticBase { - kind: LitHtmlDiagnosticKind.INVALID_ATTRIBUTE_EXPRESSION_TYPE_UNDEFINED; - htmlAttr: IHtmlNodeAttr & { assignment: IHtmlNodeAttrAssignmentExpression }; - typeA: SimpleType; - typeB: SimpleType; -} - -export interface LitHtmlDiagnosticHtmlInvalidAttributeExpressionTypeNull extends LitDiagnosticBase { - kind: LitHtmlDiagnosticKind.INVALID_ATTRIBUTE_EXPRESSION_TYPE_NULL; - htmlAttr: IHtmlNodeAttr & { assignment: IHtmlNodeAttrAssignmentExpression }; - typeA: SimpleType; - typeB: SimpleType; -} - -export interface LitHtmlDiagnosticHtmlExpressionOnlyAssignableWithBooleanBinding extends LitDiagnosticBase { - kind: LitHtmlDiagnosticKind.EXPRESSION_ONLY_ASSIGNABLE_WITH_BOOLEAN_BINDING; - htmlAttr: HtmlNodeAttr; - typeA: SimpleType; - typeB: SimpleType; -} - -export interface LitHtmlDiagnosticComplexNotBindableInAttributeBinding extends LitDiagnosticBase { - kind: LitHtmlDiagnosticKind.COMPLEX_NOT_BINDABLE_IN_ATTRIBUTE_BINDING; - htmlAttr: HtmlNodeAttr; - typeA: SimpleType; - typeB: SimpleType; -} - -export interface LitHtmlDiagnosticHtmlPropertyNeedsExpression extends LitDiagnosticBase { - kind: LitHtmlDiagnosticKind.PROPERTY_NEEDS_EXPRESSION; -} - -export interface LitHtmlDiagnosticHtmlDirectiveNotAllowedHere extends LitDiagnosticBase { - kind: LitHtmlDiagnosticKind.DIRECTIVE_NOT_ALLOWED_HERE; -} - -export interface LitHtmlDiagnosticInvalidSlotName extends LitDiagnosticBase { - kind: LitHtmlDiagnosticKind.INVALID_SLOT_NAME; - validSlotNames: (string | undefined)[]; -} - -export interface LitHtmlDiagnosticMissingSlotAttr extends LitDiagnosticBase { - kind: LitHtmlDiagnosticKind.MISSING_SLOT_ATTRIBUTE; - htmlNode: HtmlNode; - validSlotNames: string[]; -} - -export interface LitHtmlDiagnosticInvalidMixedBinding extends LitDiagnosticBase { - kind: LitHtmlDiagnosticKind.INVALID_MIXED_BINDING; -} - -export interface LitHtmlDiagnosticInvalidTagName extends LitDiagnosticBase { - kind: LitHtmlDiagnosticKind.INVALID_TAG_NAME; -} - -export interface LitHtmlDiagnosticInvalidAttributeName extends LitDiagnosticBase { - kind: LitHtmlDiagnosticKind.INVALID_ATTRIBUTE_NAME; -} - -export interface LitHtmlDiagnosticInvalidPropertyType extends LitDiagnosticBase { - kind: LitHtmlDiagnosticKind.INVALID_PROPERTY_TYPE; -} - -export type LitHtmlDiagnostic = - | LitHtmlDiagnosticInvalidPropertyType - | LitHtmlDiagnosticInvalidAttributeName - | LitHtmlDiagnosticInvalidTagName - | LitHtmlDiagnosticUnknownTag - | LitHtmlDiagnosticMissingImport - | LitHtmlDiagnosticMissingProps - | LitHtmlDiagnosticHtmlBoolMod - | LitHtmlDiagnosticUnknownMember - | LitHtmlDiagnosticHtmlDirectiveNotAllowedHere - | LitHtmlDiagnosticPrimitiveNotAssignableToComplex - | LitHtmlDiagnosticHtmlInvalidAttributeExpressionType - | LitHtmlDiagnosticHtmlInvalidAttributeExpressionTypeUndefined - | LitHtmlDiagnosticHtmlInvalidAttributeExpressionTypeNull - | LitHtmlDiagnosticHtmlNoEventListenerFunction - | LitHtmlDiagnosticComplexNotBindableInAttributeBinding - | LitHtmlDiagnosticHtmlPropertyNeedsExpression - | LitHtmlDiagnosticHtmlExpressionOnlyAssignableWithBooleanBinding - | LitHtmlDiagnosticInvalidSlotName - | LitHtmlDiagnosticMissingSlotAttr - | LitHtmlDiagnosticInvalidMixedBinding - | LitHtmlDiagnosticTagNotClosed; - -export interface LitCssDiagnostic extends LitDiagnosticBase {} - -//export interface LitSourceFileDiagnostic extends LitSourceFileDiagnosticBase {} - -export type LitDiagnostic = LitHtmlDiagnostic | LitCssDiagnostic /* | LitSourceFileDiagnostic*/; diff --git a/packages/lit-analyzer/src/analyze/types/lit-format-edit.ts b/packages/lit-analyzer/src/analyze/types/lit-format-edit.ts index ef19463e..f8063b1b 100644 --- a/packages/lit-analyzer/src/analyze/types/lit-format-edit.ts +++ b/packages/lit-analyzer/src/analyze/types/lit-format-edit.ts @@ -1,6 +1,6 @@ -import { DocumentRange } from "./lit-range"; +import { SourceFileRange } from "./range"; export interface LitFormatEdit { - range: DocumentRange; + range: SourceFileRange; newText: string; } diff --git a/packages/lit-analyzer/src/analyze/types/lit-outlining-span.ts b/packages/lit-analyzer/src/analyze/types/lit-outlining-span.ts index 0a34eb27..569f63ba 100644 --- a/packages/lit-analyzer/src/analyze/types/lit-outlining-span.ts +++ b/packages/lit-analyzer/src/analyze/types/lit-outlining-span.ts @@ -1,4 +1,4 @@ -import { DocumentRange } from "./lit-range"; +import { SourceFileRange } from "./range"; export enum LitOutliningSpanKind { Comment = "comment", @@ -8,7 +8,7 @@ export enum LitOutliningSpanKind { } export interface LitOutliningSpan { - location: DocumentRange; + location: SourceFileRange; bannerText: string; autoCollapse?: boolean; kind: LitOutliningSpanKind; diff --git a/packages/lit-analyzer/src/analyze/types/lit-quick-info.ts b/packages/lit-analyzer/src/analyze/types/lit-quick-info.ts index 3816fea4..76ef0a80 100644 --- a/packages/lit-analyzer/src/analyze/types/lit-quick-info.ts +++ b/packages/lit-analyzer/src/analyze/types/lit-quick-info.ts @@ -1,7 +1,7 @@ -import { DocumentRange } from "./lit-range"; +import { SourceFileRange } from "./range"; export interface LitQuickInfo { - range: DocumentRange; + range: SourceFileRange; primaryInfo: string; secondaryInfo?: string; } diff --git a/packages/lit-analyzer/src/analyze/types/lit-range.ts b/packages/lit-analyzer/src/analyze/types/lit-range.ts deleted file mode 100644 index 9055fda0..00000000 --- a/packages/lit-analyzer/src/analyze/types/lit-range.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Node, SourceFile } from "typescript"; -import { TextDocument } from "../parse/document/text-document/text-document"; -import { Range } from "./range"; - -export interface SourceFileRange extends Range { - file: SourceFile; -} - -export interface DocumentRange extends Range { - document: TextDocument; -} - -export interface NodeRange extends Range { - node: Node; -} - -export type LitRange = SourceFileRange | DocumentRange | NodeRange; diff --git a/packages/lit-analyzer/src/analyze/types/lit-rename-info.ts b/packages/lit-analyzer/src/analyze/types/lit-rename-info.ts index 660f726b..9acc26b7 100644 --- a/packages/lit-analyzer/src/analyze/types/lit-rename-info.ts +++ b/packages/lit-analyzer/src/analyze/types/lit-rename-info.ts @@ -1,14 +1,14 @@ import { ComponentDefinition } from "web-component-analyzer"; import { HtmlDocument } from "../parse/document/text-document/html-document/html-document"; import { HtmlNode } from "./html-node/html-node-types"; -import { DocumentRange, SourceFileRange } from "./lit-range"; import { LitTargetKind } from "./lit-target-kind"; +import { SourceFileRange } from "./range"; export interface RenameInfoBase { kind: LitTargetKind; displayName: string; fullDisplayName: string; - range: SourceFileRange | DocumentRange; + range: SourceFileRange; } export interface RenameHtmlNodeInfo extends RenameInfoBase { diff --git a/packages/lit-analyzer/src/analyze/types/lit-rename-location.ts b/packages/lit-analyzer/src/analyze/types/lit-rename-location.ts index 92571d03..224ecdac 100644 --- a/packages/lit-analyzer/src/analyze/types/lit-rename-location.ts +++ b/packages/lit-analyzer/src/analyze/types/lit-rename-location.ts @@ -1,8 +1,8 @@ -import { DocumentRange, SourceFileRange } from "./lit-range"; +import { SourceFileRange } from "./range"; export interface LitRenameLocation { fileName: string; prefixText?: string; suffixText?: string; - range: DocumentRange | SourceFileRange; + range: SourceFileRange; } diff --git a/packages/lit-analyzer/src/analyze/types/range.ts b/packages/lit-analyzer/src/analyze/types/range.ts index 53572cd5..72b8deab 100644 --- a/packages/lit-analyzer/src/analyze/types/range.ts +++ b/packages/lit-analyzer/src/analyze/types/range.ts @@ -2,3 +2,25 @@ export interface Range { start: number; end: number; } + +// Offsets and positions +export type DocumentOffset = number; + +export type SourceFilePosition = number; + +/*export type DocumentOffset = number & { _documentOffset: void }; + +export type SourceFilePosition = number & { _sourceFilePosition: void };*/ + +/*export function makeDocumentOffset(offset: number): DocumentOffset { + return offset as DocumentOffset; +} + +export function makeDocumentPosition(position: number): SourceFilePosition { + return position as SourceFilePosition; +}*/ + +// Ranges +export type DocumentRange = { start: DocumentOffset; end: DocumentOffset } & { _documentBrand: void }; + +export type SourceFileRange = { start: SourceFilePosition; end: SourceFilePosition } & { _sourceFileBrand: void }; diff --git a/packages/lit-analyzer/src/analyze/types/rule-module.ts b/packages/lit-analyzer/src/analyze/types/rule-module.ts deleted file mode 100644 index 9ac90add..00000000 --- a/packages/lit-analyzer/src/analyze/types/rule-module.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ComponentDeclaration, ComponentDefinition, ComponentMember } from "web-component-analyzer"; -import { LitAnalyzerRuleName } from "../lit-analyzer-config"; -import { LitAnalyzerRequest } from "../lit-analyzer-context"; -import { HtmlNodeAttrAssignment } from "./html-node/html-node-attr-assignment-types"; -import { HtmlNodeAttr } from "./html-node/html-node-attr-types"; -import { HtmlNode } from "./html-node/html-node-types"; -import { LitHtmlDiagnostic } from "./lit-diagnostic"; - -export interface RuleModule { - name: LitAnalyzerRuleName; - - // Document based rules - visitHtmlNode?(node: HtmlNode, context: LitAnalyzerRequest): LitHtmlDiagnostic[] | void; - visitHtmlAttribute?(attribute: HtmlNodeAttr, context: LitAnalyzerRequest): LitHtmlDiagnostic[] | void; - visitHtmlAssignment?(assignment: HtmlNodeAttrAssignment, context: LitAnalyzerRequest): LitHtmlDiagnostic[] | void; - - // Component based rules - visitComponentDefinition?(definition: ComponentDefinition, context: LitAnalyzerRequest): LitHtmlDiagnostic[] | void; - visitComponentDeclaration?(declaration: ComponentDeclaration, context: LitAnalyzerRequest): LitHtmlDiagnostic[] | void; - visitComponentMember?(declaration: ComponentMember, context: LitAnalyzerRequest): LitHtmlDiagnostic[] | void; -} diff --git a/packages/lit-analyzer/src/analyze/types/rule/rule-diagnostic.ts b/packages/lit-analyzer/src/analyze/types/rule/rule-diagnostic.ts new file mode 100644 index 00000000..130dbffe --- /dev/null +++ b/packages/lit-analyzer/src/analyze/types/rule/rule-diagnostic.ts @@ -0,0 +1,10 @@ +import { SourceFileRange } from "../range"; +import { RuleFix } from "./rule-fix"; + +export interface RuleDiagnostic { + location: SourceFileRange; + message: string; + fixMessage?: string; + suggestion?: string; + fix?: () => RuleFix[] | RuleFix; +} diff --git a/packages/lit-analyzer/src/analyze/types/rule/rule-fix-action.ts b/packages/lit-analyzer/src/analyze/types/rule/rule-fix-action.ts new file mode 100644 index 00000000..96bf4b8c --- /dev/null +++ b/packages/lit-analyzer/src/analyze/types/rule/rule-fix-action.ts @@ -0,0 +1,57 @@ +import { SourceFile } from "typescript"; +import { HtmlNodeAttrAssignment } from "../html-node/html-node-attr-assignment-types"; +import { HtmlNodeAttr } from "../html-node/html-node-attr-types"; +import { HtmlNode } from "../html-node/html-node-types"; + +export type RuleFixActionKind = "changeTagName" | "addAttribute" | "changeAttributeName" | "changeAttributeModifier" | "changeAssignment" | "import"; + +export interface RuleFixActionBase { + kind: RuleFixActionKind; + file?: SourceFile; +} + +export interface RuleFixActionChangeTagName extends RuleFixActionBase { + kind: "changeTagName"; + htmlNode: HtmlNode; + newName: string; +} + +export interface RuleFixActionAddAttribute extends RuleFixActionBase { + kind: "addAttribute"; + htmlNode: HtmlNode; + name: string; + value?: string; +} + +export interface RuleFixActionChangeAttributeName extends RuleFixActionBase { + kind: "changeAttributeName"; + htmlAttr: HtmlNodeAttr; + newName: string; +} + +export interface RuleFixActionChangeAttributeModifier extends RuleFixActionBase { + kind: "changeAttributeModifier"; + htmlAttr: HtmlNodeAttr; + newModifier: string; +} + +export interface RuleFixActionChangeAssignment extends RuleFixActionBase { + kind: "changeAssignment"; + assignment: HtmlNodeAttrAssignment; + newValue: string; +} + +export interface RuleFixActionImport extends RuleFixActionBase { + kind: "import"; + file: SourceFile; + path: string; + identifiers?: string[]; +} + +export type RuleFixAction = + | RuleFixActionChangeTagName + | RuleFixActionAddAttribute + | RuleFixActionChangeAttributeName + | RuleFixActionImport + | RuleFixActionChangeAttributeModifier + | RuleFixActionChangeAssignment; diff --git a/packages/lit-analyzer/src/analyze/types/rule/rule-fix.ts b/packages/lit-analyzer/src/analyze/types/rule/rule-fix.ts new file mode 100644 index 00000000..e8ad6dd8 --- /dev/null +++ b/packages/lit-analyzer/src/analyze/types/rule/rule-fix.ts @@ -0,0 +1,6 @@ +import { RuleFixAction } from "./rule-fix-action"; + +export interface RuleFix { + message: string; + actions: RuleFixAction[]; +} diff --git a/packages/lit-analyzer/src/analyze/types/rule/rule-module-context.ts b/packages/lit-analyzer/src/analyze/types/rule/rule-module-context.ts new file mode 100644 index 00000000..3c021c3d --- /dev/null +++ b/packages/lit-analyzer/src/analyze/types/rule/rule-module-context.ts @@ -0,0 +1,26 @@ +import * as tsMod from "typescript"; +import { Program, SourceFile } from "typescript"; +import { LitAnalyzerConfig } from "../../lit-analyzer-config"; +import { LitAnalyzerLogger } from "../../lit-analyzer-logger"; +import { AnalyzerDefinitionStore } from "../../store/analyzer-definition-store"; +import { AnalyzerDependencyStore } from "../../store/analyzer-dependency-store"; +import { AnalyzerDocumentStore } from "../../store/analyzer-document-store"; +import { AnalyzerHtmlStore } from "../../store/analyzer-html-store"; +import { RuleDiagnostic } from "./rule-diagnostic"; + +export interface RuleModuleContext { + readonly ts: typeof tsMod; + readonly program: Program; + readonly file: SourceFile; + + readonly htmlStore: AnalyzerHtmlStore; + readonly dependencyStore: AnalyzerDependencyStore; + readonly documentStore: AnalyzerDocumentStore; + readonly definitionStore: AnalyzerDefinitionStore; + + readonly logger: LitAnalyzerLogger; + readonly config: LitAnalyzerConfig; + + report(diagnostic: RuleDiagnostic): void; + break(): void; +} diff --git a/packages/lit-analyzer/src/analyze/types/rule/rule-module.ts b/packages/lit-analyzer/src/analyze/types/rule/rule-module.ts new file mode 100644 index 00000000..9d5dff65 --- /dev/null +++ b/packages/lit-analyzer/src/analyze/types/rule/rule-module.ts @@ -0,0 +1,34 @@ +import { ComponentDeclaration, ComponentDefinition, ComponentMember } from "web-component-analyzer"; +import { LitAnalyzerRuleId } from "../../lit-analyzer-config"; +import { HtmlNodeAttrAssignment } from "../html-node/html-node-attr-assignment-types"; +import { HtmlNodeAttr } from "../html-node/html-node-attr-types"; +import { HtmlNode } from "../html-node/html-node-types"; +import { RuleModuleContext } from "./rule-module-context"; + +export type RuleModulePriority = "low" | "medium" | "high"; + +//export type RuleModuleCategory = "HTML" | "CSS" | "Component"; + +export interface RuleModuleImplementation { + // Document based rules + visitHtmlNode?(node: HtmlNode, context: RuleModuleContext): void; + visitHtmlAttribute?(attribute: HtmlNodeAttr, context: RuleModuleContext): void; + visitHtmlAssignment?(assignment: HtmlNodeAttrAssignment, context: RuleModuleContext): void; + + // Component based rules + visitComponentDefinition?(definition: ComponentDefinition, context: RuleModuleContext): void; + visitComponentDeclaration?(declaration: ComponentDeclaration, context: RuleModuleContext): void; + visitComponentMember?(declaration: ComponentMember, context: RuleModuleContext): void; +} + +export interface RuleModule extends RuleModuleImplementation { + id: LitAnalyzerRuleId; + + meta?: { + priority?: RuleModulePriority; + /*docs?: { + description: string; + category: RuleModuleCategory; + };*/ + }; +} diff --git a/packages/lit-analyzer/src/analyze/types/rules.ts b/packages/lit-analyzer/src/analyze/types/rules.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/lit-analyzer/src/analyze/util/array-util.ts b/packages/lit-analyzer/src/analyze/util/array-util.ts index 5dfb287a..ac141ff7 100644 --- a/packages/lit-analyzer/src/analyze/util/array-util.ts +++ b/packages/lit-analyzer/src/analyze/util/array-util.ts @@ -1,9 +1,27 @@ /** - * Flattens a nested array. + * Flattens an array. + * Use this function to keep support for node 10 * @param items */ -export function flatten(items: T[][]): T[] { - return items.reduce((acc, item) => [...acc, ...item], []); +export function arrayFlat(items: (T[] | T)[]): T[] { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if ("flat" in (items as any)) { + return items.flat(); + } + + const flattenArray: T[] = []; + for (const item of items) { + flattenArray.push(...item); + } + return flattenArray; +} + +/** + * Filters an array returning only defined items + * @param array + */ +export function arrayDefined(array: (T | undefined)[]): T[] { + return array.filter((item): item is NonNullable => item != null); } /** diff --git a/packages/lit-analyzer/src/analyze/util/ast-util.ts b/packages/lit-analyzer/src/analyze/util/ast-util.ts index 2889be72..838c5e96 100644 --- a/packages/lit-analyzer/src/analyze/util/ast-util.ts +++ b/packages/lit-analyzer/src/analyze/util/ast-util.ts @@ -1,7 +1,7 @@ import { Node } from "typescript"; import { tsModule } from "../ts-module"; import { Range } from "../types/range"; -import { intersects } from "./general-util"; +import { intersects } from "./range-util"; /** * Tests nodes recursively walking up the tree using parent nodes. diff --git a/packages/lit-analyzer/src/analyze/util/general-util.ts b/packages/lit-analyzer/src/analyze/util/general-util.ts index 53ae4c1e..8bd9b2c2 100644 --- a/packages/lit-analyzer/src/analyze/util/general-util.ts +++ b/packages/lit-analyzer/src/analyze/util/general-util.ts @@ -1,36 +1,4 @@ import { LitHtmlAttributeModifier } from "../constants"; -import { Range } from "../types/range"; - -/** - * Compares two strings case insensitive. - * @param strA - * @param strB - */ -export function caseInsensitiveEquals(strA: string, strB: string): boolean { - return strA.localeCompare(strB, undefined, { sensitivity: "accent" }) === 0; -} - -/** - * Returns if a position is within start and end. - * @param position - * @param start - * @param end - */ -export function intersects(position: number | Range, { start, end }: Range): boolean { - if (typeof position === "number") { - return start <= position && position <= end; - } else { - return start <= position.start && position.end <= end; - } -} - -export function rangeToTSSpan({ start, end }: Range): { start: number; length: number } { - return { start, length: end - start }; -} - -export function tsSpanToRange({ start, length }: { start: number; length: number }): Range { - return { start, end: start + length }; -} // eslint-disable-next-line @typescript-eslint/no-explicit-any export type Newable = { new (...args: any[]): T }; diff --git a/packages/lit-analyzer/src/analyze/util/get-position-context-in-document.ts b/packages/lit-analyzer/src/analyze/util/get-position-context-in-document.ts index 1fb59883..1cd26618 100644 --- a/packages/lit-analyzer/src/analyze/util/get-position-context-in-document.ts +++ b/packages/lit-analyzer/src/analyze/util/get-position-context-in-document.ts @@ -1,8 +1,9 @@ import { TextDocument } from "../parse/document/text-document/text-document"; +import { DocumentOffset } from "../types/range"; export interface DocumentPositionContext { text: string; - offset: number; + offset: DocumentOffset; word: string; leftWord: string; rightWord: string; @@ -15,7 +16,7 @@ export interface DocumentPositionContext { * @param document * @param offset */ -export function getPositionContextInDocument(document: TextDocument, offset: number): DocumentPositionContext { +export function getPositionContextInDocument(document: TextDocument, offset: DocumentOffset): DocumentPositionContext { const text = document.virtualDocument.text; const leftWord = grabWordInDirection({ @@ -65,7 +66,7 @@ function grabWordInDirection({ stopChar: RegExp; direction: "left" | "right"; text: string; - startOffset: number; + startOffset: DocumentOffset; }): string { const dir = direction === "left" ? -1 : 1; let curPosition = startOffset - (dir < 0 ? 1 : 0); diff --git a/packages/lit-analyzer/src/analyze/util/lit-range-util.ts b/packages/lit-analyzer/src/analyze/util/lit-range-util.ts deleted file mode 100644 index bc0a4216..00000000 --- a/packages/lit-analyzer/src/analyze/util/lit-range-util.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Node } from "typescript"; -import { TextDocument } from "../parse/document/text-document/text-document"; -import { HtmlNodeAttr } from "../types/html-node/html-node-attr-types"; -import { HtmlNode } from "../types/html-node/html-node-types"; -import { DocumentRange, NodeRange } from "../types/lit-range"; - -export function rangeFromHtmlNodeAttr(document: TextDocument | undefined, htmlAttr: HtmlNodeAttr): DocumentRange { - return { document: document!, ...htmlAttr.location.name }; -} - -export function rangeFromHtmlNode(document: TextDocument | undefined, htmlAttr: HtmlNode): DocumentRange { - return { document: document!, ...htmlAttr.location.name }; -} - -export function rangeFromNode(node: Node): NodeRange { - return { node, start: node.getStart(), end: node.getEnd() }; -} diff --git a/packages/lit-analyzer/src/analyze/util/range-util.ts b/packages/lit-analyzer/src/analyze/util/range-util.ts new file mode 100644 index 00000000..4cccc0c9 --- /dev/null +++ b/packages/lit-analyzer/src/analyze/util/range-util.ts @@ -0,0 +1,58 @@ +import { Node } from "typescript"; +import { TextDocument } from "../parse/document/text-document/text-document"; +import { HtmlNodeAttr } from "../types/html-node/html-node-attr-types"; +import { HtmlNode } from "../types/html-node/html-node-types"; +import { DocumentRange, Range, SourceFileRange } from "../types/range"; + +export function makeSourceFileRange(range: Range): SourceFileRange { + return range as SourceFileRange; +} + +export function makeDocumentRange(range: Range): DocumentRange { + return range as DocumentRange; +} + +export function rangeFromHtmlNodeAttr(htmlAttr: HtmlNodeAttr): SourceFileRange { + return documentRangeToSFRange(htmlAttr.document, htmlAttr.location.name); + //return { document: htmlAttr.document, ...htmlAttr.location.name }; +} + +export function rangeFromHtmlNode(htmlNode: HtmlNode): SourceFileRange { + return documentRangeToSFRange(htmlNode.document, htmlNode.location.name); + //return { document: htmlNode.document, ...htmlNode.location.name }; +} + +export function rangeFromNode(node: Node): SourceFileRange { + //return { file: node.getSourceFile(), start: node.getStart(), end: node.getEnd() }; + return makeSourceFileRange({ start: node.getStart(), end: node.getEnd() }); +} + +export function documentRangeToSFRange(document: TextDocument, range: DocumentRange | Range): SourceFileRange { + return makeSourceFileRange({ + start: document.virtualDocument.documentOffsetToSFPosition(range.start), + end: document.virtualDocument.documentOffsetToSFPosition(range.end) + }); +} + +export function sfRangeToDocumentRange(document: TextDocument, range: SourceFileRange | Range): DocumentRange { + return makeDocumentRange({ + start: document.virtualDocument.sfPositionToDocumentOffset(range.start), + end: document.virtualDocument.sfPositionToDocumentOffset(range.end) + }); +} + +/** + * Returns if a position is within start and end. + * @param position + * @param start + * @param end + */ +//export function intersects(position: SourceFilePosition | SourceFileRange, { start, end }: SourceFileRange): boolean; +//export function intersects(position: DocumentOffset | DocumentRange, { start, end }: DocumentRange): boolean; +export function intersects(position: number | Range, { start, end }: Range): boolean { + if (typeof position === "number") { + return start <= position && position <= end; + } else { + return start <= position.start && position.end <= end; + } +} diff --git a/packages/lit-analyzer/src/analyze/util/rule-diagnostic-util.ts b/packages/lit-analyzer/src/analyze/util/rule-diagnostic-util.ts new file mode 100644 index 00000000..1f170e96 --- /dev/null +++ b/packages/lit-analyzer/src/analyze/util/rule-diagnostic-util.ts @@ -0,0 +1,19 @@ +import { litDiagnosticRuleSeverity } from "../lit-analyzer-config"; +import { LitAnalyzerContext } from "../lit-analyzer-context"; +import { ReportedRuleDiagnostic } from "../rule-collection"; +import { LitDiagnostic } from "../types/lit-diagnostic"; + +export function convertRuleDiagnosticToLitDiagnostic(reported: ReportedRuleDiagnostic, context: LitAnalyzerContext): LitDiagnostic { + const source = reported.source; + const { message, location, fixMessage, suggestion } = reported.diagnostic; + + return { + fixMessage, + location, + suggestion, + message, + source, + file: context.currentFile, + severity: litDiagnosticRuleSeverity(context.config, source) + }; +} diff --git a/packages/lit-analyzer/src/analyze/util/rule-fix-util.ts b/packages/lit-analyzer/src/analyze/util/rule-fix-util.ts new file mode 100644 index 00000000..500a2484 --- /dev/null +++ b/packages/lit-analyzer/src/analyze/util/rule-fix-util.ts @@ -0,0 +1,133 @@ +import { SourceFile } from "typescript"; +import { tsModule } from "../../../../ts-lit-plugin/src/ts-module"; +import { LitCodeFix } from "../types/lit-code-fix"; +import { LitCodeFixAction } from "../types/lit-code-fix-action"; +import { RuleFix } from "../types/rule/rule-fix"; +import { RuleFixAction } from "../types/rule/rule-fix-action"; +import { arrayFlat } from "./array-util"; +import { documentRangeToSFRange, makeSourceFileRange, rangeFromHtmlNodeAttr } from "./range-util"; + +export function converRuleFixToLitCodeFix(codeFix: RuleFix): LitCodeFix { + return { + name: "", + message: codeFix.message, + actions: arrayFlat(codeFix.actions.map(ruleFixActionConverter)) + }; +} + +function ruleFixActionConverter(action: RuleFixAction): LitCodeFixAction[] { + switch (action.kind) { + case "changeTagName": { + const document = action.htmlNode.document; + const startLocation = action.htmlNode.location.startTag; + const endLocation = action.htmlNode.location.endTag; + + return [ + { + range: documentRangeToSFRange(document, startLocation), + newText: action.newName + }, + ...(endLocation == null + ? [] + : [ + { + range: documentRangeToSFRange(document, { + start: endLocation.start + 2, + end: endLocation.end - 1 + }), + newText: action.newName + } + ]) + ]; + } + + case "addAttribute": { + const htmlNode = action.htmlNode; + + return [ + { + range: documentRangeToSFRange(htmlNode.document, { + start: htmlNode.location.name.end, + end: htmlNode.location.name.end + }), + newText: ` ${action.name}${action.value == null ? "" : `="${action.value}"`}` + } + ]; + } + + case "changeAttributeName": { + return [ + { + range: rangeFromHtmlNodeAttr(action.htmlAttr), + newText: action.newName + } + ]; + } + + case "changeAttributeModifier": { + const document = action.htmlAttr.document; + + return [ + { + // Make a range that includes the modifier. + range: documentRangeToSFRange(document, { + start: action.htmlAttr.location.start, + end: action.htmlAttr.location.name.start + }), + newText: action.newModifier + } + ]; + } + + case "changeAssignment": { + const assignment = action.assignment; + + if (assignment.location == null) { + return []; + } + + return [ + { + range: documentRangeToSFRange(assignment.htmlAttr.document, { + start: assignment.location.start + 2, // Offset 2 for '${' + end: assignment.location.end - 1 // Offset 1 for '}' + }), + newText: action.newValue + } + ]; + } + + case "import": { + // Get the import path and the position where it can be placed + const lastImportIndex = getLastImportIndex(action.file); + + return [ + { + range: makeSourceFileRange({ + start: lastImportIndex, + end: 0 + }), + newText: `\nimport "${action.path}";` + } + ]; + } + } + + return []; +} + +/** + * Returns the position of the last import line. + * @param sourceFile + */ +function getLastImportIndex(sourceFile: SourceFile): number { + let lastImportIndex = 0; + + for (const statement of sourceFile.statements) { + if (tsModule.ts.isImportDeclaration(statement)) { + lastImportIndex = statement.getEnd(); + } + } + + return lastImportIndex; +} diff --git a/packages/lit-analyzer/src/analyze/util/str-util.ts b/packages/lit-analyzer/src/analyze/util/str-util.ts new file mode 100644 index 00000000..e8c9d83a --- /dev/null +++ b/packages/lit-analyzer/src/analyze/util/str-util.ts @@ -0,0 +1,8 @@ +/** + * Compares two strings case insensitive. + * @param strA + * @param strB + */ +export function caseInsensitiveEquals(strA: string, strB: string): boolean { + return strA.localeCompare(strB, undefined, { sensitivity: "accent" }) === 0; +} diff --git a/packages/lit-analyzer/src/analyze/util/type/is-assignable-in-boolean-binding.ts b/packages/lit-analyzer/src/analyze/util/type/is-assignable-in-boolean-binding.ts deleted file mode 100644 index dac2d0e5..00000000 --- a/packages/lit-analyzer/src/analyze/util/type/is-assignable-in-boolean-binding.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { SimpleType, SimpleTypeKind, toTypeString } from "ts-simple-type"; -import { litDiagnosticRuleSeverity } from "../../lit-analyzer-config"; -import { LitAnalyzerRequest } from "../../lit-analyzer-context"; -import { HtmlNodeAttr } from "../../types/html-node/html-node-attr-types"; -import { LitHtmlDiagnostic, LitHtmlDiagnosticKind } from "../../types/lit-diagnostic"; -import { rangeFromHtmlNodeAttr } from "../lit-range-util"; -import { isAssignableToType } from "./is-assignable-to-type"; - -export function isAssignableInBooleanBinding( - htmlAttr: HtmlNodeAttr, - { typeA, typeB }: { typeA: SimpleType; typeB: SimpleType }, - request: LitAnalyzerRequest -): LitHtmlDiagnostic[] | undefined { - // Test if the user is trying to use ? modifier on a non-boolean type. - if (!isAssignableToType({ typeA: { kind: SimpleTypeKind.BOOLEAN }, typeB }, request)) { - return [ - { - kind: LitHtmlDiagnosticKind.INVALID_ATTRIBUTE_EXPRESSION_TYPE, - message: `Type '${toTypeString(typeB)}' is not assignable to 'boolean'`, - severity: litDiagnosticRuleSeverity(request.config, "no-incompatible-type-binding"), - source: "no-incompatible-type-binding", - location: rangeFromHtmlNodeAttr(request.document, htmlAttr), - file: request.file, - htmlAttr, - typeA, - typeB - } - ]; - } - - // Test if the user is trying to use the ? modifier on a non-boolean type. - if (!isAssignableToType({ typeA, typeB: { kind: SimpleTypeKind.BOOLEAN } }, request)) { - return [ - { - kind: LitHtmlDiagnosticKind.BOOL_MOD_ON_NON_BOOL, - message: `You are using a boolean binding on a non boolean type '${toTypeString(typeA)}'`, - severity: litDiagnosticRuleSeverity(request.config, "no-incompatible-type-binding"), - source: "no-incompatible-type-binding", - location: rangeFromHtmlNodeAttr(request.document, htmlAttr), - file: request.file, - htmlAttr, - typeA: { kind: SimpleTypeKind.BOOLEAN }, - typeB - } - ]; - } - - return undefined; -} diff --git a/packages/lit-analyzer/src/analyze/util/type/is-assignable-in-property-binding.ts b/packages/lit-analyzer/src/analyze/util/type/is-assignable-in-property-binding.ts deleted file mode 100644 index bcd0e380..00000000 --- a/packages/lit-analyzer/src/analyze/util/type/is-assignable-in-property-binding.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { SimpleType, toTypeString } from "ts-simple-type"; -import { litDiagnosticRuleSeverity } from "../../lit-analyzer-config"; -import { LitAnalyzerRequest } from "../../lit-analyzer-context"; -import { HtmlNodeAttr } from "../../types/html-node/html-node-attr-types"; -import { LitHtmlDiagnostic, LitHtmlDiagnosticKind } from "../../types/lit-diagnostic"; -import { rangeFromHtmlNodeAttr } from "../lit-range-util"; -import { isAssignableBindingUnderSecuritySystem } from "./is-assignable-binding-under-security-system"; -import { isAssignableToType } from "./is-assignable-to-type"; - -export function isAssignableInPropertyBinding( - htmlAttr: HtmlNodeAttr, - { typeA, typeB }: { typeA: SimpleType; typeB: SimpleType }, - request: LitAnalyzerRequest -): LitHtmlDiagnostic[] | undefined { - const securityDiagnostics = isAssignableBindingUnderSecuritySystem(htmlAttr, { typeA, typeB }, request, "no-incompatible-type-binding"); - if (securityDiagnostics !== undefined) { - // The security diagnostics take precedence here, and we should not - // do any more checking. Note that this may be an empty array. - return securityDiagnostics; - } - - if (!isAssignableToType({ typeA, typeB }, request)) { - return [ - { - kind: LitHtmlDiagnosticKind.INVALID_ATTRIBUTE_EXPRESSION_TYPE, - message: `Type '${toTypeString(typeB)}' is not assignable to '${toTypeString(typeA)}'`, - severity: litDiagnosticRuleSeverity(request.config, "no-incompatible-type-binding"), - source: "no-incompatible-type-binding", - location: rangeFromHtmlNodeAttr(request.document, htmlAttr), - file: request.file, - htmlAttr, - typeA, - typeB - } - ]; - } - - return undefined; -} diff --git a/packages/lit-analyzer/src/cli/analyze-globs.ts b/packages/lit-analyzer/src/cli/analyze-globs.ts index 689961d6..76ce5955 100644 --- a/packages/lit-analyzer/src/cli/analyze-globs.ts +++ b/packages/lit-analyzer/src/cli/analyze-globs.ts @@ -2,7 +2,7 @@ import { async } from "fast-glob"; import { existsSync, lstatSync } from "fs"; import { join } from "path"; import { Diagnostic, flattenDiagnosticMessageText, Program, SourceFile } from "typescript"; -import { flatten } from "../analyze/util/array-util"; +import { arrayFlat } from "../analyze/util/array-util"; import { CompileResult, compileTypescript } from "./compile"; import { LitAnalyzerCliConfig } from "./lit-analyzer-cli-config"; @@ -67,7 +67,7 @@ export async function analyzeGlobs(globs: string[], config: LitAnalyzerCliConfig async function expandGlobs(globs: string | string[]): Promise { globs = Array.isArray(globs) ? globs : [globs]; - return flatten( + return arrayFlat( await Promise.all( globs.map(g => { try { diff --git a/packages/lit-analyzer/src/cli/cli.ts b/packages/lit-analyzer/src/cli/cli.ts index fb68d490..f91f65e6 100644 --- a/packages/lit-analyzer/src/cli/cli.ts +++ b/packages/lit-analyzer/src/cli/cli.ts @@ -1,4 +1,4 @@ -import { LitAnalyzerRuleName, LitAnalyzerRules } from "../analyze/lit-analyzer-config"; +import { LitAnalyzerRuleId, LitAnalyzerRules } from "../analyze/lit-analyzer-config"; import { analyzeCommand } from "./analyze-command"; import { LitAnalyzerCliConfig } from "./lit-analyzer-cli-config"; import { parseCliArguments } from "./parse-cli-arguments"; @@ -34,7 +34,7 @@ export async function cli() { // Always convert "rules" to "dash case" because "rules" expects it. config.rules = Object.entries(config.rules || {}).reduce((acc, [k, v]) => { - acc[camelToDashCase(k) as LitAnalyzerRuleName] = v; + acc[camelToDashCase(k) as LitAnalyzerRuleId] = v; return acc; }, {} as LitAnalyzerRules); diff --git a/packages/lit-analyzer/src/cli/format/code-diagnostic-formatter.ts b/packages/lit-analyzer/src/cli/format/code-diagnostic-formatter.ts index 6d5b5fbf..8bb7d058 100644 --- a/packages/lit-analyzer/src/cli/format/code-diagnostic-formatter.ts +++ b/packages/lit-analyzer/src/cli/format/code-diagnostic-formatter.ts @@ -2,7 +2,7 @@ import chalk from "chalk"; import { SourceFile } from "typescript"; import { LitDiagnostic } from "../../analyze/types/lit-diagnostic"; import { AnalysisStats, DiagnosticFormatter } from "./diagnostic-formatter"; -import { generalReport, markText, relativeFileName, translateRange } from "./util"; +import { generalReport, markText, relativeFileName } from "./util"; export class CodeDiagnosticFormatter implements DiagnosticFormatter { report(stats: AnalysisStats): string | undefined { @@ -21,28 +21,26 @@ ${diagnosticText}`; } function diagnosticTextForFile(file: SourceFile, diagnostic: LitDiagnostic) { - const textSpan = translateRange(diagnostic.location); - const MAX_LINE_WIDTH = 50; const MIN_MESSAGE_PADDING = 10; // Get line and character of start position - const lineContext = file.getLineAndCharacterOfPosition(textSpan.start); + const lineContext = file.getLineAndCharacterOfPosition(diagnostic.location.start); // Get start and end position of the line let linePositionRange = { start: file.getPositionOfLineAndCharacter(lineContext.line, 0), - end: file.getLineEndOfPosition(textSpan.start) + end: file.getLineEndOfPosition(diagnostic.location.start) }; // Modify the line position range if the width of the line exceeds MAX_LINE_WIDTH if (linePositionRange.end - linePositionRange.start > MAX_LINE_WIDTH) { // Calculate even padding to both sides - const padding = Math.max(MIN_MESSAGE_PADDING, Math.round((MAX_LINE_WIDTH - textSpan.length) / 2)); + const padding = Math.max(MIN_MESSAGE_PADDING, Math.round((MAX_LINE_WIDTH - (diagnostic.location.end - diagnostic.location.start)) / 2)); // Calculate new start and end position without exceeding the line position range - const start = Math.max(linePositionRange.start, textSpan.start - padding); - const end = Math.min(linePositionRange.end, textSpan.start + textSpan.length + padding); + const start = Math.max(linePositionRange.start, diagnostic.location.start - padding); + const end = Math.min(linePositionRange.end, diagnostic.location.end + padding); linePositionRange = { start, end }; } @@ -57,8 +55,8 @@ function diagnosticTextForFile(file: SourceFile, diagnostic: LitDiagnostic) { const markedLine = markText( lineText, { - start: textSpan.start - linePositionRange.start, - length: textSpan.length + start: diagnostic.location.start - linePositionRange.start, + length: diagnostic.location.end - diagnostic.location.start }, highlightingColorFunction ).replace(/^\s*/, " "); diff --git a/packages/lit-analyzer/src/cli/format/list-diagnostic-formatter.ts b/packages/lit-analyzer/src/cli/format/list-diagnostic-formatter.ts index 922bef75..1a9d5656 100644 --- a/packages/lit-analyzer/src/cli/format/list-diagnostic-formatter.ts +++ b/packages/lit-analyzer/src/cli/format/list-diagnostic-formatter.ts @@ -2,7 +2,7 @@ import chalk from "chalk"; import { SourceFile } from "typescript"; import { LitDiagnostic } from "../../analyze/types/lit-diagnostic"; import { AnalysisStats, DiagnosticFormatter } from "./diagnostic-formatter"; -import { generalReport, relativeFileName, textPad, translateRange } from "./util"; +import { generalReport, relativeFileName, textPad } from "./util"; export class ListDiagnosticFormatter implements DiagnosticFormatter { report(stats: AnalysisStats): string | undefined { @@ -25,8 +25,7 @@ ${diagnosticText}`; } function litDiagnosticToErrorText(file: SourceFile, diagnostic: LitDiagnostic): string { - const textSpan = translateRange(diagnostic.location); - const lineContext = file.getLineAndCharacterOfPosition(textSpan.start); + const lineContext = file.getLineAndCharacterOfPosition(diagnostic.location.start); const linePart = `${textPad(`${lineContext.line + 1}`, { width: 5 })}:${textPad(`${lineContext.character}`, { width: 4, dir: "right" diff --git a/packages/lit-analyzer/src/cli/format/markdown-formatter.ts b/packages/lit-analyzer/src/cli/format/markdown-formatter.ts index b0bf971a..77bf5d5a 100644 --- a/packages/lit-analyzer/src/cli/format/markdown-formatter.ts +++ b/packages/lit-analyzer/src/cli/format/markdown-formatter.ts @@ -2,7 +2,7 @@ import { SourceFile } from "typescript"; import { LitDiagnostic } from "../../analyze/types/lit-diagnostic"; import { AnalysisStats, DiagnosticFormatter } from "./diagnostic-formatter"; import { markdownHeader, markdownHighlight, markdownTable } from "./markdown-util"; -import { relativeFileName, translateRange } from "./util"; +import { relativeFileName } from "./util"; export class MarkdownDiagnosticFormatter implements DiagnosticFormatter { report(stats: AnalysisStats): string | undefined { @@ -27,8 +27,7 @@ function markdownDiagnosticTable(file: SourceFile, diagnostics: LitDiagnostic[]) const headerRow: string[] = ["Line", "Column", "Type", "Rule", "Message"]; const rows: string[][] = diagnostics.map((diagnostic): string[] => { - const textSpan = translateRange(diagnostic.location); - const lineContext = file.getLineAndCharacterOfPosition(textSpan.start); + const lineContext = file.getLineAndCharacterOfPosition(diagnostic.location.start); return [ (lineContext.line + 1).toString(), diff --git a/packages/lit-analyzer/src/cli/format/util.ts b/packages/lit-analyzer/src/cli/format/util.ts index c36ebe93..cd6c0dd4 100644 --- a/packages/lit-analyzer/src/cli/format/util.ts +++ b/packages/lit-analyzer/src/cli/format/util.ts @@ -1,6 +1,5 @@ import chalk from "chalk"; import { TextSpan } from "typescript"; -import { LitRange } from "../../analyze/types/lit-range"; import { AnalysisStats } from "./diagnostic-formatter"; export function generalReport(stats: AnalysisStats): string { @@ -38,17 +37,3 @@ export function textPad(str: string, { width, fill, dir }: { width: number; fill const padding = (fill || " ").repeat(Math.max(0, width - str.length)); return `${dir !== "right" ? padding : ""}${str}${dir === "right" ? padding : ""}`; } - -export function translateRange(range: LitRange): TextSpan { - if ("document" in range) { - return { - start: range.document.virtualDocument.offsetToSCPosition(range.start), - length: range.end - range.start - }; - } - - return { - start: range.start, - length: range.end - range.start - }; -} diff --git a/packages/lit-analyzer/src/index.ts b/packages/lit-analyzer/src/index.ts index a0ad4b95..43f123ba 100644 --- a/packages/lit-analyzer/src/index.ts +++ b/packages/lit-analyzer/src/index.ts @@ -5,6 +5,7 @@ export * from "./analyze/lit-analyzer-context"; export * from "./analyze/lit-analyzer-logger"; export * from "./analyze/default-lit-analyzer-context"; +export * from "./analyze/types/range"; export * from "./analyze/types/lit-closing-tag-info"; export * from "./analyze/types/lit-code-fix"; export * from "./analyze/types/lit-code-fix-action"; @@ -16,7 +17,6 @@ export * from "./analyze/types/lit-format-edit"; export * from "./analyze/types/lit-outlining-span"; export * from "./analyze/types/lit-quick-info"; export * from "./analyze/types/lit-quick-info"; -export * from "./analyze/types/lit-range"; export * from "./analyze/types/lit-rename-info"; export * from "./analyze/types/lit-rename-location"; export * from "./analyze/types/lit-target-kind"; diff --git a/packages/lit-analyzer/src/rules/no-boolean-in-attribute-binding.ts b/packages/lit-analyzer/src/rules/no-boolean-in-attribute-binding.ts index c0404e0f..e0c83ad5 100644 --- a/packages/lit-analyzer/src/rules/no-boolean-in-attribute-binding.ts +++ b/packages/lit-analyzer/src/rules/no-boolean-in-attribute-binding.ts @@ -1,21 +1,23 @@ import { isAssignableToSimpleTypeKind, SimpleTypeKind } from "ts-simple-type"; -import { litDiagnosticRuleSeverity } from "../analyze/lit-analyzer-config"; +import { LIT_HTML_BOOLEAN_ATTRIBUTE_MODIFIER } from "../analyze/constants"; import { HtmlNodeAttrAssignmentKind } from "../analyze/types/html-node/html-node-attr-assignment-types"; import { HtmlNodeAttrKind } from "../analyze/types/html-node/html-node-attr-types"; -import { LitHtmlDiagnosticKind } from "../analyze/types/lit-diagnostic"; -import { RuleModule } from "../analyze/types/rule-module"; -import { rangeFromHtmlNodeAttr } from "../analyze/util/lit-range-util"; -import { extractBindingTypes } from "../analyze/util/type/extract-binding-types"; -import { isAssignableToTypeWithStringCoercion } from "../analyze/util/type/is-assignable-in-attribute-binding"; -import { isAssignableToType } from "../analyze/util/type/is-assignable-to-type"; +import { RuleModule } from "../analyze/types/rule/rule-module"; +import { rangeFromHtmlNodeAttr } from "../analyze/util/range-util"; +import { extractBindingTypes } from "./util/type/extract-binding-types"; +import { isAssignableToTypeWithStringCoercion } from "./util/type/is-assignable-in-attribute-binding"; +import { isAssignableToType } from "./util/type/is-assignable-to-type"; /** * This rule validates that you are not binding a boolean type in an attribute binding * This would result in binding the string 'true' or 'false' and a '?' binding should be used instead. */ const rule: RuleModule = { - name: "no-boolean-in-attribute-binding", - visitHtmlAssignment(assignment, request) { + id: "no-boolean-in-attribute-binding", + meta: { + priority: "medium" + }, + visitHtmlAssignment(assignment, context) { // Don't validate boolean attribute bindings. if (assignment.kind === HtmlNodeAttrAssignmentKind.BOOLEAN) return; @@ -23,70 +25,76 @@ const rule: RuleModule = { const { htmlAttr } = assignment; if (htmlAttr.kind !== HtmlNodeAttrKind.ATTRIBUTE) return; - const { typeA, typeB } = extractBindingTypes(assignment, request); + const { typeA, typeB } = extractBindingTypes(assignment, context); // Return early if the attribute is like 'required=""' because this is assignable to boolean. - if (typeB.kind === SimpleTypeKind.STRING_LITERAL && typeB.value.length === 0) { - return; - } + if (typeB.kind === SimpleTypeKind.STRING_LITERAL && typeB.value.length === 0) return; // Check that typeB is not of any|unknown type and typeB is assignable to boolean. // Report a diagnostic if typeB is assignable to boolean type because this would result in binding the boolean coerced to string. if ( !isAssignableToSimpleTypeKind(typeB, [SimpleTypeKind.ANY, SimpleTypeKind.UNKNOWN], { op: "or" }) && - isAssignableToType({ typeA: { kind: SimpleTypeKind.BOOLEAN }, typeB }, request) + isAssignableToType({ typeA: { kind: SimpleTypeKind.BOOLEAN }, typeB }, context) ) { // Don't emit error if typeB is assignable to typeA with string coercion. - if (isAssignableToType({ typeA, typeB }, request, { isAssignable: isAssignableToTypeWithStringCoercion })) { + if (isAssignableToType({ typeA, typeB }, context, { isAssignable: isAssignableToTypeWithStringCoercion })) { return; } - return [ - { - kind: LitHtmlDiagnosticKind.EXPRESSION_ONLY_ASSIGNABLE_WITH_BOOLEAN_BINDING, - severity: litDiagnosticRuleSeverity(request.config, "no-boolean-in-attribute-binding"), - source: "no-boolean-in-attribute-binding", - message: `The value being assigned is a boolean type, but you are not using a boolean binding.`, - fix: "Change to boolean binding?", - location: rangeFromHtmlNodeAttr(request.document, htmlAttr), - file: request.file, - htmlAttr, - typeA, - typeB + context.report({ + location: rangeFromHtmlNodeAttr(htmlAttr), + message: `The value being assigned is a boolean type, but you are not using a boolean binding.`, + fixMessage: "Change to boolean binding?", + fix: () => { + const newName = `${LIT_HTML_BOOLEAN_ATTRIBUTE_MODIFIER}${htmlAttr.name}`; + + return { + message: `Change to '${newName}'`, + actions: [ + { + kind: "changeAttributeName", + htmlAttr, + newName + } + ] + }; } - ]; + }); } // Check that typeA is not of any|unknown type and typeA is assignable to boolean. // Report a diagnostic if typeA is assignable to boolean type because then // we should probably be using a boolean binding instead of an attribute binding. - if ( + else if ( !isAssignableToSimpleTypeKind(typeA, [SimpleTypeKind.ANY, SimpleTypeKind.UNKNOWN], { op: "or" }) && isAssignableToType( { typeA: { kind: SimpleTypeKind.BOOLEAN }, typeB: typeA }, - request + context ) ) { - return [ - { - kind: LitHtmlDiagnosticKind.EXPRESSION_ONLY_ASSIGNABLE_WITH_BOOLEAN_BINDING, - severity: litDiagnosticRuleSeverity(request.config, "no-boolean-in-attribute-binding"), - source: "no-boolean-in-attribute-binding", - message: `The '${htmlAttr.name}' attribute is of boolean type but you are not using a boolean binding.`, - fix: "Change to boolean binding?", - location: rangeFromHtmlNodeAttr(request.document, htmlAttr), - file: request.file, - htmlAttr, - typeA, - typeB + context.report({ + location: rangeFromHtmlNodeAttr(htmlAttr), + message: `The '${htmlAttr.name}' attribute is of boolean type but you are not using a boolean binding.`, + fixMessage: "Change to boolean binding?", + fix: () => { + const newName = `${LIT_HTML_BOOLEAN_ATTRIBUTE_MODIFIER}${htmlAttr.name}`; + + return { + message: `Change to '${newName}'`, + actions: [ + { + kind: "changeAttributeName", + htmlAttr, + newName + } + ] + }; } - ]; + }); } - - return; } }; diff --git a/packages/lit-analyzer/src/rules/no-complex-attribute-binding.ts b/packages/lit-analyzer/src/rules/no-complex-attribute-binding.ts index cb97d562..88b1c183 100644 --- a/packages/lit-analyzer/src/rules/no-complex-attribute-binding.ts +++ b/packages/lit-analyzer/src/rules/no-complex-attribute-binding.ts @@ -1,73 +1,78 @@ import { isAssignableToPrimitiveType, toTypeString } from "ts-simple-type"; -import { litDiagnosticRuleSeverity } from "../analyze/lit-analyzer-config"; import { HtmlNodeAttrKind } from "../analyze/types/html-node/html-node-attr-types"; -import { LitHtmlDiagnosticKind } from "../analyze/types/lit-diagnostic"; -import { RuleModule } from "../analyze/types/rule-module"; -import { isLitDirective } from "../analyze/util/directive/is-lit-directive"; -import { rangeFromHtmlNodeAttr } from "../analyze/util/lit-range-util"; -import { extractBindingTypes } from "../analyze/util/type/extract-binding-types"; -import { isAssignableBindingUnderSecuritySystem } from "../analyze/util/type/is-assignable-binding-under-security-system"; +import { RuleModule } from "../analyze/types/rule/rule-module"; +import { rangeFromHtmlNodeAttr } from "../analyze/util/range-util"; +import { isLitDirective } from "./util/directive/is-lit-directive"; +import { extractBindingTypes } from "./util/type/extract-binding-types"; +import { isAssignableBindingUnderSecuritySystem } from "./util/type/is-assignable-binding-under-security-system"; /** * This rule validates that complex types are not used within an expression in an attribute binding. */ const rule: RuleModule = { - name: "no-complex-attribute-binding", - visitHtmlAssignment(assignment, request) { + id: "no-complex-attribute-binding", + meta: { + priority: "medium" + }, + visitHtmlAssignment(assignment, context) { // Only validate attribute bindings, because you are able to assign complex types in property bindings. const { htmlAttr } = assignment; if (htmlAttr.kind !== HtmlNodeAttrKind.ATTRIBUTE) return; - const { typeA, typeB } = extractBindingTypes(assignment, request); + const { typeA, typeB } = extractBindingTypes(assignment, context); // Don't validate directives in this rule, because they are assignable even though they are complex types (functions). if (isLitDirective(typeB)) return; // Only primitive types should be allowed as "typeB" if (!isAssignableToPrimitiveType(typeB)) { - if (isAssignableBindingUnderSecuritySystem(htmlAttr, { typeA, typeB }, request, this.name) !== undefined) { + if (isAssignableBindingUnderSecuritySystem(htmlAttr, { typeA, typeB }, context) !== undefined) { // This is binding via a security sanitization system, let it do // this check. Apparently complex values are OK to assign here. - return undefined; + return; } - return [ - { - kind: LitHtmlDiagnosticKind.COMPLEX_NOT_BINDABLE_IN_ATTRIBUTE_BINDING, - severity: litDiagnosticRuleSeverity(request.config, "no-complex-attribute-binding"), - source: "no-complex-attribute-binding", - message: `You are binding a non-primitive type '${toTypeString(typeB)}'. This could result in binding the string "[object Object]".`, - fix: "Use '.' binding instead?", - location: rangeFromHtmlNodeAttr(request.document, htmlAttr), - file: request.file, - htmlAttr, - typeA, - typeB - } - ]; + const message = `You are binding a non-primitive type '${toTypeString(typeB)}'. This could result in binding the string "[object Object]".`; + const newModifier = "."; + + context.report({ + location: rangeFromHtmlNodeAttr(htmlAttr), + message, + fixMessage: `Use '${newModifier}' binding instead?`, + fix: () => ({ + message: `Use '${newModifier}' modifier instead`, + actions: [ + { + kind: "changeAttributeModifier", + htmlAttr, + newModifier + } + ] + }) + }); } // Only primitive types should be allowed as "typeA" - if (!isAssignableToPrimitiveType(typeA)) { + else if (!isAssignableToPrimitiveType(typeA)) { const message = `You are assigning the primitive '${toTypeString(typeB)}' to a non-primitive type '${toTypeString(typeA)}'.`; + const newModifier = "."; - return [ - { - kind: LitHtmlDiagnosticKind.PRIMITIVE_NOT_ASSIGNABLE_TO_COMPLEX, - severity: litDiagnosticRuleSeverity(request.config, "no-complex-attribute-binding"), - source: "no-complex-attribute-binding", - message, - fix: "Use '.' binding instead?", - location: rangeFromHtmlNodeAttr(request.document, htmlAttr), - file: request.file, - htmlAttr, - typeA, - typeB - } - ]; + context.report({ + location: rangeFromHtmlNodeAttr(htmlAttr), + message, + fixMessage: `Use '${newModifier}' binding instead?`, + fix: () => ({ + message: `Use '${newModifier}' modifier instead`, + actions: [ + { + kind: "changeAttributeModifier", + htmlAttr, + newModifier + } + ] + }) + }); } - - return; } }; diff --git a/packages/lit-analyzer/src/rules/no-expressionless-property-binding.ts b/packages/lit-analyzer/src/rules/no-expressionless-property-binding.ts index 0105057f..3d083997 100644 --- a/packages/lit-analyzer/src/rules/no-expressionless-property-binding.ts +++ b/packages/lit-analyzer/src/rules/no-expressionless-property-binding.ts @@ -1,16 +1,18 @@ -import { litDiagnosticRuleSeverity } from "../analyze/lit-analyzer-config"; import { HtmlNodeAttrAssignmentKind } from "../analyze/types/html-node/html-node-attr-assignment-types"; import { HtmlNodeAttrKind } from "../analyze/types/html-node/html-node-attr-types"; -import { LitHtmlDiagnosticKind } from "../analyze/types/lit-diagnostic"; -import { RuleModule } from "../analyze/types/rule-module"; -import { rangeFromHtmlNodeAttr } from "../analyze/util/lit-range-util"; +import { RuleModule } from "../analyze/types/rule/rule-module"; +import { rangeFromHtmlNodeAttr } from "../analyze/util/range-util"; /** * This rule validates that non-attribute bindings are always used with an expression. */ const rule: RuleModule = { - name: "no-expressionless-property-binding", - visitHtmlAssignment(assignment, request) { + id: "no-expressionless-property-binding", + meta: { + priority: "high" + }, + + visitHtmlAssignment(assignment, context) { const { htmlAttr } = assignment; // Only make this check non-expression type assignments. @@ -19,42 +21,25 @@ const rule: RuleModule = { case HtmlNodeAttrAssignmentKind.BOOLEAN: switch (htmlAttr.kind) { case HtmlNodeAttrKind.EVENT_LISTENER: - return [ - { - kind: LitHtmlDiagnosticKind.PROPERTY_NEEDS_EXPRESSION, - message: `You are using an event listener binding without an expression`, - severity: litDiagnosticRuleSeverity(request.config, "no-expressionless-property-binding"), - source: "no-expressionless-property-binding", - location: rangeFromHtmlNodeAttr(request.document, htmlAttr), - file: request.file - } - ]; + context.report({ + location: rangeFromHtmlNodeAttr(htmlAttr), + message: `You are using an event listener binding without an expression` + }); + break; case HtmlNodeAttrKind.BOOLEAN_ATTRIBUTE: - return [ - { - kind: LitHtmlDiagnosticKind.PROPERTY_NEEDS_EXPRESSION, - message: `You are using a boolean attribute binding without an expression`, - severity: litDiagnosticRuleSeverity(request.config, "no-expressionless-property-binding"), - source: "no-expressionless-property-binding", - location: rangeFromHtmlNodeAttr(request.document, htmlAttr), - file: request.file - } - ]; + context.report({ + location: rangeFromHtmlNodeAttr(htmlAttr), + message: `You are using a boolean attribute binding without an expression` + }); + break; case HtmlNodeAttrKind.PROPERTY: - return [ - { - kind: LitHtmlDiagnosticKind.PROPERTY_NEEDS_EXPRESSION, - message: `You are using a property binding without an expression`, - severity: litDiagnosticRuleSeverity(request.config, "no-expressionless-property-binding"), - source: "no-expressionless-property-binding", - location: rangeFromHtmlNodeAttr(request.document, htmlAttr), - file: request.file - } - ]; + context.report({ + location: rangeFromHtmlNodeAttr(htmlAttr), + message: `You are using a property binding without an expression` + }); + break; } } - - return; } }; diff --git a/packages/lit-analyzer/src/rules/no-incompatible-property-type.ts b/packages/lit-analyzer/src/rules/no-incompatible-property-type.ts index 450939bc..75a8c472 100644 --- a/packages/lit-analyzer/src/rules/no-incompatible-property-type.ts +++ b/packages/lit-analyzer/src/rules/no-incompatible-property-type.ts @@ -1,30 +1,31 @@ -import { isAssignableToSimpleTypeKind, SimpleType, SimpleTypeKind, toSimpleType, toTypeString } from "ts-simple-type"; +import { isAssignableToSimpleTypeKind, isSimpleType, SimpleType, SimpleTypeKind, toSimpleType, toTypeString } from "ts-simple-type"; import { Node } from "typescript"; -import { ComponentMember } from "web-component-analyzer"; import { LitElementPropertyConfig } from "web-component-analyzer/lib/cjs/lit-element-property-config-a6e5ad36"; -import { litDiagnosticRuleSeverity } from "../analyze/lit-analyzer-config"; -import { LitAnalyzerRequest } from "../analyze/lit-analyzer-context"; -import { LitHtmlDiagnostic, LitHtmlDiagnosticKind } from "../analyze/types/lit-diagnostic"; -import { RuleModule } from "../analyze/types/rule-module"; +import { RuleModule } from "../analyze/types/rule/rule-module"; +import { RuleModuleContext } from "../analyze/types/rule/rule-module-context"; import { joinArray } from "../analyze/util/array-util"; import { lazy } from "../analyze/util/general-util"; -import { rangeFromNode } from "../analyze/util/lit-range-util"; +import { rangeFromNode } from "../analyze/util/range-util"; const rule: RuleModule = { - name: "no-incompatible-property-type", + id: "no-incompatible-property-type", + meta: { + priority: "medium" + }, + visitComponentMember(member, context) { + if (member.kind !== "property" || member.meta == null) return; - visitComponentMember(member: ComponentMember, request: LitAnalyzerRequest): LitHtmlDiagnostic[] | void { - if (member.meta == null) return; + // Grab the type and fallback to "any" + const type = member.type?.() || { kind: SimpleTypeKind.ANY }; - const checker = request.program.getTypeChecker(); return validateLitPropertyConfig( member.meta.node?.type || member.meta.node?.decorator?.expression || member.node, member.meta, { - propName: member.propName || "", - simplePropType: toSimpleType(member.node, checker) + propName: member.propName, + simplePropType: isSimpleType(type) ? type : toSimpleType(type, context.program.getTypeChecker()) }, - request + context ); } }; @@ -108,26 +109,20 @@ function prepareSimpleAssignabilityTester( * @param litConfig * @param propName * @param simplePropType - * @param request + * @param context */ function validateLitPropertyConfig( node: Node, litConfig: LitElementPropertyConfig, { propName, simplePropType }: { propName: string; simplePropType: SimpleType }, - request: LitAnalyzerRequest -): LitHtmlDiagnostic[] | void { + context: RuleModuleContext +) { // Check if "type" is one of the built in default type converter hint if (typeof litConfig.type === "string" && !litConfig.hasConverter) { - return [ - { - kind: LitHtmlDiagnosticKind.INVALID_PROPERTY_TYPE, - source: "no-incompatible-property-type", - severity: litDiagnosticRuleSeverity(request.config, "no-incompatible-property-type"), - message: `'${litConfig.type}' is not a valid type for the default converter. Have you considered {attribute: false} instead?`, - file: request.file, - location: rangeFromNode(node) - } - ]; + context.report({ + location: rangeFromNode(node), + message: `'${litConfig.type}' is not a valid type for the default converter. Have you considered {attribute: false} instead?` + }); } // Don't continue if we don't know the property type (eg if we are in a js file) @@ -150,31 +145,19 @@ function validateLitPropertyConfig( "or" ); - return [ - { - kind: LitHtmlDiagnosticKind.INVALID_PROPERTY_TYPE, - source: "no-incompatible-property-type", - severity: litDiagnosticRuleSeverity(request.config, "no-incompatible-property-type"), - message: `@property type should be ${potentialKindText} instead of '${toLitPropertyTypeString(litConfig.type.kind)}'`, - file: request.file, - location: rangeFromNode(node) - } - ]; + context.report({ + location: rangeFromNode(node), + message: `@property type should be ${potentialKindText} instead of '${toLitPropertyTypeString(litConfig.type.kind)}'` + }); } // If no suggesting can be provided, report that they are not assignable // The OBJECT @property type is an escape from this error else if (litConfig.type.kind !== SimpleTypeKind.OBJECT) { - return [ - { - kind: LitHtmlDiagnosticKind.INVALID_PROPERTY_TYPE, - source: "no-incompatible-property-type", - severity: litDiagnosticRuleSeverity(request.config, "no-incompatible-property-type"), - message: `@property type '${toTypeString(litConfig.type)}' is not assignable to the actual type '${toTypeString(simplePropType)}'`, - file: request.file, - location: rangeFromNode(node) - } - ]; + context.report({ + location: rangeFromNode(node), + message: `@property type '${toTypeString(litConfig.type)}' is not assignable to the actual type '${toTypeString(simplePropType)}'` + }); } } } @@ -203,29 +186,17 @@ function validateLitPropertyConfig( "or" ); - return [ - { - kind: LitHtmlDiagnosticKind.INVALID_PROPERTY_TYPE, - source: "no-incompatible-property-type", - severity: litDiagnosticRuleSeverity(request.config, "no-incompatible-property-type"), - message: `Missing ${acceptedTypeText} on @property decorator for '${propName}'`, - file: request.file, - location: rangeFromNode(node) - } - ]; + context.report({ + location: rangeFromNode(node), + message: `Missing ${acceptedTypeText} on @property decorator for '${propName}'` + }); } else { - return [ - { - kind: LitHtmlDiagnosticKind.INVALID_PROPERTY_TYPE, - source: "no-incompatible-property-type", - severity: litDiagnosticRuleSeverity(request.config, "no-incompatible-property-type"), - message: `The built in converter doesn't handle the property type '${toTypeString( - simplePropType - )}'. Please add '{attribute: false}' on @property decorator for '${propName}'`, - file: request.file, - location: rangeFromNode(node) - } - ]; + context.report({ + location: rangeFromNode(node), + message: `The built in converter doesn't handle the property type '${toTypeString( + simplePropType + )}'. Please add '{attribute: false}' on @property decorator for '${propName}'` + }); } } diff --git a/packages/lit-analyzer/src/rules/no-incompatible-type-binding.ts b/packages/lit-analyzer/src/rules/no-incompatible-type-binding.ts index 01001950..ebaff2ec 100644 --- a/packages/lit-analyzer/src/rules/no-incompatible-type-binding.ts +++ b/packages/lit-analyzer/src/rules/no-incompatible-type-binding.ts @@ -3,39 +3,43 @@ import { LIT_HTML_EVENT_LISTENER_ATTRIBUTE_MODIFIER, LIT_HTML_PROP_ATTRIBUTE_MODIFIER } from "../analyze/constants"; -import { RuleModule } from "../analyze/types/rule-module"; -import { extractBindingTypes } from "../analyze/util/type/extract-binding-types"; -import { isAssignableInAttributeBinding } from "../analyze/util/type/is-assignable-in-attribute-binding"; -import { isAssignableInBooleanBinding } from "../analyze/util/type/is-assignable-in-boolean-binding"; -import { isAssignableInPropertyBinding } from "../analyze/util/type/is-assignable-in-property-binding"; +import { RuleModule } from "../analyze/types/rule/rule-module"; +import { extractBindingTypes } from "./util/type/extract-binding-types"; +import { isAssignableInAttributeBinding } from "./util/type/is-assignable-in-attribute-binding"; +import { isAssignableInBooleanBinding } from "./util/type/is-assignable-in-boolean-binding"; +import { isAssignableInPropertyBinding } from "./util/type/is-assignable-in-property-binding"; /** * This rule validate if the types of a binding are assignable. */ const rule: RuleModule = { - name: "no-incompatible-type-binding", - visitHtmlAssignment(assignment, request) { + id: "no-incompatible-type-binding", + meta: { + priority: "low" + }, + visitHtmlAssignment(assignment, context) { const { htmlAttr } = assignment; - const { typeA, typeB } = extractBindingTypes(assignment, request); + const { typeA, typeB } = extractBindingTypes(assignment, context); // Validate types based on the binding in which they appear switch (htmlAttr.modifier) { case LIT_HTML_BOOLEAN_ATTRIBUTE_MODIFIER: - return isAssignableInBooleanBinding(htmlAttr, { typeA, typeB }, request); + isAssignableInBooleanBinding(htmlAttr, { typeA, typeB }, context); + break; case LIT_HTML_PROP_ATTRIBUTE_MODIFIER: - return isAssignableInPropertyBinding(htmlAttr, { typeA, typeB }, request); + isAssignableInPropertyBinding(htmlAttr, { typeA, typeB }, context); + break; case LIT_HTML_EVENT_LISTENER_ATTRIBUTE_MODIFIER: break; default: { - return isAssignableInAttributeBinding(htmlAttr, { typeA, typeB }, request); + isAssignableInAttributeBinding(htmlAttr, { typeA, typeB }, context); + break; } } - - return; } }; diff --git a/packages/lit-analyzer/src/rules/no-invalid-attribute-name.ts b/packages/lit-analyzer/src/rules/no-invalid-attribute-name.ts index f75c09e9..b81dc76c 100644 --- a/packages/lit-analyzer/src/rules/no-invalid-attribute-name.ts +++ b/packages/lit-analyzer/src/rules/no-invalid-attribute-name.ts @@ -1,16 +1,14 @@ import { Node } from "typescript"; -import { ComponentMember } from "web-component-analyzer"; -import { litDiagnosticRuleSeverity } from "../analyze/lit-analyzer-config"; -import { LitAnalyzerRequest } from "../analyze/lit-analyzer-context"; -import { LitHtmlDiagnostic, LitHtmlDiagnosticKind } from "../analyze/types/lit-diagnostic"; -import { RuleModule } from "../analyze/types/rule-module"; +import { RuleModule } from "../analyze/types/rule/rule-module"; import { isValidAttributeName } from "../analyze/util/is-valid-name"; -import { rangeFromNode } from "../analyze/util/lit-range-util"; +import { rangeFromNode } from "../analyze/util/range-util"; const rule: RuleModule = { - name: "no-invalid-attribute-name", - - visitComponentMember(member: ComponentMember, request: LitAnalyzerRequest): LitHtmlDiagnostic[] | void { + id: "no-invalid-attribute-name", + meta: { + priority: "low" + }, + visitComponentMember(member, context) { // Check if the tag name is invalid let attrName: undefined | string; let attrNameNode: undefined | Node; @@ -24,16 +22,10 @@ const rule: RuleModule = { } if (attrName != null && attrNameNode != null && !isValidAttributeName(attrName)) { - return [ - { - kind: LitHtmlDiagnosticKind.INVALID_ATTRIBUTE_NAME, - source: "no-invalid-attribute-name", - severity: litDiagnosticRuleSeverity(request.config, "no-invalid-attribute-name"), - message: `'${attrName}' is not a valid attribute name.`, - file: request.file, - location: rangeFromNode(attrNameNode) - } - ]; + context.report({ + location: rangeFromNode(attrNameNode), + message: `'${attrName}' is not a valid attribute name.` + }); } } }; diff --git a/packages/lit-analyzer/src/rules/no-invalid-directive-binding.ts b/packages/lit-analyzer/src/rules/no-invalid-directive-binding.ts index 3d59880e..636ccdce 100644 --- a/packages/lit-analyzer/src/rules/no-invalid-directive-binding.ts +++ b/packages/lit-analyzer/src/rules/no-invalid-directive-binding.ts @@ -1,28 +1,28 @@ -import { litDiagnosticRuleSeverity } from "../analyze/lit-analyzer-config"; import { HtmlNodeAttrAssignmentKind } from "../analyze/types/html-node/html-node-attr-assignment-types"; import { HtmlNodeAttrKind } from "../analyze/types/html-node/html-node-attr-types"; -import { LitHtmlDiagnosticKind } from "../analyze/types/lit-diagnostic"; -import { RuleModule } from "../analyze/types/rule-module"; -import { getDirective } from "../analyze/util/directive/get-directive"; -import { rangeFromHtmlNodeAttr } from "../analyze/util/lit-range-util"; +import { RuleModule } from "../analyze/types/rule/rule-module"; +import { getDirective } from "./util/directive/get-directive"; +import { rangeFromHtmlNodeAttr } from "../analyze/util/range-util"; /** * This rule validates that directives are used properly. */ const rule: RuleModule = { - name: "no-invalid-directive-binding", - visitHtmlAssignment(assignment, request) { + id: "no-invalid-directive-binding", + meta: { + priority: "high" + }, + visitHtmlAssignment(assignment, context) { const { htmlAttr } = assignment; // Only validate expression because this is where directives can be used. if (assignment.kind !== HtmlNodeAttrAssignmentKind.EXPRESSION) return; // Check if the expression is a directive - const directive = getDirective(assignment, request); + const directive = getDirective(assignment, context); if (directive == null) return; // Validate built-in directive kind - const { document } = request; if (typeof directive.kind === "string") { switch (directive.kind) { case "ifDefined": @@ -32,16 +32,10 @@ const rule: RuleModule = { if (directive.args.length === 1) { // "ifDefined" only has an effect on "attribute" bindings if (htmlAttr.kind !== HtmlNodeAttrKind.ATTRIBUTE) { - return [ - { - kind: LitHtmlDiagnosticKind.DIRECTIVE_NOT_ALLOWED_HERE, - source: "no-invalid-directive-binding", - severity: litDiagnosticRuleSeverity(request.config, "no-invalid-directive-binding"), - message: `The 'ifDefined' directive has no effect here.`, - location: rangeFromHtmlNodeAttr(document, htmlAttr), - file: request.file - } - ]; + context.report({ + location: rangeFromHtmlNodeAttr(htmlAttr), + message: `The 'ifDefined' directive has no effect here.` + }); } } @@ -50,32 +44,20 @@ const rule: RuleModule = { case "classMap": // Report error if "classMap" is not being used on the "class" attribute. if (htmlAttr.name !== "class" || htmlAttr.kind !== HtmlNodeAttrKind.ATTRIBUTE) { - return [ - { - kind: LitHtmlDiagnosticKind.DIRECTIVE_NOT_ALLOWED_HERE, - message: `The 'classMap' directive can only be used in an attribute binding for the 'class' attribute`, - source: "no-invalid-directive-binding", - severity: litDiagnosticRuleSeverity(request.config, "no-invalid-directive-binding"), - location: rangeFromHtmlNodeAttr(document, htmlAttr), - file: request.file - } - ]; + context.report({ + location: rangeFromHtmlNodeAttr(htmlAttr), + message: `The 'classMap' directive can only be used in an attribute binding for the 'class' attribute` + }); } break; case "styleMap": // Report error if "styleMap" is not being used on the "style" attribute. if (htmlAttr.name !== "style" || htmlAttr.kind !== HtmlNodeAttrKind.ATTRIBUTE) { - return [ - { - kind: LitHtmlDiagnosticKind.DIRECTIVE_NOT_ALLOWED_HERE, - message: `The 'styleMap' directive can only be used in an attribute binding for the 'style' attribute`, - source: "no-invalid-directive-binding", - severity: litDiagnosticRuleSeverity(request.config, "no-invalid-directive-binding"), - location: rangeFromHtmlNodeAttr(document, htmlAttr), - file: request.file - } - ]; + context.report({ + location: rangeFromHtmlNodeAttr(htmlAttr), + message: `The 'styleMap' directive can only be used in an attribute binding for the 'style' attribute` + }); } break; @@ -86,24 +68,16 @@ const rule: RuleModule = { case "asyncAppend": // These directives can only be used within a text binding. // This function validating assignments is per definition used NOT in a text binding - return [ - { - kind: LitHtmlDiagnosticKind.DIRECTIVE_NOT_ALLOWED_HERE, - message: `The '${directive.kind}' directive can only be used within a text binding.`, - source: "no-invalid-directive-binding", - severity: litDiagnosticRuleSeverity(request.config, "no-invalid-directive-binding"), - location: rangeFromHtmlNodeAttr(document, htmlAttr), - file: request.file - } - ]; + context.report({ + location: rangeFromHtmlNodeAttr(htmlAttr), + message: `The '${directive.kind}' directive can only be used within a text binding.` + }); } } else { // Now we have an unknown (user defined) directive. - // Return empty array and opt out of any more type checking for this directive - return []; + // This needs no further type checking, so break the chain + context.break(); } - - return; } }; diff --git a/packages/lit-analyzer/src/rules/no-invalid-property.ts b/packages/lit-analyzer/src/rules/no-invalid-property.ts deleted file mode 100644 index a5ddebd8..00000000 --- a/packages/lit-analyzer/src/rules/no-invalid-property.ts +++ /dev/null @@ -1,235 +0,0 @@ -/*import { isAssignableToSimpleTypeKind, SimpleType, SimpleTypeKind, toSimpleType, toTypeString } from "ts-simple-type"; -import { Node } from "typescript"; -import { ComponentMember } from "web-component-analyzer"; -import { LitElementPropertyConfig } from "web-component-analyzer/lib/cjs/lit-element-property-config-a6e5ad36"; -import { LitAnalyzerRequest } from "../analyze/lit-analyzer-context"; -import { LitHtmlDiagnostic } from "../analyze/types/lit-diagnostic"; -import { RuleModule } from "../analyze/types/rule-module"; -import { lazy } from "../analyze/util/general-util"; - -const rule: RuleModule = { - name: "no-invalid-property", - - visitComponentMember(member: ComponentMember, request: LitAnalyzerRequest): LitHtmlDiagnostic[] | void { - if (member.meta == null) return; - - const checker = request.program.getTypeChecker(); - validateLitPropertyConfig( - member.node, - member.meta, - { - propName: member.propName || "", - simplePropType: toSimpleType(member.node, checker) - }, - request - ); - - return []; - } -};*/ - -/** - * Returns a string, that can be used in a lit @property decorator for the type key, representing the simple type kind. - * @param simpleTypeKind - */ -/*function toLitPropertyTypeString(simpleTypeKind: SimpleTypeKind): string { - switch (simpleTypeKind) { - case SimpleTypeKind.STRING: - return "String"; - case SimpleTypeKind.NUMBER: - return "Number"; - case SimpleTypeKind.BOOLEAN: - return "Boolean"; - case SimpleTypeKind.ARRAY: - return "Array"; - case SimpleTypeKind.OBJECT: - return "Object"; - default: - return ""; - } -}*/ - -/** - * Runs through a lit configuration and validates against the "simplePropType". - * Emits diagnostics through the context. - * @param node - * @param litConfig - * @param propName - * @param simplePropType - * @param request - */ -/*function validateLitPropertyConfig( - node: Node, - litConfig: LitElementPropertyConfig, - { propName, simplePropType }: { propName: string; simplePropType: SimpleType }, - request: LitAnalyzerRequest -) { - // Check if "type" is one of the built in default type converter hint - if (typeof litConfig.type === "string" && !litConfig.hasConverter) { - console.log({ message: `'${litConfig.type}' is not a valid type for the default converter. Have you considered {attribute: false} instead?` }); - /*context.emitDiagnostics({ - node: (litConfig.node && litConfig.node.type) || node, - message: `'${litConfig.type}' is not a valid type for the default converter. Have you considered {attribute: false} instead?`, - severity: "warning" - }); - return; - } - - // Don't continue if we don't know the property type (eg if we are in a js file) - // Don't continue if this property has a custom converter (because then we don't know how the value will be converted) - if (simplePropType == null || litConfig.hasConverter || typeof litConfig.type === "string") { - return; - } - - // Test assignments to all possible type kinds - const _isAssignableToCache = new Map(); - function isAssignableTo(simpleTypeKind: SimpleTypeKind): boolean { - if (_isAssignableToCache.has(simpleTypeKind)) { - return _isAssignableToCache.get(simpleTypeKind)!; - } - - if (simplePropType == null) { - return false; - } - - const result = (() => { - switch (simpleTypeKind) { - case SimpleTypeKind.STRING: - return isAssignableToSimpleTypeKind(simplePropType, [SimpleTypeKind.STRING, SimpleTypeKind.STRING_LITERAL], { op: "or" }); - case SimpleTypeKind.NUMBER: - return isAssignableToSimpleTypeKind(simplePropType, [SimpleTypeKind.NUMBER, SimpleTypeKind.NUMBER_LITERAL], { op: "or" }); - case SimpleTypeKind.BOOLEAN: - return isAssignableToSimpleTypeKind(simplePropType, [SimpleTypeKind.BOOLEAN, SimpleTypeKind.BOOLEAN_LITERAL], { op: "or" }); - case SimpleTypeKind.ARRAY: - return isAssignableToSimpleTypeKind(simplePropType, [SimpleTypeKind.ARRAY, SimpleTypeKind.TUPLE], { op: "or" }); - case SimpleTypeKind.OBJECT: - return isAssignableToSimpleTypeKind(simplePropType, [SimpleTypeKind.OBJECT, SimpleTypeKind.INTERFACE], { - op: "or" - }); - case SimpleTypeKind.ANY: - return isAssignableToSimpleTypeKind(simplePropType, SimpleTypeKind.ANY); - default: - return false; - } - })(); - - _isAssignableToCache.set(simpleTypeKind, result); - - return result; - } - /*const isAssignableTo: Partial boolean>> = { - [SimpleTypeKind.STRING]: lazy(() => - isAssignableToSimpleTypeKind(simplePropType, [SimpleTypeKind.STRING, SimpleTypeKind.STRING_LITERAL], { op: "or" }) - ), - [SimpleTypeKind.NUMBER]: lazy(() => - isAssignableToSimpleTypeKind(simplePropType, [SimpleTypeKind.NUMBER, SimpleTypeKind.NUMBER_LITERAL], { op: "or" }) - ), - [SimpleTypeKind.BOOLEAN]: lazy(() => - isAssignableToSimpleTypeKind(simplePropType, [SimpleTypeKind.BOOLEAN, SimpleTypeKind.BOOLEAN_LITERAL], { op: "or" }) - ), - [SimpleTypeKind.ARRAY]: lazy(() => isAssignableToSimpleTypeKind(simplePropType, [SimpleTypeKind.ARRAY, SimpleTypeKind.TUPLE], { op: "or" })), - [SimpleTypeKind.OBJECT]: lazy(() => - isAssignableToSimpleTypeKind(simplePropType, [SimpleTypeKind.OBJECT, SimpleTypeKind.INTERFACE], { - op: "or" - }) - ), - [SimpleTypeKind.ANY]: lazy(() => isAssignableToSimpleTypeKind(simplePropType, SimpleTypeKind.ANY)) - };/ - - // Collect type kinds that can be used in as "type" in the @property decorator - const acceptedTypeKinds = lazy(() => { - return [SimpleTypeKind.STRING, SimpleTypeKind.NUMBER, SimpleTypeKind.BOOLEAN, SimpleTypeKind.ARRAY, SimpleTypeKind.OBJECT, SimpleTypeKind.ANY] - .filter(kind => kind !== SimpleTypeKind.ANY) - .filter(kind => isAssignableTo(kind)); - }); - - // Test the @property type against the actual type if a type has been provided - if (litConfig.type != null) { - // Report error if the @property type is not assignable to the actual type - if (!isAssignableTo(litConfig.type.kind) && !isAssignableTo(SimpleTypeKind.ANY)) { - // Suggest what to use instead - if (acceptedTypeKinds().length >= 1) { - const potentialKindText = joinArray( - acceptedTypeKinds().map(kind => `'${toLitPropertyTypeString(kind)}'`), - ", ", - "or" - ); - - console.log({ message: `@property type should be ${potentialKindText} instead of '${toLitPropertyTypeString(litConfig.type.kind)}'` }); - /*context.emitDiagnostics({ - node: (litConfig.node && litConfig.node.type) || node, - message: `@property type should be ${potentialKindText} instead of '${toLitPropertyTypeString(litConfig.type.kind)}'`, - severity: "warning" - });/ - } - - // If no suggesting can be provided, report that they are not assignable - // The OBJECT @property type is an escape from this error - else if (litConfig.type.kind !== SimpleTypeKind.OBJECT) { - console.log({ - message: `@property type '${toTypeString(litConfig.type)}' is not assignable to the actual type '${toTypeString(simplePropType)}'` - }); - /*context.emitDiagnostics({ - node: (litConfig.node && litConfig.node.type) || node, - message: `@property type '${toTypeString(litConfig.type)}' is not assignable to the actual type '${toTypeString(simplePropType)}'`, - severity: "warning" - });/ - } - } - } - - // If no type has been specified, suggest what to use as the @property type - else if (litConfig.attribute !== false) { - // Don't do anything if there are multiple possibilities for a type. - if (isAssignableTo(SimpleTypeKind.ANY)) { - } - - // Don't report errors because String conversion is default - else if (isAssignableTo(SimpleTypeKind.STRING)) { - } - - // Suggest what to use instead if there are multiple accepted @property types for this property - else if (acceptedTypeKinds().length > 0) { - // Suggest types to use and include "{attribute: false}" if the @property type is ARRAY or OBJECT - const acceptedTypeText = joinArray( - [ - ...acceptedTypeKinds().map(kind => `'{type: ${toLitPropertyTypeString(kind)}}'`), - ...(isAssignableTo(SimpleTypeKind.ARRAY) || isAssignableTo(SimpleTypeKind.OBJECT) ? ["'{attribute: false}'"] : []) - ], - ", ", - "or" - ); - - console.log({ message: `Missing ${acceptedTypeText} on @property decorator for '${propName}'` }); - /*context.emitDiagnostics({ - node, - severity: "warning", - message: `Missing ${acceptedTypeText} on @property decorator for '${propName}'` - });/ - } else { - console.log({ - message: `The built in converter doesn't handle the property type '${toTypeString( - simplePropType - )}'. Please add '{attribute: false}' on @property decorator for '${propName}'` - }); - /*context.emitDiagnostics({ - node, - severity: "warning", - message: `The built in converter doesn't handle the property type '${toTypeString( - simplePropType - )}'. Please add '{attribute: false}' on @property decorator for '${propName}'` - });/ - } - } - - /*if (litConfig.attribute !== false && !isAssignableToPrimitiveType(simplePropType)) { - context.emitDiagnostics({ - node, - severity: "warning", - message: `You need to add '{attribute: false}' to @property decorator for '${propName}' because '${toTypeString(simplePropType)}' type is not a primitive` - }); - }/ -} - -export default rule; -*/ diff --git a/packages/lit-analyzer/src/rules/no-invalid-tag-name.ts b/packages/lit-analyzer/src/rules/no-invalid-tag-name.ts index 1d858323..a0e2343a 100644 --- a/packages/lit-analyzer/src/rules/no-invalid-tag-name.ts +++ b/packages/lit-analyzer/src/rules/no-invalid-tag-name.ts @@ -1,38 +1,28 @@ -import { ComponentDefinition } from "web-component-analyzer"; -import { litDiagnosticRuleSeverity } from "../analyze/lit-analyzer-config"; -import { LitAnalyzerRequest } from "../analyze/lit-analyzer-context"; -import { LitHtmlDiagnostic, LitHtmlDiagnosticKind } from "../analyze/types/lit-diagnostic"; -import { RuleModule } from "../analyze/types/rule-module"; +import { RuleModule } from "../analyze/types/rule/rule-module"; import { isValidCustomElementName } from "../analyze/util/is-valid-name"; import { iterableFirst } from "../analyze/util/iterable-util"; -import { rangeFromNode } from "../analyze/util/lit-range-util"; +import { rangeFromNode } from "../analyze/util/range-util"; const rule: RuleModule = { - name: "no-invalid-tag-name", - - visitComponentDefinition(definition: ComponentDefinition, request: LitAnalyzerRequest): LitHtmlDiagnostic[] | void { + id: "no-invalid-tag-name", + meta: { + priority: "low" + }, + visitComponentDefinition(definition, context) { // Check if the tag name is invalid if (!isValidCustomElementName(definition.tagName)) { const node = iterableFirst(definition.tagNameNodes) || iterableFirst(definition.identifierNodes); // Only report diagnostic if the tag is not built in, // because this function among other things tests for missing "-" in custom element names - const tag = request.htmlStore.getHtmlTag(definition.tagName); + const tag = context.htmlStore.getHtmlTag(definition.tagName); if (node != null && tag != null && !tag.builtIn) { - return [ - { - kind: LitHtmlDiagnosticKind.INVALID_TAG_NAME, - source: "no-invalid-tag-name", - severity: litDiagnosticRuleSeverity(request.config, "no-invalid-tag-name"), - message: `'${definition.tagName}' is not a valid custom element name.`, - file: request.file, - location: rangeFromNode(node) - } - ]; + context.report({ + location: rangeFromNode(node), + message: `'${definition.tagName}' is not a valid custom element name.` + }); } } - - return []; } }; diff --git a/packages/lit-analyzer/src/rules/no-missing-import.ts b/packages/lit-analyzer/src/rules/no-missing-import.ts index 8e0ecd26..7588fb74 100644 --- a/packages/lit-analyzer/src/rules/no-missing-import.ts +++ b/packages/lit-analyzer/src/rules/no-missing-import.ts @@ -1,17 +1,20 @@ import { basename, dirname, relative } from "path"; -import { litDiagnosticRuleSeverity } from "../analyze/lit-analyzer-config"; -import { LitHtmlDiagnostic, LitHtmlDiagnosticKind } from "../analyze/types/lit-diagnostic"; -import { RuleModule } from "../analyze/types/rule-module"; +import { RuleModule } from "../analyze/types/rule/rule-module"; import { isCustomElementTagName } from "../analyze/util/is-valid-name"; import { iterableFirst } from "../analyze/util/iterable-util"; -import { rangeFromHtmlNode } from "../analyze/util/lit-range-util"; +import { rangeFromHtmlNode } from "../analyze/util/range-util"; /** * This rule makes sure that all custom elements used are imported in a given file. */ const rule: RuleModule = { - name: "no-missing-import", - visitHtmlNode(htmlNode, { htmlStore, config, definitionStore, dependencyStore, document, file }) { + id: "no-missing-import", + meta: { + priority: "low" + }, + visitHtmlNode(htmlNode, context) { + const { htmlStore, config, definitionStore, dependencyStore, file } = context; + // Return if the html tag doesn't exists or if the html tag doesn't have a declaration const htmlTag = htmlStore.getHtmlTag(htmlNode); if (htmlTag == null) return; @@ -30,30 +33,26 @@ const rule: RuleModule = { // Report diagnostic if the html tag hasn't been imported. if (!isDefinitionImported) { - // Get the import path and the position where it can be placed - const importPath = getRelativePathForImport(file.fileName, iterableFirst(definition.tagNameNodes)!.getSourceFile().fileName); - - const report: LitHtmlDiagnostic = { - kind: LitHtmlDiagnosticKind.MISSING_IMPORT, + context.report({ + location: rangeFromHtmlNode(htmlNode), message: `Missing import for <${htmlNode.tagName}>`, suggestion: config.dontSuggestConfigChanges ? undefined : `You can disable this check by disabling the 'no-missing-import' rule.`, - source: "no-missing-import", - severity: litDiagnosticRuleSeverity(config, "no-missing-import"), - location: rangeFromHtmlNode(document, htmlNode), - file, - htmlNode, - definition, - importPath - }; + fix: () => { + const importPath = getRelativePathForImport(file.fileName, iterableFirst(definition.tagNameNodes)!.getSourceFile().fileName); - if (config.dontSuggestConfigChanges) { - report.suggestion = undefined; - } - - return [report]; + return { + message: `Import "${iterableFirst(definition.identifierNodes)?.getText() || "component"}" from module "${importPath}"`, + actions: [ + { + kind: "import", + path: importPath, + file: context.file + } + ] + }; + } + }); } - - return; } }; diff --git a/packages/lit-analyzer/src/rules/no-noncallable-event-binding.ts b/packages/lit-analyzer/src/rules/no-noncallable-event-binding.ts index 4199628a..89be5022 100644 --- a/packages/lit-analyzer/src/rules/no-noncallable-event-binding.ts +++ b/packages/lit-analyzer/src/rules/no-noncallable-event-binding.ts @@ -1,40 +1,32 @@ import { isAssignableToSimpleTypeKind, SimpleType, SimpleTypeKind, toTypeString } from "ts-simple-type"; -import { litDiagnosticRuleSeverity } from "../analyze/lit-analyzer-config"; import { HtmlNodeAttrKind } from "../analyze/types/html-node/html-node-attr-types"; -import { LitHtmlDiagnosticKind } from "../analyze/types/lit-diagnostic"; -import { RuleModule } from "../analyze/types/rule-module"; -import { rangeFromHtmlNodeAttr } from "../analyze/util/lit-range-util"; -import { extractBindingTypes } from "../analyze/util/type/extract-binding-types"; +import { RuleModule } from "../analyze/types/rule/rule-module"; +import { rangeFromHtmlNodeAttr } from "../analyze/util/range-util"; +import { extractBindingTypes } from "./util/type/extract-binding-types"; /** * This rule validates that only callable types are used within event binding expressions. * This rule catches typos like: @click="onClick()" */ const rule: RuleModule = { - name: "no-noncallable-event-binding", - visitHtmlAssignment(assignment, request) { + id: "no-noncallable-event-binding", + meta: { + priority: "high" + }, + visitHtmlAssignment(assignment, context) { // Only validate event listener bindings. const { htmlAttr } = assignment; if (htmlAttr.kind !== HtmlNodeAttrKind.EVENT_LISTENER) return; - const { typeB } = extractBindingTypes(assignment, request); + const { typeB } = extractBindingTypes(assignment, context); // Make sure that the expression given to the event listener binding a function or an object with "handleEvent" property. if (!isTypeBindableToEventListener(typeB)) { - return [ - { - kind: LitHtmlDiagnosticKind.NO_EVENT_LISTENER_FUNCTION, - message: `You are setting up an event listener with a non-callable type '${toTypeString(typeB)}'`, - source: "no-noncallable-event-binding", - severity: litDiagnosticRuleSeverity(request.config, "no-noncallable-event-binding"), - location: rangeFromHtmlNodeAttr(request.document, htmlAttr), - file: request.file, - typeB - } - ]; + context.report({ + location: rangeFromHtmlNodeAttr(htmlAttr), + message: `You are setting up an event listener with a non-callable type '${toTypeString(typeB)}'` + }); } - - return; } }; diff --git a/packages/lit-analyzer/src/rules/no-nullable-attribute-binding.ts b/packages/lit-analyzer/src/rules/no-nullable-attribute-binding.ts index 9e8d69d0..2c41f052 100644 --- a/packages/lit-analyzer/src/rules/no-nullable-attribute-binding.ts +++ b/packages/lit-analyzer/src/rules/no-nullable-attribute-binding.ts @@ -1,18 +1,19 @@ import { isAssignableToSimpleTypeKind, SimpleTypeKind, toTypeString } from "ts-simple-type"; -import { litDiagnosticRuleSeverity } from "../analyze/lit-analyzer-config"; import { HtmlNodeAttrAssignmentKind } from "../analyze/types/html-node/html-node-attr-assignment-types"; import { HtmlNodeAttrKind } from "../analyze/types/html-node/html-node-attr-types"; -import { LitHtmlDiagnosticKind } from "../analyze/types/lit-diagnostic"; -import { RuleModule } from "../analyze/types/rule-module"; -import { rangeFromHtmlNodeAttr } from "../analyze/util/lit-range-util"; -import { extractBindingTypes } from "../analyze/util/type/extract-binding-types"; +import { RuleModule } from "../analyze/types/rule/rule-module"; +import { rangeFromHtmlNodeAttr } from "../analyze/util/range-util"; +import { extractBindingTypes } from "./util/type/extract-binding-types"; /** * This rule validates that "null" and "undefined" types are not bound in an attribute binding. */ const rule: RuleModule = { - name: "no-nullable-attribute-binding", - visitHtmlAssignment(assignment, request) { + id: "no-nullable-attribute-binding", + meta: { + priority: "high" + }, + visitHtmlAssignment(assignment, context) { // Only validate "expression" kind bindings. if (assignment.kind !== HtmlNodeAttrAssignmentKind.EXPRESSION) return; @@ -20,45 +21,37 @@ const rule: RuleModule = { const { htmlAttr } = assignment; if (htmlAttr.kind !== HtmlNodeAttrKind.ATTRIBUTE) return; - const { typeA, typeB } = extractBindingTypes(assignment, request); + const { typeB } = extractBindingTypes(assignment, context); // Test if removing "null" from typeB would work and suggest using "ifDefined(exp === null ? undefined : exp)". if (isAssignableToSimpleTypeKind(typeB, SimpleTypeKind.NULL)) { - return [ - { - kind: LitHtmlDiagnosticKind.INVALID_ATTRIBUTE_EXPRESSION_TYPE_NULL, - message: `This attribute binds the type '${toTypeString(typeB)}' which can be 'null'.`, - fix: "Use the 'ifDefined' directive and strict null check?", - source: "no-nullable-attribute-binding", - severity: litDiagnosticRuleSeverity(request.config, "no-nullable-attribute-binding"), - location: rangeFromHtmlNodeAttr(request.document, htmlAttr), - file: request.file, - htmlAttr: htmlAttr as typeof htmlAttr & { assignment: typeof assignment }, - typeA, - typeB + context.report({ + location: rangeFromHtmlNodeAttr(htmlAttr), + message: `This attribute binds the type '${toTypeString(typeB)}' which can be 'null'.`, + fixMessage: "Use the 'ifDefined' directive and strict null check?", + fix: () => { + const newValue = `ifDefined(${assignment.expression.getText()} === null ? undefined : ${assignment.expression.getText()})`; + + return { + message: `Use '${newValue}'`, + actions: [{ kind: "changeAssignment", assignment, newValue }] + }; } - ]; + }); } // Test if removing "undefined" from typeB would work and suggest using "ifDefined". else if (isAssignableToSimpleTypeKind(typeB, SimpleTypeKind.UNDEFINED)) { - return [ - { - kind: LitHtmlDiagnosticKind.INVALID_ATTRIBUTE_EXPRESSION_TYPE_UNDEFINED, - message: `This attribute binds the type '${toTypeString(typeB)}' which can be 'undefined'.`, - fix: "Use the 'ifDefined' directive?", - source: "no-nullable-attribute-binding", - severity: litDiagnosticRuleSeverity(request.config, "no-nullable-attribute-binding"), - location: rangeFromHtmlNodeAttr(request.document, htmlAttr), - file: request.file, - htmlAttr: htmlAttr as typeof htmlAttr & { assignment: typeof assignment }, - typeA, - typeB - } - ]; + context.report({ + location: rangeFromHtmlNodeAttr(htmlAttr), + message: `This attribute binds the type '${toTypeString(typeB)}' which can be 'undefined'.`, + fixMessage: "Use the 'ifDefined' directive?", + fix: () => ({ + message: `Use the 'ifDefined' directive.`, + actions: [{ kind: "changeAssignment", assignment, newValue: `ifDefined(${assignment.expression.getText()})` }] + }) + }); } - - return; } }; export default rule; diff --git a/packages/lit-analyzer/src/rules/no-unclosed-tag.ts b/packages/lit-analyzer/src/rules/no-unclosed-tag.ts index e7f2232b..5c88aa2c 100644 --- a/packages/lit-analyzer/src/rules/no-unclosed-tag.ts +++ b/packages/lit-analyzer/src/rules/no-unclosed-tag.ts @@ -1,32 +1,25 @@ -import { litDiagnosticRuleSeverity } from "../analyze/lit-analyzer-config"; -import { LitHtmlDiagnosticKind } from "../analyze/types/lit-diagnostic"; -import { RuleModule } from "../analyze/types/rule-module"; +import { RuleModule } from "../analyze/types/rule/rule-module"; import { isCustomElementTagName } from "../analyze/util/is-valid-name"; -import { rangeFromHtmlNode } from "../analyze/util/lit-range-util"; +import { rangeFromHtmlNode } from "../analyze/util/range-util"; /** * This rule validates that all tags are closed properly. */ const rule: RuleModule = { - name: "no-unclosed-tag", - visitHtmlNode(htmlNode, request) { + id: "no-unclosed-tag", + meta: { + priority: "low" + }, + visitHtmlNode(htmlNode, context) { if (!htmlNode.selfClosed && htmlNode.location.endTag == null) { // Report specifically that a custom element cannot be self closing // if the user is trying to close a custom element. const isCustomElement = isCustomElementTagName(htmlNode.tagName); - return [ - { - message: `This tag isn't closed.${isCustomElement ? " Custom elements cannot be self closing." : ""}`, - location: rangeFromHtmlNode(request.document, htmlNode), - htmlNode, - - kind: LitHtmlDiagnosticKind.TAG_NOT_CLOSED, - source: "no-unclosed-tag", - severity: litDiagnosticRuleSeverity(request.config, "no-unclosed-tag"), - file: request.file - } - ]; + context.report({ + location: rangeFromHtmlNode(htmlNode), + message: `This tag isn't closed.${isCustomElement ? " Custom elements cannot be self closing." : ""}` + }); } return; diff --git a/packages/lit-analyzer/src/rules/no-unintended-mixed-binding.ts b/packages/lit-analyzer/src/rules/no-unintended-mixed-binding.ts index a51fdd98..960ea0e7 100644 --- a/packages/lit-analyzer/src/rules/no-unintended-mixed-binding.ts +++ b/packages/lit-analyzer/src/rules/no-unintended-mixed-binding.ts @@ -1,9 +1,7 @@ -import { litDiagnosticRuleSeverity } from "../analyze/lit-analyzer-config"; import { HtmlNodeAttrAssignmentKind } from "../analyze/types/html-node/html-node-attr-assignment-types"; import { HtmlNodeAttrKind } from "../analyze/types/html-node/html-node-attr-types"; -import { LitHtmlDiagnosticKind } from "../analyze/types/lit-diagnostic"; -import { RuleModule } from "../analyze/types/rule-module"; -import { rangeFromHtmlNodeAttr } from "../analyze/util/lit-range-util"; +import { RuleModule } from "../analyze/types/rule/rule-module"; +import { rangeFromHtmlNodeAttr } from "../analyze/util/range-util"; const CONTROL_CHARACTERS = ["'", '"', "}", "/"]; @@ -16,9 +14,12 @@ const CONTROL_CHARACTERS = ["'", '"', "}", "/"]; * */ const rule: RuleModule = { - name: "no-unintended-mixed-binding", - visitHtmlAssignment(assignment, request) { - // Check mixed bindings + id: "no-unintended-mixed-binding", + meta: { + priority: "high" + }, + visitHtmlAssignment(assignment, context) { + // Check only mixed bindings if (assignment.kind !== HtmlNodeAttrAssignmentKind.MIXED) { return; } @@ -53,16 +54,10 @@ const rule: RuleModule = { } })(); - return [ - { - kind: LitHtmlDiagnosticKind.INVALID_MIXED_BINDING, - source: "no-unintended-mixed-binding", - severity: litDiagnosticRuleSeverity(request.config, "no-unintended-mixed-binding"), - location: rangeFromHtmlNodeAttr(request.document, assignment.htmlAttr), - file: request.file, - message - } - ]; + context.report({ + location: rangeFromHtmlNodeAttr(assignment.htmlAttr), + message + }); } return; diff --git a/packages/lit-analyzer/src/rules/no-unknown-attribute.ts b/packages/lit-analyzer/src/rules/no-unknown-attribute.ts index 47eeb981..76e69fb9 100644 --- a/packages/lit-analyzer/src/rules/no-unknown-attribute.ts +++ b/packages/lit-analyzer/src/rules/no-unknown-attribute.ts @@ -1,20 +1,25 @@ -import { LitAnalyzerConfig, litDiagnosticRuleSeverity } from "../analyze/lit-analyzer-config"; +import { LitAnalyzerConfig } from "../analyze/lit-analyzer-config"; import { HtmlTag, litAttributeModifierForTarget } from "../analyze/parse/parse-html-data/html-tag"; import { AnalyzerDefinitionStore } from "../analyze/store/analyzer-definition-store"; import { HtmlNodeAttrKind } from "../analyze/types/html-node/html-node-attr-types"; import { HtmlNodeKind } from "../analyze/types/html-node/html-node-types"; -import { LitHtmlDiagnosticKind } from "../analyze/types/lit-diagnostic"; -import { RuleModule } from "../analyze/types/rule-module"; +import { RuleFix } from "../analyze/types/rule/rule-fix"; +import { RuleModule } from "../analyze/types/rule/rule-module"; import { suggestTargetForHtmlAttr } from "../analyze/util/attribute-util"; import { iterableFirst } from "../analyze/util/iterable-util"; -import { rangeFromHtmlNodeAttr } from "../analyze/util/lit-range-util"; +import { rangeFromHtmlNodeAttr } from "../analyze/util/range-util"; /** * This rule validates that only known attributes are used in attribute bindings. */ const rule: RuleModule = { - name: "no-unknown-attribute", - visitHtmlAttribute(htmlAttr, { htmlStore, config, definitionStore, document, file }) { + id: "no-unknown-attribute", + meta: { + priority: "low" + }, + visitHtmlAttribute(htmlAttr, context) { + const { htmlStore, config, definitionStore } = context; + // Ignore "style" and "svg" attrs because I don't yet have all data for them. if (htmlAttr.htmlNode.kind !== HtmlNodeKind.NODE) return; @@ -33,24 +38,49 @@ const rule: RuleModule = { // Get suggested target const suggestedTarget = suggestTargetForHtmlAttr(htmlAttr, htmlStore); - const suggestedMemberName = (suggestedTarget && `${litAttributeModifierForTarget(suggestedTarget)}${suggestedTarget.name}`) || undefined; + const suggestedModifier = suggestedTarget == null ? undefined : litAttributeModifierForTarget(suggestedTarget); + const suggestedMemberName = suggestedTarget == null ? undefined : suggestedTarget.name; const suggestion = getSuggestionText({ config, htmlTag, definitionStore }); - return [ - { - kind: LitHtmlDiagnosticKind.UNKNOWN_TARGET, - message: `Unknown attribute '${htmlAttr.name}'.`, - fix: suggestedMemberName == null ? undefined : `Did you mean '${suggestedMemberName}'?`, - source: "no-unknown-attribute", - severity: litDiagnosticRuleSeverity(config, "no-unknown-attribute"), - location: rangeFromHtmlNodeAttr(document, htmlAttr), - file, - suggestion, - htmlAttr, - suggestedTarget - } - ]; + context.report({ + location: rangeFromHtmlNodeAttr(htmlAttr), + message: `Unknown attribute '${htmlAttr.name}'.`, + fixMessage: suggestedMemberName == null ? undefined : `Did you mean '${suggestedModifier}${suggestedMemberName}'?`, + suggestion, + fix: () => + [ + { + message: `Change attribute to 'data-${htmlAttr.name}'`, + actions: [ + { + kind: "CHANGE_ATTRIBUTE_NAME", + newName: `data-${htmlAttr.name}`, + htmlAttr + } + ] + }, + ...(suggestedMemberName == null + ? [] + : [ + { + message: `Change attribute to '${suggestedModifier}${suggestedMemberName}'`, + actions: [ + { + kind: "CHANGE_ATTRIBUTE_NAME", + newName: suggestedMemberName, + htmlAttr + }, + { + kind: "CHANGE_ATTRIBUTE_MODIFIER", + newModifier: suggestedModifier, + htmlAttr + } + ] + } + ]) + ] as RuleFix[] + }); } return; diff --git a/packages/lit-analyzer/src/rules/no-unknown-event.ts b/packages/lit-analyzer/src/rules/no-unknown-event.ts index 6843c1b8..33b26f0b 100644 --- a/packages/lit-analyzer/src/rules/no-unknown-event.ts +++ b/packages/lit-analyzer/src/rules/no-unknown-event.ts @@ -1,19 +1,23 @@ -import { LitAnalyzerConfig, litDiagnosticRuleSeverity } from "../analyze/lit-analyzer-config"; +import { LitAnalyzerConfig } from "../analyze/lit-analyzer-config"; import { HtmlTag, litAttributeModifierForTarget } from "../analyze/parse/parse-html-data/html-tag"; import { AnalyzerDefinitionStore } from "../analyze/store/analyzer-definition-store"; import { HtmlNodeAttrKind } from "../analyze/types/html-node/html-node-attr-types"; import { HtmlNodeKind } from "../analyze/types/html-node/html-node-types"; -import { LitHtmlDiagnosticKind } from "../analyze/types/lit-diagnostic"; -import { RuleModule } from "../analyze/types/rule-module"; +import { RuleModule } from "../analyze/types/rule/rule-module"; import { suggestTargetForHtmlAttr } from "../analyze/util/attribute-util"; -import { rangeFromHtmlNodeAttr } from "../analyze/util/lit-range-util"; +import { rangeFromHtmlNodeAttr } from "../analyze/util/range-util"; /** * This rule validates that only known events are used in event listener bindings. */ const rule: RuleModule = { - name: "no-unknown-event", - visitHtmlAttribute(htmlAttr, { htmlStore, config, definitionStore, file, document }) { + id: "no-unknown-event", + meta: { + priority: "low" + }, + visitHtmlAttribute(htmlAttr, context) { + const { htmlStore, config, definitionStore } = context; + // Ignore "style" and "svg" attrs because I don't yet have all data for them. if (htmlAttr.htmlNode.kind !== HtmlNodeKind.NODE) return; @@ -33,23 +37,26 @@ const rule: RuleModule = { const suggestion = getSuggestionText({ config, definitionStore, htmlTag }); - return [ - { - kind: LitHtmlDiagnosticKind.UNKNOWN_TARGET, - message: `Unknown event '${htmlAttr.name}'.`, - fix: suggestedMemberName == null ? undefined : `Did you mean '${suggestedMemberName}'?`, - source: "no-unknown-event", - severity: litDiagnosticRuleSeverity(config, "no-unknown-event"), - location: rangeFromHtmlNodeAttr(document, htmlAttr), - file, - suggestion, - htmlAttr, - suggestedTarget - } - ]; + context.report({ + location: rangeFromHtmlNodeAttr(htmlAttr), + message: `Unknown event '${htmlAttr.name}'.`, + fixMessage: suggestedMemberName == null ? undefined : `Did you mean '${suggestedMemberName}'?`, + suggestion, + fix: + suggestedMemberName == null + ? undefined + : () => ({ + message: `Change event to '${suggestedMemberName}'`, + actions: [ + { + kind: "changeAttributeName", + newName: suggestedMemberName, + htmlAttr + } + ] + }) + }); } - - return; } }; diff --git a/packages/lit-analyzer/src/rules/no-unknown-property.ts b/packages/lit-analyzer/src/rules/no-unknown-property.ts index b23861e5..a25f3791 100644 --- a/packages/lit-analyzer/src/rules/no-unknown-property.ts +++ b/packages/lit-analyzer/src/rules/no-unknown-property.ts @@ -1,20 +1,25 @@ -import { LitAnalyzerConfig, litDiagnosticRuleSeverity } from "../analyze/lit-analyzer-config"; +import { LitAnalyzerConfig } from "../analyze/lit-analyzer-config"; import { HtmlTag, litAttributeModifierForTarget } from "../analyze/parse/parse-html-data/html-tag"; import { AnalyzerDefinitionStore } from "../analyze/store/analyzer-definition-store"; import { HtmlNodeAttrKind } from "../analyze/types/html-node/html-node-attr-types"; import { HtmlNodeKind } from "../analyze/types/html-node/html-node-types"; -import { LitHtmlDiagnosticKind } from "../analyze/types/lit-diagnostic"; -import { RuleModule } from "../analyze/types/rule-module"; +import { RuleFix } from "../analyze/types/rule/rule-fix"; +import { RuleModule } from "../analyze/types/rule/rule-module"; import { suggestTargetForHtmlAttr } from "../analyze/util/attribute-util"; import { iterableFirst } from "../analyze/util/iterable-util"; -import { rangeFromHtmlNodeAttr } from "../analyze/util/lit-range-util"; +import { rangeFromHtmlNodeAttr } from "../analyze/util/range-util"; /** * This rule validates that only known properties are used in bindings. */ const rule: RuleModule = { - name: "no-unknown-property", - visitHtmlAttribute(htmlAttr, { htmlStore, config, definitionStore, document, file }) { + id: "no-unknown-property", + meta: { + priority: "low" + }, + visitHtmlAttribute(htmlAttr, context) { + const { htmlStore, config, definitionStore } = context; + // Ignore "style" and "svg" attrs because I don't yet have all data for them. if (htmlAttr.htmlNode.kind !== HtmlNodeKind.NODE) return; @@ -30,24 +35,36 @@ const rule: RuleModule = { // Get suggested target because the name could be a typo. const suggestedTarget = suggestTargetForHtmlAttr(htmlAttr, htmlStore); - const suggestedMemberName = (suggestedTarget && `${litAttributeModifierForTarget(suggestedTarget)}${suggestedTarget.name}`) || undefined; + const suggestedModifier = suggestedTarget == null ? undefined : litAttributeModifierForTarget(suggestedTarget); + const suggestedMemberName = suggestedTarget == null ? undefined : suggestedTarget.name; const suggestion = getSuggestionText({ config, definitionStore, htmlTag }); - return [ - { - kind: LitHtmlDiagnosticKind.UNKNOWN_TARGET, - message: `Unknown property '${htmlAttr.name}'.`, - fix: suggestedMemberName == null ? undefined : `Did you mean '${suggestedMemberName}'?`, - source: "no-unknown-property", - severity: litDiagnosticRuleSeverity(config, "no-unknown-property"), - location: rangeFromHtmlNodeAttr(document, htmlAttr), - file, - suggestion, - htmlAttr, - suggestedTarget - } - ]; + context.report({ + location: rangeFromHtmlNodeAttr(htmlAttr), + message: `Unknown property '${htmlAttr.name}'.`, + fixMessage: suggestedMemberName == null ? undefined : `Did you mean '${suggestedModifier}${suggestedMemberName}'?`, + suggestion, + fix: + suggestedMemberName == null + ? undefined + : () => + ({ + message: `Change property to '${suggestedModifier}${suggestedMemberName}'`, + actions: [ + { + kind: "changeAttributeModifier", + newModifier: suggestedModifier, + htmlAttr + }, + { + kind: "changeAttributeName", + newName: suggestedMemberName, + htmlAttr + } + ] + } as RuleFix) + }); } return; diff --git a/packages/lit-analyzer/src/rules/no-unknown-slot.ts b/packages/lit-analyzer/src/rules/no-unknown-slot.ts index 11bbc6f9..ea68ad08 100644 --- a/packages/lit-analyzer/src/rules/no-unknown-slot.ts +++ b/packages/lit-analyzer/src/rules/no-unknown-slot.ts @@ -1,17 +1,20 @@ -import { litDiagnosticRuleSeverity } from "../analyze/lit-analyzer-config"; import { HtmlNodeAttrAssignmentKind } from "../analyze/types/html-node/html-node-attr-assignment-types"; import { HtmlNodeAttrKind } from "../analyze/types/html-node/html-node-attr-types"; -import { LitHtmlDiagnosticKind } from "../analyze/types/lit-diagnostic"; -import { RuleModule } from "../analyze/types/rule-module"; -import { rangeFromHtmlNode, rangeFromHtmlNodeAttr } from "../analyze/util/lit-range-util"; +import { RuleModule } from "../analyze/types/rule/rule-module"; +import { rangeFromHtmlNode, rangeFromHtmlNodeAttr } from "../analyze/util/range-util"; /** * This rule checks validates the slot attribute * and makes sure that slot names used have been defined using jsdoc. */ const rule: RuleModule = { - name: "no-unknown-slot", - visitHtmlNode(htmlNode, { htmlStore, document, config, file }) { + id: "no-unknown-slot", + meta: { + priority: "high" + }, + visitHtmlNode(htmlNode, context) { + const { htmlStore } = context; + // This visitor function validates that a "slot" attribute is present on children elements if a slot name is required. // Get available slot names from the parent node of this node, because this node defined what slots are available. @@ -29,24 +32,27 @@ const rule: RuleModule = { if (slotAttr == null) { const parentTagName = (htmlNode.parent && htmlNode.parent.tagName) || ""; // The slot attribute is missing, and it's not possible to use an unnamed slot. - return [ - { - kind: LitHtmlDiagnosticKind.MISSING_SLOT_ATTRIBUTE, - validSlotNames: slots.map(s => s.name), - htmlNode, - message: `Missing slot attribute. Parent element <${parentTagName}> only allows named slots as children.`, - severity: litDiagnosticRuleSeverity(config, "no-unknown-slot"), - source: "no-unknown-slot", - location: rangeFromHtmlNode(document, htmlNode), - file - } - ]; + + //validSlotNames: slots.map(s => s.name), + context.report({ + location: rangeFromHtmlNode(htmlNode), + message: `Missing slot attribute. Parent element <${parentTagName}> only allows named slots as children.`, + fix: () => ({ + message: `Add slot attribute.`, + actions: [ + { + kind: "addAttribute", + htmlNode, + name: "slot", + value: `""` + } + ] + }) + }); } } - - return; }, - visitHtmlAssignment(assignment, request) { + visitHtmlAssignment(assignment, context) { // This visitor function validates that the value of a "slot" attribute is valid. const { htmlAttr } = assignment; @@ -64,14 +70,14 @@ const rule: RuleModule = { if (parent == null) return; // Validate slots for this attribute if any slots have been defined on the parent element, else opt out. - const parentHtmlTag = request.htmlStore.getHtmlTag(parent.tagName); + const parentHtmlTag = context.htmlStore.getHtmlTag(parent.tagName); if (parentHtmlTag == null || parentHtmlTag.slots.length === 0) return; // Grab the slot name of the "slot" attribute. const slotName = assignment.value; // Find which slots names are valid, and find if the slot name matches any of these. - const validSlots = Array.from(request.htmlStore.getAllSlotsForTag(parentHtmlTag.tagName)); + const validSlots = Array.from(context.htmlStore.getAllSlotsForTag(parentHtmlTag.tagName)); const matchingSlot = validSlots.find(slot => slot.name === slotName); if (matchingSlot == null) { @@ -82,20 +88,11 @@ const rule: RuleModule = { ? `Invalid slot name '${slotName}'. Only the unnamed slot is valid for <${parentHtmlTag.tagName}>` : `Invalid slot name '${slotName}'. Valid slot names for <${parentHtmlTag.tagName}> are: ${validSlotNames.map(n => `'${n}'`).join(" | ")}`; - return [ - { - kind: LitHtmlDiagnosticKind.INVALID_SLOT_NAME, - message, - validSlotNames, - source: "no-unknown-slot", - severity: litDiagnosticRuleSeverity(request.config, "no-unknown-slot"), - location: rangeFromHtmlNodeAttr(request.document, htmlAttr), - file: request.file - } - ]; + context.report({ + location: rangeFromHtmlNodeAttr(htmlAttr), + message + }); } - - return; } }; diff --git a/packages/lit-analyzer/src/rules/no-unknown-tag-name.ts b/packages/lit-analyzer/src/rules/no-unknown-tag-name.ts index 7c31e4c2..17a2cb3d 100644 --- a/packages/lit-analyzer/src/rules/no-unknown-tag-name.ts +++ b/packages/lit-analyzer/src/rules/no-unknown-tag-name.ts @@ -1,16 +1,19 @@ -import { litDiagnosticRuleSeverity } from "../analyze/lit-analyzer-config"; import { HtmlNodeKind } from "../analyze/types/html-node/html-node-types"; -import { LitHtmlDiagnosticKind } from "../analyze/types/lit-diagnostic"; -import { RuleModule } from "../analyze/types/rule-module"; +import { RuleModule } from "../analyze/types/rule/rule-module"; import { findBestStringMatch } from "../analyze/util/find-best-match"; -import { rangeFromHtmlNode } from "../analyze/util/lit-range-util"; +import { rangeFromHtmlNode } from "../analyze/util/range-util"; /** * This rule checks that all tag names used in a template are defined. */ const rule: RuleModule = { - name: "no-unknown-tag-name", - visitHtmlNode(htmlNode, { htmlStore, config, document, file }) { + id: "no-unknown-tag-name", + meta: { + priority: "low" + }, + visitHtmlNode(htmlNode, context) { + const { htmlStore, config } = context; + // Don't validate style and svg yet if (htmlNode.kind !== HtmlNodeKind.NODE) return; @@ -32,20 +35,25 @@ const rule: RuleModule = { suggestion += ` If it can't be imported, consider adding it to the 'globalTags' plugin configuration or disabling the 'no-unknown-tag' rule.`; } - return [ - { - kind: LitHtmlDiagnosticKind.UNKNOWN_TAG, - message: `Unknown tag <${htmlNode.tagName}>.`, - fix: suggestedName == null ? undefined : `Did you mean <${suggestedName}>?`, - source: "no-unknown-tag-name", - severity: litDiagnosticRuleSeverity(config, "no-unknown-tag-name"), - location: rangeFromHtmlNode(document, htmlNode), - file, - suggestion, - htmlNode, - suggestedName - } - ]; + context.report({ + location: rangeFromHtmlNode(htmlNode), + message: `Unknown tag <${htmlNode.tagName}>.`, + fixMessage: suggestedName == null ? undefined : `Did you mean <${suggestedName}>?`, + suggestion, + fix: + suggestedName == null + ? undefined + : () => ({ + message: `Change tag name to '${suggestedName}'`, + actions: [ + { + kind: "changeTagName", + htmlNode, + newName: suggestedName + } + ] + }) + }); } return; diff --git a/packages/lit-analyzer/src/analyze/util/directive/get-directive.ts b/packages/lit-analyzer/src/rules/util/directive/get-directive.ts similarity index 92% rename from packages/lit-analyzer/src/analyze/util/directive/get-directive.ts rename to packages/lit-analyzer/src/rules/util/directive/get-directive.ts index bf8650d2..d2c2f5f9 100644 --- a/packages/lit-analyzer/src/analyze/util/directive/get-directive.ts +++ b/packages/lit-analyzer/src/rules/util/directive/get-directive.ts @@ -1,7 +1,7 @@ import { SimpleType, SimpleTypeKind, toSimpleType } from "ts-simple-type"; import { Expression } from "typescript"; -import { LitAnalyzerRequest } from "../../lit-analyzer-context"; -import { HtmlNodeAttrAssignment, HtmlNodeAttrAssignmentKind } from "../../types/html-node/html-node-attr-assignment-types"; +import { HtmlNodeAttrAssignment, HtmlNodeAttrAssignmentKind } from "../../../analyze/types/html-node/html-node-attr-assignment-types"; +import { RuleModuleContext } from "../../../analyze/types/rule/rule-module-context"; import { removeUndefinedFromType } from "../type/remove-undefined-from-type"; import { isLitDirective } from "./is-lit-directive"; @@ -26,8 +26,8 @@ interface Directive { args: Expression[]; } -export function getDirective(assignment: HtmlNodeAttrAssignment, request: LitAnalyzerRequest): Directive | undefined { - const { ts, program } = request; +export function getDirective(assignment: HtmlNodeAttrAssignment, context: RuleModuleContext): Directive | undefined { + const { ts, program } = context; const checker = program.getTypeChecker(); if (assignment.kind !== HtmlNodeAttrAssignmentKind.EXPRESSION) return; diff --git a/packages/lit-analyzer/src/analyze/util/directive/is-lit-directive.ts b/packages/lit-analyzer/src/rules/util/directive/is-lit-directive.ts similarity index 100% rename from packages/lit-analyzer/src/analyze/util/directive/is-lit-directive.ts rename to packages/lit-analyzer/src/rules/util/directive/is-lit-directive.ts diff --git a/packages/lit-analyzer/src/analyze/util/type/extract-binding-types.ts b/packages/lit-analyzer/src/rules/util/type/extract-binding-types.ts similarity index 89% rename from packages/lit-analyzer/src/analyze/util/type/extract-binding-types.ts rename to packages/lit-analyzer/src/rules/util/type/extract-binding-types.ts index 01e6ce75..17c8138d 100644 --- a/packages/lit-analyzer/src/analyze/util/type/extract-binding-types.ts +++ b/packages/lit-analyzer/src/rules/util/type/extract-binding-types.ts @@ -9,19 +9,19 @@ import { toSimpleType } from "ts-simple-type"; import { Type, TypeChecker, Expression } from "typescript"; -import { LitAnalyzerRequest } from "../../lit-analyzer-context"; -import { HtmlNodeAttrAssignment, HtmlNodeAttrAssignmentKind } from "../../types/html-node/html-node-attr-assignment-types"; -import { HtmlNodeAttrKind } from "../../types/html-node/html-node-attr-types"; +import { HtmlNodeAttrAssignment, HtmlNodeAttrAssignmentKind } from "../../../analyze/types/html-node/html-node-attr-assignment-types"; +import { HtmlNodeAttrKind } from "../../../analyze/types/html-node/html-node-attr-types"; +import { RuleModuleContext } from "../../../analyze/types/rule/rule-module-context"; import { getDirective } from "../directive/get-directive"; const cache = new WeakMap(); -export function extractBindingTypes(assignment: HtmlNodeAttrAssignment, request: LitAnalyzerRequest): { typeA: SimpleType; typeB: SimpleType } { +export function extractBindingTypes(assignment: HtmlNodeAttrAssignment, context: RuleModuleContext): { typeA: SimpleType; typeB: SimpleType } { if (cache.has(assignment)) { return cache.get(assignment)!; } - const checker = request.program.getTypeChecker(); + const checker = context.program.getTypeChecker(); // Relax the type we are looking at an expression in javascript files //const inJavascriptFile = request.file.fileName.endsWith(".js"); @@ -39,13 +39,13 @@ export function extractBindingTypes(assignment: HtmlNodeAttrAssignment, request: })(); // Find a corresponding target for this attribute - const htmlAttrTarget = request.htmlStore.getHtmlAttrTarget(assignment.htmlAttr); + const htmlAttrTarget = context.htmlStore.getHtmlAttrTarget(assignment.htmlAttr); //if (htmlAttrTarget == null) return []; const typeA = htmlAttrTarget == null ? ({ kind: SimpleTypeKind.ANY } as SimpleType) : htmlAttrTarget.getType(); // Handle directives - const directive = getDirective(assignment, request); + const directive = getDirective(assignment, context); if (directive != null && directive.actualType != null) { typeB = directive.actualType; } diff --git a/packages/lit-analyzer/src/analyze/util/type/is-assignable-binding-under-security-system.ts b/packages/lit-analyzer/src/rules/util/type/is-assignable-binding-under-security-system.ts similarity index 63% rename from packages/lit-analyzer/src/analyze/util/type/is-assignable-binding-under-security-system.ts rename to packages/lit-analyzer/src/rules/util/type/is-assignable-binding-under-security-system.ts index f071feb9..1cbbd620 100644 --- a/packages/lit-analyzer/src/analyze/util/type/is-assignable-binding-under-security-system.ts +++ b/packages/lit-analyzer/src/rules/util/type/is-assignable-binding-under-security-system.ts @@ -1,10 +1,8 @@ -import { SimpleType, SimpleTypeKind, toTypeString } from "ts-simple-type"; -import { LitAnalyzerRuleName } from "../../lit-analyzer-config"; -import { LitAnalyzerRequest } from "../../lit-analyzer-context"; -import { HtmlNodeAttr } from "../../types/html-node/html-node-attr-types"; -import { LitHtmlDiagnostic, LitHtmlDiagnosticKind } from "../../types/lit-diagnostic"; +import { SimpleType, toTypeString } from "ts-simple-type"; +import { HtmlNodeAttr } from "../../../analyze/types/html-node/html-node-attr-types"; +import { RuleModuleContext } from "../../../analyze/types/rule/rule-module-context"; import { isLitDirective } from "../directive/is-lit-directive"; -import { rangeFromHtmlNodeAttr } from "../lit-range-util"; +import { rangeFromHtmlNodeAttr } from "../../../analyze/util/range-util"; /** * If the user's security policy overrides normal type checking for this @@ -15,18 +13,17 @@ import { rangeFromHtmlNodeAttr } from "../lit-range-util"; export function isAssignableBindingUnderSecuritySystem( htmlAttr: HtmlNodeAttr, { typeA, typeB }: { typeA: SimpleType; typeB: SimpleType }, - request: LitAnalyzerRequest, - source: LitAnalyzerRuleName -): LitHtmlDiagnostic[] | undefined { - const securityPolicy = request.config.securitySystem; + context: RuleModuleContext +): boolean | undefined { + const securityPolicy = context.config.securitySystem; switch (securityPolicy) { case "off": return undefined; // No security checks apply. case "ClosureSafeTypes": - return checkClosureSecurityAssignability(typeB, htmlAttr, request, source); + return checkClosureSecurityAssignability(typeB, htmlAttr, context); default: { const never: never = securityPolicy; - request.logger.error(`Unexpected security policy: ${never}`); + context.logger.error(`Unexpected security policy: ${never}`); return undefined; } } @@ -63,12 +60,7 @@ const closureGlobalOverrides: SecurityOverrideMap = { style: ["SafeStyle", "string"] }; -function checkClosureSecurityAssignability( - typeB: SimpleType, - htmlAttr: HtmlNodeAttr, - request: LitAnalyzerRequest, - source: LitAnalyzerRuleName -): LitHtmlDiagnostic[] | undefined { +function checkClosureSecurityAssignability(typeB: SimpleType, htmlAttr: HtmlNodeAttr, context: RuleModuleContext): boolean | undefined { const scopedOverride = closureScopedOverrides[htmlAttr.htmlNode.tagName]; const overriddenTypes = (scopedOverride && scopedOverride[htmlAttr.name]) || closureGlobalOverrides[htmlAttr.name]; if (overriddenTypes === undefined) { @@ -78,28 +70,23 @@ function checkClosureSecurityAssignability( if (isLitDirective(typeB)) { return undefined; } + const typeMatch = matchesAtLeastOneNominalType(overriddenTypes, typeB); if (typeMatch === false) { - const nominalType: SimpleType = { + /*const nominalType: SimpleType = { kind: SimpleTypeKind.INTERFACE, members: [], name: "A security type" - }; - return [ - { - kind: LitHtmlDiagnosticKind.INVALID_ATTRIBUTE_EXPRESSION_TYPE, - message: `Type '${toTypeString(typeB)}' is not assignable to '${overriddenTypes.join(" | ")}'. This is due to Closure Safe Type enforcement.`, - severity: "error", - source, - location: rangeFromHtmlNodeAttr(request.document, htmlAttr), - file: request.file, - htmlAttr, - typeA: nominalType, - typeB - } - ]; + };*/ + + context.report({ + location: rangeFromHtmlNodeAttr(htmlAttr), + message: `Type '${toTypeString(typeB)}' is not assignable to '${overriddenTypes.join(" | ")}'. This is due to Closure Safe Type enforcement.` + }); + return false; } - return []; + + return true; } function matchesAtLeastOneNominalType(typeNames: string[], typeB: SimpleType): boolean { diff --git a/packages/lit-analyzer/src/analyze/util/type/is-assignable-in-attribute-binding.ts b/packages/lit-analyzer/src/rules/util/type/is-assignable-in-attribute-binding.ts similarity index 69% rename from packages/lit-analyzer/src/analyze/util/type/is-assignable-in-attribute-binding.ts rename to packages/lit-analyzer/src/rules/util/type/is-assignable-in-attribute-binding.ts index 102c36d2..cdbb98fb 100644 --- a/packages/lit-analyzer/src/analyze/util/type/is-assignable-in-attribute-binding.ts +++ b/packages/lit-analyzer/src/rules/util/type/is-assignable-in-attribute-binding.ts @@ -1,37 +1,28 @@ import { isAssignableToType as _isAssignableToType, SimpleType, SimpleTypeComparisonOptions, SimpleTypeKind, toTypeString } from "ts-simple-type"; -import { litDiagnosticRuleSeverity } from "../../lit-analyzer-config"; -import { LitAnalyzerRequest } from "../../lit-analyzer-context"; -import { HtmlNodeAttrAssignmentKind } from "../../types/html-node/html-node-attr-assignment-types"; -import { HtmlNodeAttr } from "../../types/html-node/html-node-attr-types"; -import { LitHtmlDiagnostic, LitHtmlDiagnosticKind } from "../../types/lit-diagnostic"; -import { rangeFromHtmlNodeAttr } from "../lit-range-util"; -import { isAssignableToType } from "./is-assignable-to-type"; +import { HtmlNodeAttrAssignmentKind } from "../../../analyze/types/html-node/html-node-attr-assignment-types"; +import { HtmlNodeAttr } from "../../../analyze/types/html-node/html-node-attr-types"; +import { RuleModuleContext } from "../../../analyze/types/rule/rule-module-context"; import { isLitDirective } from "../directive/is-lit-directive"; +import { rangeFromHtmlNodeAttr } from "../../../analyze/util/range-util"; import { isAssignableBindingUnderSecuritySystem } from "./is-assignable-binding-under-security-system"; +import { isAssignableToType } from "./is-assignable-to-type"; export function isAssignableInAttributeBinding( htmlAttr: HtmlNodeAttr, { typeA, typeB }: { typeA: SimpleType; typeB: SimpleType }, - request: LitAnalyzerRequest -): LitHtmlDiagnostic[] | undefined { + context: RuleModuleContext +): boolean | undefined { const { assignment } = htmlAttr; if (assignment == null) return undefined; if (assignment.kind === HtmlNodeAttrAssignmentKind.BOOLEAN) { - if (!isAssignableToType({ typeA, typeB }, request)) { - return [ - { - kind: LitHtmlDiagnosticKind.INVALID_ATTRIBUTE_EXPRESSION_TYPE, - message: `Type '${toTypeString(typeB)}' is not assignable to '${toTypeString(typeA)}'`, - severity: litDiagnosticRuleSeverity(request.config, "no-incompatible-type-binding"), - source: "no-incompatible-type-binding", - location: rangeFromHtmlNodeAttr(request.document, htmlAttr), - file: request.file, - htmlAttr, - typeA, - typeB - } - ]; + if (!isAssignableToType({ typeA, typeB }, context)) { + context.report({ + location: rangeFromHtmlNodeAttr(htmlAttr), + message: `Type '${toTypeString(typeB)}' is not assignable to '${toTypeString(typeA)}'` + }); + + return false; } } else { if (assignment.kind !== HtmlNodeAttrAssignmentKind.STRING) { @@ -42,31 +33,25 @@ export function isAssignableInAttributeBinding( // For everything else, we may need to apply a different type comparison // for some security-sensitive built in attributes and properties (like // `"); - hasDiagnostic(t, diagnostics, "no-complex-attribute-binding"); + hasDiagnostic(t, diagnostics, "no-incompatible-type-binding"); }); testName = "May pass a TrustedResourceUrl to script src with ClosureSafeTypes config"; @@ -61,7 +59,7 @@ tsTest(testName, t => { testName = "May not pass a SafeUrl to script src with ClosureSafeTypes config"; tsTest(testName, t => { const { diagnostics } = getDiagnostics(preface + "html``", { securitySystem: "ClosureSafeTypes" }); - hasDiagnostic(t, diagnostics, "no-incompatible-type-binding"); + hasDiagnostic(t, diagnostics, "no-complex-attribute-binding"); }); testName = "May not pass a SafeUrl to script .src with ClosureSafeTypes config"; diff --git a/packages/ts-lit-plugin/src/ts-lit-plugin/translate/translate-code-fixes.ts b/packages/ts-lit-plugin/src/ts-lit-plugin/translate/translate-code-fixes.ts index 524898bd..d2bc5ea7 100644 --- a/packages/ts-lit-plugin/src/ts-lit-plugin/translate/translate-code-fixes.ts +++ b/packages/ts-lit-plugin/src/ts-lit-plugin/translate/translate-code-fixes.ts @@ -1,7 +1,6 @@ +import { LitCodeFix, LitCodeFixAction } from "lit-analyzer"; import { CodeFixAction, FileTextChanges, SourceFile } from "typescript"; -import { tsModule } from "../../ts-module"; import { translateRange } from "./translate-range"; -import { CodeActionKind, LitCodeFix, LitCodeFixAction } from "lit-analyzer"; export function translateCodeFixes(codeFixes: LitCodeFix[], file: SourceFile): CodeFixAction[] { return codeFixes.map(codeFix => translateCodeFix(file, codeFix)); @@ -9,53 +8,20 @@ export function translateCodeFixes(codeFixes: LitCodeFix[], file: SourceFile): C export function translateCodeFix(file: SourceFile, codeFix: LitCodeFix): CodeFixAction { return { - fixName: codeFix.kind.toLowerCase(), + fixName: codeFix.name, description: codeFix.message, changes: codeFix.actions.map(action => translateCodeFixAction(file, action)) }; } function translateCodeFixAction(file: SourceFile, action: LitCodeFixAction): FileTextChanges { - switch (action.kind) { - case CodeActionKind.TEXT_CHANGE: - return { - fileName: file.fileName, - textChanges: [ - { - span: translateRange(action.change.range), - newText: action.change.newText - } - ] - }; - case CodeActionKind.IMPORT_COMPONENT: { - // Get the import path and the position where it can be placed - const lastImportIndex = getLastImportIndex(file); - - return { - fileName: file.fileName, - textChanges: [ - { - span: { start: lastImportIndex, length: 0 }, - newText: `\nimport "${action.importPath}";` - } - ] - }; - } - } -} - -/** - * Returns the position of the last import line. - * @param sourceFile - */ -function getLastImportIndex(sourceFile: SourceFile): number { - let lastImportIndex = 0; - - for (const statement of sourceFile.statements) { - if (tsModule.ts.isImportDeclaration(statement)) { - lastImportIndex = statement.getEnd(); - } - } - - return lastImportIndex; + return { + fileName: file.fileName, + textChanges: [ + { + span: translateRange(action.range), + newText: action.newText + } + ] + }; } diff --git a/packages/ts-lit-plugin/src/ts-lit-plugin/translate/translate-diagnostics.ts b/packages/ts-lit-plugin/src/ts-lit-plugin/translate/translate-diagnostics.ts index 2f71b9bf..0e1f3872 100644 --- a/packages/ts-lit-plugin/src/ts-lit-plugin/translate/translate-diagnostics.ts +++ b/packages/ts-lit-plugin/src/ts-lit-plugin/translate/translate-diagnostics.ts @@ -11,7 +11,7 @@ export function translateDiagnostics(reports: LitDiagnostic[], file: SourceFile, * @param diagnostic */ function getMessageTextFromDiagnostic(diagnostic: LitDiagnostic): string { - return `${diagnostic.message}${diagnostic.fix == null ? "" : ` ${diagnostic.fix}`}`; + return `${diagnostic.message}${diagnostic.fixMessage == null ? "" : ` ${diagnostic.fixMessage}`}`; } function translateDiagnostic(diagnostic: LitDiagnostic, file: SourceFile, context: LitAnalyzerContext): DiagnosticWithLocation { diff --git a/packages/ts-lit-plugin/src/ts-lit-plugin/translate/translate-range.ts b/packages/ts-lit-plugin/src/ts-lit-plugin/translate/translate-range.ts index 5d45a738..76537253 100644 --- a/packages/ts-lit-plugin/src/ts-lit-plugin/translate/translate-range.ts +++ b/packages/ts-lit-plugin/src/ts-lit-plugin/translate/translate-range.ts @@ -1,14 +1,7 @@ +import { Range } from "lit-analyzer"; import { TextSpan } from "typescript"; -import { LitRange } from "../../../../lit-analyzer/src/analyze/types/lit-range"; - -export function translateRange(range: LitRange): TextSpan { - if ("document" in range) { - return { - start: range.document.virtualDocument.offsetToSCPosition(range.start), - length: range.end - range.start - }; - } +export function translateRange(range: Range): TextSpan { return { start: range.start, length: range.end - range.start diff --git a/packages/ts-lit-plugin/src/ts-lit-plugin/ts-lit-plugin.ts b/packages/ts-lit-plugin/src/ts-lit-plugin/ts-lit-plugin.ts index 685eb7a7..979ee487 100644 --- a/packages/ts-lit-plugin/src/ts-lit-plugin/ts-lit-plugin.ts +++ b/packages/ts-lit-plugin/src/ts-lit-plugin/ts-lit-plugin.ts @@ -15,10 +15,10 @@ import { RenameInfo, RenameInfoOptions, RenameLocation, - TextChange, - UserPreferences, + SignatureHelpItems, SignatureHelpItemsOptions, - SignatureHelpItems + TextChange, + UserPreferences } from "typescript"; import { LitPluginContext } from "./lit-plugin-context"; import { translateCodeFixes } from "./translate/translate-code-fixes"; @@ -119,7 +119,7 @@ export class TsLitPlugin { const result = this.prevLangService.getSignatureHelpItems(fileName, position, options); // Test if the signature is "html" or "css - // Don't return a signature if trying to show signature fo the html/css tagged template literal + // Don't return a signature if trying to show signature for the html/css tagged template literal if (result != null && result.items.length === 1) { const displayPart = result.items[0].prefixDisplayParts[0]; if (displayPart.kind === "aliasName" && (displayPart.text === "html" || displayPart.text === "css")) {