Skip to content

Commit

Permalink
Fix #1596
Browse files Browse the repository at this point in the history
  • Loading branch information
octref committed Sep 20, 2020
1 parent e68b119 commit 021fa20
Show file tree
Hide file tree
Showing 36 changed files with 665 additions and 276 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@
"vetur.validation.templateProps": {
"type": "boolean",
"default": false,
"description": "Validate props usage in <template> region. Show error/warning for not passing declared props to child components."
"description": "Validate props usage in <template> region. Show error/warning for not passing declared props to child components and show error for passing wrongly typed interpolation expressions"
},
"vetur.validation.interpolation": {
"type": "boolean",
Expand Down
2 changes: 1 addition & 1 deletion server/src/embeddedSupport/languageModes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ export class LanguageModes {
}

/**
* Documents where everything outside `<script>~ is replaced with whitespace
* Documents where everything outside `<script>` is replaced with whitespace
*/
const scriptRegionDocuments = getLanguageModelCache(10, 60, document => {
const vueDocument = this.documentRegions.refreshAndGet(document);
Expand Down
84 changes: 80 additions & 4 deletions server/src/modes/script/componentInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,16 +138,92 @@ function getProps(tsModule: T_TypeScript, defaultExportType: ts.Type, checker: t

function getPropValidatorInfo(
propertyValue: ts.Node | undefined
): { hasObjectValidator: boolean; required: boolean } {
if (!propertyValue || !tsModule.isObjectLiteralExpression(propertyValue)) {
): { hasObjectValidator: boolean; required: boolean; typeString?: string } {
if (!propertyValue) {
return { hasObjectValidator: false, required: true };
}

let typeString: string | undefined = undefined;
let typeDeclaration: ts.Identifier | ts.AsExpression | undefined = undefined;

/**
* case `foo: { type: String }`
* extract type value: `String`
*/
if (tsModule.isObjectLiteralExpression(propertyValue)) {
const propertyValueSymbol = checker.getTypeAtLocation(propertyValue).symbol;
const typeValue = propertyValueSymbol?.members?.get('type' as ts.__String)?.valueDeclaration;
if (typeValue && tsModule.isPropertyAssignment(typeValue)) {
if (tsModule.isIdentifier(typeValue.initializer) || tsModule.isAsExpression(typeValue.initializer)) {
typeDeclaration = typeValue.initializer;
}
}
} else {
/**
* case `foo: String`
* extract type value: `String`
*/
if (tsModule.isIdentifier(propertyValue) || tsModule.isAsExpression(propertyValue)) {
typeDeclaration = propertyValue;
}
}

if (typeDeclaration) {
/**
* `String` case
*
* Per https://vuejs.org/v2/guide/components-props.html#Type-Checks, handle:
*
* String
* Number
* Boolean
* Array
* Object
* Date
* Function
* Symbol
*/
if (tsModule.isIdentifier(typeDeclaration)) {
const vueTypeCheckConstructorToTSType: Record<string, string> = {
String: 'string',
Number: 'number',
Boolean: 'boolean',
Array: 'any[]',
Object: 'object',
Date: 'Date',
Function: 'Function',
Symbol: 'Symbol'
};
const vueTypeString = typeDeclaration.getText();
if (vueTypeCheckConstructorToTSType[vueTypeString]) {
typeString = vueTypeCheckConstructorToTSType[vueTypeString];
}
} else if (
/**
* `String as PropType<'a' | 'b'>` case
*/
tsModule.isAsExpression(typeDeclaration) &&
tsModule.isTypeReferenceNode(typeDeclaration.type) &&
typeDeclaration.type.typeName.getText() === 'PropType' &&
typeDeclaration.type.typeArguments &&
typeDeclaration.type.typeArguments[0]
) {
const extractedPropType = typeDeclaration.type.typeArguments[0];
typeString = extractedPropType.getText();
}
}

// console.log(`${(propertyValue.parent as ts.PropertyAssignment).name.getText()}: ${typeString}`)

if (!propertyValue || !tsModule.isObjectLiteralExpression(propertyValue)) {
return { hasObjectValidator: false, required: true, typeString };
}

const propertyValueSymbol = checker.getTypeAtLocation(propertyValue).symbol;
const requiredValue = propertyValueSymbol?.members?.get('required' as ts.__String)?.valueDeclaration;
const defaultValue = propertyValueSymbol?.members?.get('default' as ts.__String)?.valueDeclaration;
if (!requiredValue && !defaultValue) {
return { hasObjectValidator: false, required: true };
return { hasObjectValidator: false, required: true, typeString };
}

const required = Boolean(
Expand All @@ -156,7 +232,7 @@ function getProps(tsModule: T_TypeScript, defaultExportType: ts.Type, checker: t
requiredValue?.initializer.kind === tsModule.SyntaxKind.TrueKeyword
);

return { hasObjectValidator: true, required };
return { hasObjectValidator: true, required, typeString };
}

function getClassProps(type: ts.Type) {
Expand Down
2 changes: 1 addition & 1 deletion server/src/modes/template/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export class VueHTMLMode implements LanguageMode {
const vueDocuments = getLanguageModelCache<HTMLDocument>(10, 60, document => parseHTMLDocument(document));
const vueVersion = inferVueVersion(tsModule, workspacePath);
this.htmlMode = new HTMLMode(documentRegions, workspacePath, vueVersion, vueDocuments, vueInfoService);
this.vueInterpolationMode = new VueInterpolationMode(tsModule, serviceHost, vueDocuments);
this.vueInterpolationMode = new VueInterpolationMode(tsModule, serviceHost, vueDocuments, vueInfoService);
}
getId() {
return 'vue-html';
Expand Down
73 changes: 47 additions & 26 deletions server/src/modes/template/interpolationMode.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,44 @@
import { LanguageMode } from '../../embeddedSupport/languageModes';
import * as _ from 'lodash';
import * as ts from 'typescript';
import {
CompletionItem,
CompletionList,
Definition,
Diagnostic,
TextDocument,
DiagnosticSeverity,
Position,
Location,
MarkedString,
MarkupContent,
Position,
Range,
Location,
Definition,
CompletionList,
TextEdit,
CompletionItem,
MarkupContent
TextDocument,
TextEdit
} from 'vscode-languageserver-types';
import { IServiceHost } from '../../services/typescriptService/serviceHost';
import { languageServiceIncludesFile } from '../script/javascript';
import { getFileFsPath } from '../../utils/paths';
import { mapBackRange, mapFromPositionToOffset } from '../../services/typescriptService/sourceMap';
import { URI } from 'vscode-uri';
import * as ts from 'typescript';
import { VLSFullConfig } from '../../config';
import { LanguageModelCache } from '../../embeddedSupport/languageModelCache';
import { LanguageMode } from '../../embeddedSupport/languageModes';
import { T_TypeScript } from '../../services/dependencyService';
import * as _ from 'lodash';
import { IServiceHost } from '../../services/typescriptService/serviceHost';
import { mapBackRange, mapFromPositionToOffset } from '../../services/typescriptService/sourceMap';
import { createTemplateDiagnosticFilter } from '../../services/typescriptService/templateDiagnosticFilter';
import { NULL_COMPLETION } from '../nullMode';
import { toCompletionItemKind } from '../../services/typescriptService/util';
import { LanguageModelCache } from '../../embeddedSupport/languageModelCache';
import { VueInfoService } from '../../services/vueInfoService';
import { getFileFsPath } from '../../utils/paths';
import { NULL_COMPLETION } from '../nullMode';
import { languageServiceIncludesFile } from '../script/javascript';
import * as Previewer from '../script/previewer';
import { HTMLDocument } from './parser/htmlParser';
import { isInsideInterpolation } from './services/isInsideInterpolation';
import * as Previewer from '../script/previewer';
import { VLSFullConfig } from '../../config';

export class VueInterpolationMode implements LanguageMode {
private config: VLSFullConfig;

constructor(
private tsModule: T_TypeScript,
private serviceHost: IServiceHost,
private vueDocuments: LanguageModelCache<HTMLDocument>
private vueDocuments: LanguageModelCache<HTMLDocument>,
private vueInfoService?: VueInfoService
) {}

getId() {
Expand Down Expand Up @@ -67,7 +69,15 @@ export class VueInterpolationMode implements LanguageMode {
document.getText()
);

const { templateService, templateSourceMap } = this.serviceHost.updateCurrentVirtualVueTextDocument(templateDoc);
const childComponents = this.config.vetur.validation.templateProps
? this.vueInfoService && this.vueInfoService.getInfo(document)?.componentInfo.childComponents
: [];

const { templateService, templateSourceMap } = this.serviceHost.updateCurrentVirtualVueTextDocument(
templateDoc,
childComponents
);

if (!languageServiceIncludesFile(templateService, templateDoc.uri)) {
return [];
}
Expand Down Expand Up @@ -135,16 +145,27 @@ export class VueInterpolationMode implements LanguageMode {
const mappedOffset = mapFromPositionToOffset(templateDoc, completionPos, templateSourceMap);
const templateFileFsPath = getFileFsPath(templateDoc.uri);

const completions = templateService.getCompletionsAtPosition(templateFileFsPath, mappedOffset, {
includeCompletionsWithInsertText: true,
includeCompletionsForModuleExports: false
});
/**
* A lot of times interpolation expressions aren't valid
* TODO: Make sure interpolation expression, even incomplete, can generate incomplete
* TS files that can be fed into language service
*/
let completions: ts.WithMetadata<ts.CompletionInfo> | undefined;
try {
completions = templateService.getCompletionsAtPosition(templateFileFsPath, mappedOffset, {
includeCompletionsWithInsertText: true,
includeCompletionsForModuleExports: false
});
} catch (err) {
console.log('Interpolation completion failed');
console.error(err.toString());
}

if (!completions) {
return NULL_COMPLETION;
}

const tsItems = completions.entries.map((entry, index) => {
const tsItems = completions!.entries.map((entry, index) => {
return {
uri: templateDoc.uri,
position,
Expand Down
105 changes: 0 additions & 105 deletions server/src/modes/template/services/vueInterpolationCompletion.ts

This file was deleted.

6 changes: 3 additions & 3 deletions server/src/services/typescriptService/bridge.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { renderHelperName, componentHelperName, iterationHelperName } from './transformTemplate';
import { renderHelperName, componentHelperName, iterationHelperName, componentDataName } from './transformTemplate';

// This bridge file will be injected into TypeScript language service
// it enable type checking and completion, yet still preserve precise option type
Expand All @@ -11,7 +11,7 @@ const renderHelpers = `
type ComponentListeners<T> = {
[K in keyof T]?: ($event: T[K]) => any;
};
interface ComponentData<T> {
export interface ${componentDataName}<T> {
props: Record<string, any>;
on: ComponentListeners<T>;
directives: any[];
Expand All @@ -23,7 +23,7 @@ export declare const ${componentHelperName}: {
<T>(
vm: T,
tag: string,
data: ComponentData<Record<string, any>> & ThisType<T>,
data: ${componentDataName}<Record<string, any>> & ThisType<T>,
children: any[]
): any;
};
Expand Down
Loading

0 comments on commit 021fa20

Please sign in to comment.