Skip to content

Commit

Permalink
feat(compiler): adds more compiler checks
Browse files Browse the repository at this point in the history
- Component must be the only decorator
- Component must be exported
- Component must the only export of a module
- Component can not inherit from a base class
  • Loading branch information
manucorporat committed Aug 24, 2018
1 parent 2865fae commit 6fa63fd
Show file tree
Hide file tree
Showing 2 changed files with 73 additions and 17 deletions.
10 changes: 10 additions & 0 deletions src/compiler/transpile/datacollection/component-decorator.ts
Expand Up @@ -22,6 +22,16 @@ export function getComponentDecoratorMeta(diagnostics: d.Diagnostic[], checker:
throw new Error(`tag missing in component decorator: ${JSON.stringify(componentOptions, null, 2)}`);
}

if (node.heritageClauses && node.heritageClauses.some(c => c.token === ts.SyntaxKind.ExtendsKeyword)) {
throw new Error(`Classes decorated with @Component can not extend from a base class.
Inherency is temporarily disabled for stencil components.`);
}

// check if class has more than one decorator
if (node.decorators.length > 1) {

This comment has been minimized.

Copy link
@kurtlobato

kurtlobato Sep 24, 2018

Why this? Adding a decorator to the class seems to work fine, and since inheritance doesn't work. forbidding decorators is basically forcing to copy/paste code all over the components

throw new Error(`@Component({ tag: "${componentOptions.tag}"}) can not be decorated with more decorators at the same time`);
}

const symbol = checker.getSymbolAtLocation(node.name);

const cmpMeta: d.ComponentMeta = {
Expand Down
80 changes: 63 additions & 17 deletions src/compiler/transpile/datacollection/gather-metadata.ts
Expand Up @@ -13,45 +13,91 @@ import { normalizeAssetsDir } from '../../component-plugins/assets-plugin';
import { normalizeStyles } from '../../style/normalize-styles';
import { validateComponentClass } from './validate-component';
import * as ts from 'typescript';
import { buildError } from '../../util';
import { isDecoratorNamed } from './utils';


export function gatherMetadata(config: d.Config, compilerCtx: d.CompilerCtx, buildCtx: d.BuildCtx, typeChecker: ts.TypeChecker): ts.TransformerFactory<ts.SourceFile> {

return (transformContext) => {

function visit(node: ts.Node, tsSourceFile: ts.SourceFile, moduleFile: d.ModuleFile): ts.VisitResult<ts.Node> {
function visit(node: ts.Node, tsSourceFile: ts.SourceFile, moduleFile: d.ModuleFile) {

if (node.kind === ts.SyntaxKind.ImportDeclaration) {
getCollections(config, compilerCtx, buildCtx.collections, moduleFile, node as ts.ImportDeclaration);
}

if (ts.isClassDeclaration(node)) {
const cmpMeta = visitClass(buildCtx.diagnostics, typeChecker, node as ts.ClassDeclaration, tsSourceFile);
if (cmpMeta) {
moduleFile.cmpMeta = cmpMeta;
try {
if (node.kind === ts.SyntaxKind.ImportDeclaration) {
getCollections(config, compilerCtx, buildCtx.collections, moduleFile, node as ts.ImportDeclaration);
}

cmpMeta.stylesMeta = normalizeStyles(config, moduleFile.sourceFilePath, cmpMeta.stylesMeta);
cmpMeta.assetsDirsMeta = normalizeAssetsDir(config, moduleFile.sourceFilePath, cmpMeta.assetsDirsMeta);
if (ts.isClassDeclaration(node)) {
const cmpMeta = visitClass(buildCtx.diagnostics, typeChecker, node as ts.ClassDeclaration, tsSourceFile);
if (cmpMeta) {
if (moduleFile.cmpMeta) {
throw new Error(`More than one @Component() class in a single file is not valid`);
}
moduleFile.cmpMeta = cmpMeta;

cmpMeta.stylesMeta = normalizeStyles(config, moduleFile.sourceFilePath, cmpMeta.stylesMeta);
cmpMeta.assetsDirsMeta = normalizeAssetsDir(config, moduleFile.sourceFilePath, cmpMeta.assetsDirsMeta);
}
}
}
return node;

return ts.visitEachChild(node, (node) => {
return visit(node, tsSourceFile, moduleFile);
}, transformContext);
} catch ({message}) {
const error = buildError(buildCtx.diagnostics);
error.messageText = message;
error.relFilePath = tsSourceFile.fileName;
}
return undefined;
}

return (tsSourceFile) => {
const moduleFile = getModuleFile(compilerCtx, tsSourceFile.fileName);
moduleFile.externalImports.length = 0;
moduleFile.localImports.length = 0;
moduleFile.cmpMeta = undefined;

return visit(tsSourceFile, tsSourceFile, moduleFile) as ts.SourceFile;
const results = ts.visitEachChild(tsSourceFile, (node) => {
return visit(node, tsSourceFile, moduleFile);
}, transformContext);

if (moduleFile.cmpMeta) {
const fileSymbol = typeChecker.getSymbolAtLocation(tsSourceFile);
const fileExports = (fileSymbol && typeChecker.getExportsOfModule(fileSymbol)) || [];

if (fileExports.length > 1) {
const error = buildError(buildCtx.diagnostics);
error.messageText = `@Component() must be the only export of the module`;
error.relFilePath = tsSourceFile.fileName;

} else if (
fileExports.length === 0 ||
!isComponentClass(fileExports[0])
) {
const error = buildError(buildCtx.diagnostics);
error.messageText = `Missing export in @Component() class`;
error.relFilePath = tsSourceFile.fileName;
}
}
return results;
};
};
}

function isComponentClass(symbol: ts.Symbol) {
const decorators = symbol.valueDeclaration && symbol.valueDeclaration.decorators;
if (!decorators) {
return false;
}
return isDecoratorNamed('Component')(decorators[0]);
}


export function visitClass(diagnostics: d.Diagnostic[], typeChecker: ts.TypeChecker, classNode: ts.ClassDeclaration, sourceFile: ts.SourceFile): d.ComponentMeta | undefined {
export function visitClass(
diagnostics: d.Diagnostic[],
typeChecker: ts.TypeChecker,
classNode: ts.ClassDeclaration,
sourceFile: ts.SourceFile,
): d.ComponentMeta | undefined {
const cmpMeta = getComponentDecoratorMeta(diagnostics, typeChecker, classNode);

if (!cmpMeta) {
Expand Down

0 comments on commit 6fa63fd

Please sign in to comment.