Compare commits

..

17 Commits

Author SHA1 Message Date
Alex Rickabaugh b8183614f5 release: cut the v12.2.3 release (#43257)
PR Close #43257
2021-08-25 14:20:31 -07:00
George Kalpakas b46ce7d42f build(docs-infra): upgrade cli command docs sources to 56c5962df (#43239)
Updating [angular#12.2.x](https://github.com/angular/angular/tree/12.2.x) from
[cli-builds#12.2.x](https://github.com/angular/cli-builds/tree/12.2.x).

##
Relevant changes in
[commit range](https://github.com/angular/cli-builds/compare/7c2d76d9e...56c5962df):

**Modified**
- help/lint.json

PR Close #43239
2021-08-25 10:48:04 -07:00
Daniel Trevino 36bf0ae985 test(language-service): add test for extended diagnostics (#43134)
Add a test to make sure extended template diagnostics are being correctly
generated in the language service for non-ts files.

Refs #42966

PR Close #43134
2021-08-25 10:43:02 -07:00
Daniel Trevino 3b317dcf87 refactor(compiler-cli): integrate extended diagnostics with the language service (#43134)
Add the call to get the extended template diagnostics in
the compiler's `getDiagnosticsForComponent`. This makes showing
extended diagnostics in non-ts files posible.

Refs #42966

PR Close #43134
2021-08-25 10:43:02 -07:00
Daniel Trevino 3ad527d86e refactor(compiler-cli): rename ExtendedTemplateChecker get diagnostics fucntion (#43134)
Rename `getExtendedTemplateDiagnosticsForComponent` to
`getDiagnosticsForComponent` since it's implied they are extended
diagnostics.

Refs #42966

PR Close #43134
2021-08-25 10:43:02 -07:00
Daniel Trevino b6ff762e36 refactor(compiler-cli): move getDiagnosticsForComponent to compiler (#43134)
Change the language service to call `getDiagnosticsForComponent` from
the compiler instead of the `TemplateTypeChecker`.

Refs #42966

PR Close #43134
2021-08-25 10:43:02 -07:00
Daniel Trevino e196311e5c refactor(compiler-cli): move ExtendedTemplateChecker to LazyCompilationState (#43134)
Create the `ExtendedTemplateChecker` during `makeCompilation`, that way
it's avaiblable in the whole compiler.

Refs #42966

PR Close #43134
2021-08-25 10:43:01 -07:00
Daniel Trevino 3cf8f66f3a refactor(compiler-cli): use TemplateDiagnostic in extended package (#43134)
Return `TemplateDiagnostic` instead of `ts.Diagnostic` when getting the
extended template diagnostics. This makes the integration with the
language service easier. This also fixes the error code and now uses the
`ngErrorCode` for extended template diagnostics.

Refs #42966

PR Close #43134
2021-08-25 10:43:01 -07:00
dario-piotrowicz d7b6416fcf refactor(common): fix emiting typos (#43221)
fix some of the aio examples which log strings containing "emiting" instead of "emitting"

PR Close #43221
2021-08-23 13:30:29 -07:00
shlasouski fc7f92159d fix(service-worker): NPE if onActionClick is undefined (#43210)
Previously, it leads to fail if notification.data.onActionClick is undefined

PR Close #43210
2021-08-23 13:29:59 -07:00
Daniel Trevino cb8e51f8cf test(language-service): add extended diagnostics test (#43107)
Add a test in the langauge-service to make sure the extended template
diagnostics are being correctly generated.

Refs #42966

PR Close #43107
2021-08-19 11:55:28 -07:00
Daniel Trevino e6b2bedd70 test(compiler-cli): add extended template diagnostics end-to-end tests (#43107)
This commit adds extended template diagnostics end-to-end tests, to make
sure the diagnostics are generated correctly. Template checks are
already tested with unit tests.

Refs #42966

PR Close #43107
2021-08-19 11:55:28 -07:00
Daniel Trevino 58a19a3021 refactor(compiler-cli): add extendedTemplateCheck phase to compiler (#43107)
This commit integrates extended template checks with the compiler, by
adding another phase of diagnostics generation. This integration is
under the `_extendedTemplateDiagnostics` flag.

Refs #42966

PR Close #43107
2021-08-19 11:55:28 -07:00
Daniel Trevino f70d2a2981 refactor(compiler-cli): export ExtendedTemplateCheckerImpl and template checks (#43107)
This commit exports the implementation of `ExtendedTemplateChecker` to
generate extended template diagnostics and all the template checks.

Refs #42966

PR Close #43107
2021-08-19 11:55:28 -07:00
Daniel Trevino 3bbf00fdd4 refactor(compiler-cli): add ExtendedTemplateChecker (#43107)
Change the current way to run template checks to the
`ExtendedTemplateChecker` instead of just the
`getExtendedTemplateDiagnosticsForComponent` function. Refactored the
tests that used the previous function to use the new class.

Refs #42966

PR Close #43107
2021-08-19 11:55:28 -07:00
dario-piotrowicz 19fa033f79 refactor(docs-infra): use tslint in aio's tools-lint script (#43010)
Instead of the deprecated tslint use eslint in the aio's tools-lint
script

PR Close #43010
2021-08-19 11:20:06 -07:00
Erik Slack 513b17043e docs: fix typo (#43190)
Fix typo in description of ngxp and link for angular community meetup

PR Close #43190
2021-08-18 10:52:19 -07:00
44 changed files with 537 additions and 227 deletions
+10
View File
@@ -1,3 +1,13 @@
<a name="12.2.3"></a>
# 12.2.3 (2021-08-25)
### service-worker
| Commit | Description |
| -- | -- |
| [fc7f92159d](https://github.com/angular/angular/commit/fc7f92159df16e894d9909cfc8969ed4b7d9924a) | fix: NPE if onActionClick is undefined ([#43210](https://github.com/angular/angular/pull/43210)) |
## Special Thanks:
Daniel Trevino, Erik Slack, George Kalpakas, dario-piotrowicz and shlasouski
<a name="12.2.2"></a>
# 12.2.2 (2021-08-18)
### animations
@@ -21,12 +21,12 @@ export class AliasingComponent {
saveIt() {
console.warn('Child says: emiting outputEvent1 with', this.input1);
console.warn('Child says: emitting outputEvent1 with', this.input1);
this.outputEvent1.emit(this.input1);
}
wishForIt() {
console.warn('Child says: emiting outputEvent2', this.input2);
console.warn('Child says: emitting outputEvent2', this.input2);
this.outputEvent2.emit(this.input2);
}
@@ -20,7 +20,7 @@ export class InTheMetadataComponent {
clearanceItem = '';
buyIt() {
console.warn('Child says: emiting buyEvent with', this.clearanceItem);
console.warn('Child says: emitting buyEvent with', this.clearanceItem);
this.buyEvent.emit(this.clearanceItem);
}
@@ -12,7 +12,7 @@ export class InputOutputComponent {
lineThrough = '';
delete() {
console.warn('Child says: emiting item deleteRequest with', this.item);
console.warn('Child says: emitting item deleteRequest with', this.item);
this.deleteRequest.emit(this.item);
this.lineThrough = this.lineThrough ? '' : 'line-through';
}
+2 -2
View File
@@ -52,7 +52,7 @@
},
"angular-community-meetup": {
"desc": "Come learn technical skills and network at this monthly, multi-series, multi-event, multi-timezone online meetup.",
"url": "https://ngxp.show",
"url": "https://angularcommunity.net",
"title": "Angular Community Meetup"
}
}
@@ -97,7 +97,7 @@
"url": "https://github.com/ngRuAir/ngruair"
},
"ngxp": {
"desc": "Weekly technical-adjacent podcast by Brooke Avery & Erik Slack.on't just code in Angular, come enjoy the Angular Experience!",
"desc": "Weekly technical-adjacent podcast by Brooke Avery & Erik Slack. Don't just code in Angular, come enjoy the Angular Experience!",
"logo": "",
"title": "Angular Experience (NgXP)",
"url": "https://ngxp.show"
+2 -2
View File
@@ -18,7 +18,7 @@
"build-local": "yarn ~~build",
"prebuild-local-ci": "yarn setup-local-ci",
"build-local-ci": "yarn ~~build --progress=false",
"extract-cli-command-docs": "node tools/transforms/cli-docs-package/extract-cli-commands.js 7c2d76d9e",
"extract-cli-command-docs": "node tools/transforms/cli-docs-package/extract-cli-commands.js 56c5962df",
"lint": "yarn check-env && yarn docs-lint && ng lint && yarn example-lint && yarn tools-lint",
"test": "yarn check-env && ng test",
"pree2e": "yarn check-env && yarn update-webdriver",
@@ -56,7 +56,7 @@
"docs-test": "node tools/transforms/test.js",
"redirects-test": "node tests/deployment/unit/test",
"firebase-utils-test": "node tools/firebase-test-utils/test",
"tools-lint": "tslint --config \"tools/tslint.json\" --project \"tools/firebase-test-utils\" && eslint tools/ng-packages-installer",
"tools-lint": "eslint tools/firebase-test-utils && eslint tools/ng-packages-installer",
"tools-test": "yarn docs-test && yarn boilerplate:test && jasmine tools/ng-packages-installer/index.spec.js && jasmine scripts/deploy-to-firebase.spec.js && yarn firebase-utils-test",
"preserve-and-sync": "yarn docs",
"serve-and-sync": "run-p \"docs-watch --watch-only\" \"start {@}\" --",
@@ -0,0 +1,23 @@
{
"extends": "../../.eslintrc.json",
"overrides": [
{
"files": ["*.js"],
"env": {
"es6": true,
"node": true
},
"extends": [
"eslint:recommended"
],
"parserOptions": {
"ecmaVersion": 2020
},
"rules": {
"quotes": ["error", "single"],
"semi": ["error", "always"],
"no-var": ["error"]
}
}
]
}
@@ -7,11 +7,10 @@ export class FirebaseRedirect {
destination: string;
constructor(readonly rawConfig: FirebaseRedirectConfig) {
const {source, regex, destination} = rawConfig;
this.source = (typeof source === 'string') ?
FirebaseRedirectSource.fromGlobPattern(source) :
FirebaseRedirectSource.fromRegexPattern(regex!);
this.destination = destination;
this.source = (rawConfig.regex === undefined) ?
FirebaseRedirectSource.fromGlobPattern(rawConfig.source) :
FirebaseRedirectSource.fromRegexPattern(rawConfig.regex);
this.destination = rawConfig.destination;
}
replace(url: string): string | undefined {
@@ -344,21 +344,21 @@ function testSource(source: FirebaseRedirectSource, matches: string[], nonMatche
function testGlobMatch(
pattern: string,
captures: { named?: string[], rest?: string[] },
matches: { [url: string]: object|undefined }) {
matches: { [url: string]: unknown|undefined }) {
return testSourceMatch(FirebaseRedirectSource.fromGlobPattern(pattern), captures, matches);
}
function testRegexMatch(
pattern: string,
captures: { named?: string[], rest?: string[] },
matches: { [url: string]: object|undefined }) {
matches: { [url: string]: unknown|undefined }) {
return testSourceMatch(FirebaseRedirectSource.fromRegexPattern(pattern), captures, matches);
}
function testSourceMatch(
source: FirebaseRedirectSource,
captures: { named?: string[], rest?: string[] },
matches: { [url: string]: object|undefined }) {
matches: { [url: string]: unknown|undefined }) {
expect(source.namedGroups).toEqual(captures.named || []);
expect(source.restNamedGroups).toEqual(captures.rest || []);
Object.keys(matches).forEach(url => expect(source.match(url)).toEqual(matches[url]));
@@ -40,7 +40,7 @@ export class FirebaseRedirectSource {
restNamedGroups.push(groupName);
return `(?:${leadingSlash}(?<${groupName}>.🐷))?`;
})
.replace(namedParam, `$1(?<$2>[^/]+)`)
.replace(namedParam, '$1(?<$2>[^/]+)')
.replace(doubleStar, '$1.🐷$2') // use the pig to avoid replacing ** in next rule
.replace(star, '[^/]*') // match a single segment
.replace(possiblyEmptyInitialSegments, '(?:.*)')// deal with **/ special cases
@@ -89,6 +89,7 @@ export class FirebaseRedirectSource {
const result: { [key: string]: string } = {};
const names = this.regex.xregexp.captureNames || [];
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
names.forEach(name => result[name] = match.groups![name]);
return result;
}
-87
View File
@@ -1,87 +0,0 @@
{
"rulesDirectory": [
"../node_modules/codelyzer"
],
"rules": {
"class-name": true,
"comment-format": [
true,
"check-space"
],
"curly": true,
"eofline": true,
"forin": true,
"indent": [
true,
"spaces"
],
"label-position": true,
"max-line-length": [
true,
140
],
"member-access": false,
"no-arg": true,
"no-bitwise": true,
"no-console": [
true,
"debug",
"info",
"time",
"timeEnd",
"trace"
],
"no-construct": true,
"no-debugger": true,
"no-duplicate-variable": true,
"no-empty": false,
"no-eval": true,
"no-inferrable-types": true,
"no-string-literal": false,
"no-switch-case-fall-through": true,
"no-trailing-whitespace": true,
"no-unused-expression": true,
"no-var-keyword": true,
"object-literal-sort-keys": false,
"one-line": [
true,
"check-open-brace",
"check-catch",
"check-else",
"check-whitespace"
],
"quotemark": [
true,
"single"
],
"radix": true,
"semicolon": [
true
],
"triple-equals": [
true,
"allow-null-check"
],
"typedef-whitespace": [
true,
{
"call-signature": "nospace",
"index-signature": "nospace",
"parameter": "nospace",
"property-declaration": "nospace",
"variable-declaration": "nospace"
}
],
"variable-name": false,
"whitespace": [
true,
"check-branch",
"check-decl",
"check-operator",
"check-separator",
"check-type"
],
"import-destructuring-spacing": true
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "angular-srcs",
"version": "12.2.2",
"version": "12.2.3",
"private": true,
"description": "Angular - a web framework for modern web apps",
"homepage": "https://github.com/angular/angular",
@@ -26,6 +26,7 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/transform",
"//packages/compiler-cli/src/ngtsc/typecheck/api",
"//packages/compiler-cli/src/ngtsc/typecheck/diagnostics",
"//packages/compiler-cli/src/ngtsc/typecheck/extended/api",
"//packages/compiler-cli/src/ngtsc/util",
"//packages/compiler-cli/src/ngtsc/xi18n",
"@npm//@types/node",
@@ -24,6 +24,7 @@ import {ClassDeclaration, DeclarationNode, Decorator, ReflectionHost, reflectObj
import {ComponentScopeReader, LocalModuleScopeRegistry, TypeCheckScopeRegistry} from '../../scope';
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerFlags, HandlerPrecedence, ResolveResult} from '../../transform';
import {TemplateSourceMapping, TypeCheckContext} from '../../typecheck/api';
import {ExtendedTemplateChecker} from '../../typecheck/extended/api';
import {SubsetOfKeys} from '../../util/src/typescript';
import {Xi18nContext} from '../../xi18n';
@@ -612,6 +613,12 @@ export class ComponentDecoratorHandler implements
meta.template.sourceMapping, meta.template.file, meta.template.errors);
}
extendedTemplateCheck(
component: ts.ClassDeclaration,
extendedTemplateChecker: ExtendedTemplateChecker): ts.Diagnostic[] {
return extendedTemplateChecker.getDiagnosticsForComponent(component);
}
resolve(
node: ClassDeclaration, analysis: Readonly<ComponentAnalysisData>,
symbol: ComponentSymbol): ResolveResult<ComponentResolutionData> {
@@ -37,6 +37,9 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/typecheck",
"//packages/compiler-cli/src/ngtsc/typecheck/api",
"//packages/compiler-cli/src/ngtsc/typecheck/diagnostics",
"//packages/compiler-cli/src/ngtsc/typecheck/extended",
"//packages/compiler-cli/src/ngtsc/typecheck/extended/api",
"//packages/compiler-cli/src/ngtsc/typecheck/extended/src/template_checks/invalid_banana_in_box",
"//packages/compiler-cli/src/ngtsc/util",
"//packages/compiler-cli/src/ngtsc/xi18n",
"@npm//typescript",
@@ -36,6 +36,16 @@ export interface TestOnlyOptions {
tracePerformance?: string;
}
/**
* Internal only options for compiler.
*/
export interface InternalOptions {
/**
* Whether to run all template checks and generate extended template diagnostics.
*/
_extendedTemplateDiagnostics?: boolean;
}
/**
* A merged interface of all of the various Angular compiler options, as well as the standard
* `ts.CompilerOptions`.
@@ -45,4 +55,4 @@ export interface TestOnlyOptions {
export interface NgCompilerOptions extends ts.CompilerOptions, LegacyNgcOptions, BazelAndG3Options,
NgcCompatibilityOptions, StrictTemplateOptions,
TestOnlyOptions, I18nOptions, TargetOptions,
MiscOptions {}
InternalOptions, MiscOptions {}
@@ -30,6 +30,9 @@ import {ivySwitchTransform} from '../../switch';
import {aliasTransformFactory, CompilationMode, declarationTransformFactory, DecoratorHandler, DtsTransformRegistry, ivyTransformFactory, TraitCompiler} from '../../transform';
import {TemplateTypeCheckerImpl} from '../../typecheck';
import {OptimizeFor, TemplateTypeChecker, TypeCheckingConfig} from '../../typecheck/api';
import {ExtendedTemplateCheckerImpl} from '../../typecheck/extended';
import {ExtendedTemplateChecker} from '../../typecheck/extended/api';
import {InvalidBananaInBoxCheck} from '../../typecheck/extended/src/template_checks/invalid_banana_in_box';
import {getSourceFileOrNull, isDtsPath, resolveModuleName, toUnredirectedSourceFile} from '../../util/src/typescript';
import {Xi18nContext} from '../../xi18n';
import {LazyRoute, NgCompilerAdapter, NgCompilerOptions} from '../api';
@@ -52,6 +55,7 @@ interface LazyCompilationState {
refEmitter: ReferenceEmitter;
templateTypeChecker: TemplateTypeChecker;
resourceRegistry: ResourceRegistry;
extendedTemplateChecker: ExtendedTemplateChecker;
}
@@ -316,6 +320,12 @@ export class NgCompiler {
readonly usePoisonedData: boolean,
private livePerfRecorder: ActivePerfRecorder,
) {
if (this.options._extendedTemplateDiagnostics === true &&
this.options.strictTemplates === false) {
throw new Error(
'The \'_extendedTemplateDiagnostics\' option requires \'strictTemplates\' to also be enabled.');
}
this.constructionDiagnostics.push(...this.adapter.constructionDiagnostics);
const incompatibleTypeCheckOptionsDiagnostic = verifyCompatibleTypeCheckOptions(this.options);
if (incompatibleTypeCheckOptionsDiagnostic !== null) {
@@ -425,8 +435,12 @@ export class NgCompiler {
* Get all Angular-related diagnostics for this compilation.
*/
getDiagnostics(): ts.Diagnostic[] {
return this.addMessageTextDetails(
[...this.getNonTemplateDiagnostics(), ...this.getTemplateDiagnostics()]);
const diagnostics: ts.Diagnostic[] = [];
diagnostics.push(...this.getNonTemplateDiagnostics(), ...this.getTemplateDiagnostics());
if (this.options._extendedTemplateDiagnostics) {
diagnostics.push(...this.getExtendedTemplateDiagnostics());
}
return this.addMessageTextDetails(diagnostics);
}
/**
@@ -435,10 +449,29 @@ export class NgCompiler {
* If a `ts.SourceFile` is passed, only diagnostics related to that file are returned.
*/
getDiagnosticsForFile(file: ts.SourceFile, optimizeFor: OptimizeFor): ts.Diagnostic[] {
return this.addMessageTextDetails([
...this.getNonTemplateDiagnostics().filter(diag => diag.file === file),
...this.getTemplateDiagnosticsForFile(file, optimizeFor)
]);
const diagnostics: ts.Diagnostic[] = [];
diagnostics.push(
...this.getNonTemplateDiagnostics().filter(diag => diag.file === file),
...this.getTemplateDiagnosticsForFile(file, optimizeFor));
if (this.options._extendedTemplateDiagnostics) {
diagnostics.push(...this.getExtendedTemplateDiagnostics(file));
}
return this.addMessageTextDetails(diagnostics);
}
/**
* Get all `ts.Diagnostic`s currently available that pertain to the given component.
*/
getDiagnosticsForComponent(component: ts.ClassDeclaration): ts.Diagnostic[] {
const compilation = this.ensureAnalyzed();
const ttc = compilation.templateTypeChecker;
const diagnostics: ts.Diagnostic[] = [];
diagnostics.push(...ttc.getDiagnosticsForComponent(component));
if (this.options._extendedTemplateDiagnostics) {
const extendedTemplateChecker = compilation.extendedTemplateChecker;
diagnostics.push(...extendedTemplateChecker.getDiagnosticsForComponent(component));
}
return this.addMessageTextDetails(diagnostics);
}
/**
@@ -892,6 +925,27 @@ export class NgCompiler {
return this.nonTemplateDiagnostics;
}
/**
* Calls the `extendedTemplateCheck` phase of the trait compiler
* @param sf optional parameter to get diagnostics for a certain file
* or all files in the program if `sf` is undefined
* @returns generated extended template diagnostics
*/
private getExtendedTemplateDiagnostics(sf?: ts.SourceFile): ts.Diagnostic[] {
const diagnostics: ts.Diagnostic[] = [];
const compilation = this.ensureAnalyzed();
const extendedTemplateChecker = compilation.extendedTemplateChecker;
if (sf !== undefined) {
return compilation.traitCompiler.extendedTemplateCheck(sf, extendedTemplateChecker);
}
for (const sf of this.inputProgram.getSourceFiles()) {
diagnostics.push(
...compilation.traitCompiler.extendedTemplateCheck(sf, extendedTemplateChecker));
}
return diagnostics;
}
private makeCompilation(): LazyCompilationState {
const checker = this.inputProgram.getTypeChecker();
@@ -1055,6 +1109,10 @@ export class NgCompiler {
reflector, this.adapter, this.incrementalCompilation, scopeRegistry, typeCheckScopeRegistry,
this.delegatingPerfRecorder);
const templateChecks = [new InvalidBananaInBoxCheck()];
const extendedTemplateChecker =
new ExtendedTemplateCheckerImpl(templateTypeChecker, checker, templateChecks);
return {
isCore,
traitCompiler,
@@ -1069,6 +1127,7 @@ export class NgCompiler {
refEmitter,
templateTypeChecker,
resourceRegistry,
extendedTemplateChecker
};
}
}
@@ -19,6 +19,7 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/reflection",
"//packages/compiler-cli/src/ngtsc/translator",
"//packages/compiler-cli/src/ngtsc/typecheck/api",
"//packages/compiler-cli/src/ngtsc/typecheck/extended/api",
"//packages/compiler-cli/src/ngtsc/util",
"//packages/compiler-cli/src/ngtsc/xi18n",
"@npm//typescript",
@@ -15,6 +15,7 @@ import {IndexingContext} from '../../indexer';
import {ClassDeclaration, Decorator} from '../../reflection';
import {ImportManager} from '../../translator';
import {TypeCheckContext} from '../../typecheck/api';
import {ExtendedTemplateChecker} from '../../typecheck/extended/api';
import {Xi18nContext} from '../../xi18n';
/**
@@ -187,6 +188,10 @@ export interface DecoratorHandler<D, A, S extends SemanticSymbol|null, R> {
(ctx: TypeCheckContext, node: ClassDeclaration, analysis: Readonly<A>,
resolution: Readonly<R>): void;
extendedTemplateCheck?
(component: ts.ClassDeclaration, extendedTemplateChecker: ExtendedTemplateChecker):
ts.Diagnostic[];
/**
* Generate a description of the field which should be added to the class, including any
* initialization code to be generated.
@@ -14,8 +14,9 @@ import {IncrementalBuild} from '../../incremental/api';
import {SemanticDepGraphUpdater, SemanticSymbol} from '../../incremental/semantic_graph';
import {IndexingContext} from '../../indexer';
import {PerfEvent, PerfRecorder} from '../../perf';
import {ClassDeclaration, DeclarationNode, Decorator, ReflectionHost} from '../../reflection';
import {ClassDeclaration, DeclarationNode, Decorator, isNamedClassDeclaration, ReflectionHost} from '../../reflection';
import {ProgramTypeCheckAdapter, TypeCheckContext} from '../../typecheck/api';
import {ExtendedTemplateChecker} from '../../typecheck/extended/api';
import {getSourceFile, isExported} from '../../util/src/typescript';
import {Xi18nContext} from '../../xi18n';
@@ -490,6 +491,29 @@ export class TraitCompiler implements ProgramTypeCheckAdapter {
}
}
extendedTemplateCheck(sf: ts.SourceFile, extendedTemplateChecker: ExtendedTemplateChecker):
ts.Diagnostic[] {
const classes = this.fileToClasses.get(sf);
if (classes === undefined) {
return [];
}
const diagnostics: ts.Diagnostic[] = [];
for (const clazz of classes) {
if (!isNamedClassDeclaration(clazz)) {
continue;
}
const record = this.classes.get(clazz)!;
for (const trait of record.traits) {
if (trait.handler.extendedTemplateCheck === undefined) {
continue;
}
diagnostics.push(...trait.handler.extendedTemplateCheck(clazz, extendedTemplateChecker));
}
}
return diagnostics;
}
index(ctx: IndexingContext): void {
for (const clazz of this.classes.keys()) {
const record = this.classes.get(clazz)!;
@@ -8,6 +8,7 @@
import {AbsoluteSourceSpan, BoundTarget, DirectiveMeta, ParseSourceSpan, SchemaMetadata} from '@angular/compiler';
import * as ts from 'typescript';
import {ErrorCode} from '../../diagnostics';
import {AbsoluteFsPath} from '../../file_system';
import {Reference} from '../../imports';
@@ -28,6 +29,27 @@ export interface TypeCheckableDirectiveMeta extends DirectiveMeta, DirectiveType
export type TemplateId = string&{__brand: 'TemplateId'};
/**
* A `ts.Diagnostic` with additional information about the diagnostic related to template
* type-checking.
*/
export interface TemplateDiagnostic extends ts.Diagnostic {
/**
* The component with the template that resulted in this diagnostic.
*/
componentFile: ts.SourceFile;
/**
* The template id of the component that resulted in this diagnostic.
*/
templateId: TemplateId;
}
/**
* A `TemplateDiagnostic` with a specific error code.
*/
export type NgTemplateDiagnostic<T extends ErrorCode> = TemplateDiagnostic&{__ngCode: T};
/**
* Metadata required in addition to a component class in order to generate a type check block (TCB)
* for that component.
@@ -12,7 +12,7 @@ import {TextAttribute} from '@angular/compiler/src/render3/r3_ast';
import * as ts from 'typescript';
import {ErrorCode} from '../../diagnostics';
import {FullTemplateMapping, TypeCheckableDirectiveMeta} from './api';
import {FullTemplateMapping, NgTemplateDiagnostic, TypeCheckableDirectiveMeta} from './api';
import {GlobalCompletion} from './completion';
import {DirectiveInScope, PipeInScope} from './scope';
import {DirectiveSymbol, ElementSymbol, ShimLocation, Symbol, TemplateSymbol} from './symbols';
@@ -175,7 +175,7 @@ export interface TemplateTypeChecker {
start: number,
end: number,
sourceFile: ts.SourceFile,
}[]): ts.Diagnostic;
}[]): NgTemplateDiagnostic<T>;
}
/**
@@ -9,23 +9,7 @@
import {ParseSourceSpan} from '@angular/compiler';
import * as ts from 'typescript';
import {ExternalTemplateSourceMapping, TemplateId, TemplateSourceMapping} from '../../api';
/**
* A `ts.Diagnostic` with additional information about the diagnostic related to template
* type-checking.
*/
export interface TemplateDiagnostic extends ts.Diagnostic {
/**
* The component with the template that resulted in this diagnostic.
*/
componentFile: ts.SourceFile;
/**
* The template id of the component that resulted in this diagnostic.
*/
templateId: TemplateId;
}
import {ExternalTemplateSourceMapping, TemplateDiagnostic, TemplateId, TemplateSourceMapping} from '../../api';
/**
* Constructs a `ts.Diagnostic` for a given `ParseSourceSpan` within a template.
@@ -10,12 +10,7 @@ import {TmplAstNode} from '@angular/compiler';
import * as ts from 'typescript';
import {ErrorCode} from '../../../diagnostics';
import {TemplateTypeChecker} from '../../api';
/**
* A `ts.Diangostic` with a specific error code.
*/
export type TemplateDiagnostic<T extends ErrorCode> = ts.Diagnostic&{code: T};
import {NgTemplateDiagnostic, TemplateTypeChecker} from '../../api';
/**
* A Template Check receives information about the template it's checking and returns
@@ -26,7 +21,7 @@ export interface TemplateCheck<T extends ErrorCode> {
code: T;
/** Runs check and returns information about the diagnostics to be generated. */
run(ctx: TemplateContext, template: TmplAstNode[]): TemplateDiagnostic<T>[];
run(ctx: TemplateContext, template: TmplAstNode[]): NgTemplateDiagnostic<T>[];
}
/**
@@ -0,0 +1,20 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as ts from 'typescript';
import {TemplateDiagnostic} from '../../api';
/**
* Interface to generate extended template diangostics from the component tempaltes.
*/
export interface ExtendedTemplateChecker {
/**
* Run `TemplateCheck`s for a component and return the generated `ts.Diagnostic`s.
*/
getDiagnosticsForComponent(component: ts.ClassDeclaration): TemplateDiagnostic[];
}
@@ -0,0 +1,10 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
export * from './api';
export * from './extended_template_checker';
@@ -0,0 +1,9 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
export {ExtendedTemplateCheckerImpl} from './src/extended_template_checker';
@@ -7,39 +7,42 @@
*/
import * as ts from 'typescript';
import {ErrorCode} from '../../../diagnostics';
import {TemplateTypeChecker} from '../../api';
import {TemplateCheck, TemplateContext} from '../api/api';
import {TemplateDiagnostic, TemplateTypeChecker} from '../../api';
import {ExtendedTemplateChecker, TemplateCheck, TemplateContext} from '../api';
/**
* Run all `TemplateChecks` for a component and return the generated `ts.Diagnostic`s.
* @param component the `@Component()` class from which the template is obtained
* @param templateTypeChecker interface to get information about template nodes
* @param typeChecker program's type checker
* @param templateChecks specific checks to be run
* @returns generated `ts.Diagnostic[]`
*/
export function getExtendedTemplateDiagnosticsForComponent(
component: ts.ClassDeclaration, templateTypeChecker: TemplateTypeChecker,
typeChecker: ts.TypeChecker, templateChecks: TemplateCheck<ErrorCode>[]): ts.Diagnostic[] {
const template = templateTypeChecker.getTemplate(component);
// Skip checks if component has no template. This can happen if the user writes a
// `@Component()` but doesn't add the template, could happen in the language service
// when users are in the middle of typing code.
if (template === null) {
return [];
export class ExtendedTemplateCheckerImpl implements ExtendedTemplateChecker {
constructor(
private readonly templateTypeChecker: TemplateTypeChecker,
private readonly typeChecker: ts.TypeChecker,
private readonly templateChecks: TemplateCheck<ErrorCode>[]) {}
getDiagnosticsForComponent(component: ts.ClassDeclaration): TemplateDiagnostic[] {
const template = this.templateTypeChecker.getTemplate(component);
// Skip checks if component has no template. This can happen if the user writes a
// `@Component()` but doesn't add the template, could happen in the language service
// when users are in the middle of typing code.
if (template === null) {
return [];
}
const diagnostics: TemplateDiagnostic[] = [];
const ctx = {
templateTypeChecker: this.templateTypeChecker,
typeChecker: this.typeChecker,
component
} as TemplateContext;
for (const check of this.templateChecks) {
diagnostics.push(...deduplicateDiagnostics(check.run(ctx, template)));
}
return diagnostics;
}
const diagnostics: ts.Diagnostic[] = [];
const ctx = {templateTypeChecker, typeChecker, component} as TemplateContext;
for (const check of templateChecks) {
diagnostics.push(...deduplicateDiagnostics(check.run(ctx, template)));
}
return diagnostics;
}
// Filter out duplicated diagnostics, this is possible due to the way the compiler
// handles desugaring and produces `AST`s. Ex.
//
@@ -75,8 +78,8 @@ export function getExtendedTemplateDiagnosticsForComponent(
// TODO(danieltrevino): handle duplicated diagnostics when they are being generated
// to avoid extra work (could be directly in the visitor).
// https://github.com/angular/angular/pull/42984#discussion_r684823926
function deduplicateDiagnostics(diagnostics: ts.Diagnostic[]): ts.Diagnostic[] {
const result: ts.Diagnostic[] = [];
function deduplicateDiagnostics<T extends ts.Diagnostic>(diagnostics: T[]): T[] {
const result: T[] = [];
for (const newDiag of diagnostics) {
const isDuplicateDiag = result.some(existingDiag => areDiagnosticsEqual(newDiag, existingDiag));
if (!isDuplicateDiag) {
@@ -3,10 +3,11 @@ load("//tools:defaults.bzl", "ts_library")
ts_library(
name = "invalid_banana_in_box",
srcs = ["index.ts"],
visibility = ["//packages/compiler-cli/src/ngtsc/typecheck/extended:__subpackages__"],
visibility = ["//packages/compiler-cli/src/ngtsc:__subpackages__"],
deps = [
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/diagnostics",
"//packages/compiler-cli/src/ngtsc/typecheck/api",
"//packages/compiler-cli/src/ngtsc/typecheck/extended/api",
"@npm//typescript",
],
@@ -8,8 +8,10 @@
import {TmplAstBoundEvent, TmplAstNode, TmplAstRecursiveVisitor} from '@angular/compiler';
import * as ts from 'typescript';
import {ErrorCode} from '../../../../../diagnostics';
import {TemplateCheck, TemplateContext, TemplateDiagnostic} from '../../../api/api';
import {NgTemplateDiagnostic} from '../../../../api';
import {TemplateCheck, TemplateContext} from '../../../api';
/**
* Ensures the two-way binding syntax is correct.
@@ -20,7 +22,7 @@ export class InvalidBananaInBoxCheck implements TemplateCheck<ErrorCode.INVALID_
code: ErrorCode.INVALID_BANANA_IN_BOX = 8101;
run(ctx: TemplateContext,
template: TmplAstNode[]): TemplateDiagnostic<ErrorCode.INVALID_BANANA_IN_BOX>[] {
template: TmplAstNode[]): NgTemplateDiagnostic<ErrorCode.INVALID_BANANA_IN_BOX>[] {
const visitor = new BananaVisitor(ctx);
return visitor.getDiagnostics(template);
@@ -28,7 +30,7 @@ export class InvalidBananaInBoxCheck implements TemplateCheck<ErrorCode.INVALID_
}
class BananaVisitor extends TmplAstRecursiveVisitor {
private diagnostics: ts.Diagnostic[] = [];
private diagnostics: NgTemplateDiagnostic<ErrorCode.INVALID_BANANA_IN_BOX>[] = [];
constructor(public readonly ctx: TemplateContext) {
super();
@@ -45,17 +47,16 @@ class BananaVisitor extends TmplAstRecursiveVisitor {
if (name.startsWith('[') && name.endsWith(']')) {
const boundSyntax = boundEvent.sourceSpan.toString();
const expectedBoundSyntax = boundSyntax.replace(`(${name})`, `[(${name.slice(1, -1)})]`);
this.diagnostics.push(
this.ctx.templateTypeChecker.makeTemplateDiagnostic<ErrorCode.INVALID_BANANA_IN_BOX>(
this.ctx.component, boundEvent.sourceSpan, ts.DiagnosticCategory.Warning,
ErrorCode.INVALID_BANANA_IN_BOX,
`In the two-way binding syntax the parentheses should be inside the brackets, ex. '${
expectedBoundSyntax}'.
this.diagnostics.push(this.ctx.templateTypeChecker.makeTemplateDiagnostic(
this.ctx.component, boundEvent.sourceSpan, ts.DiagnosticCategory.Warning,
ErrorCode.INVALID_BANANA_IN_BOX,
`In the two-way binding syntax the parentheses should be inside the brackets, ex. '${
expectedBoundSyntax}'.
Find more at https://angular.io/guide/two-way-binding`));
}
}
getDiagnostics(template: TmplAstNode[]): ts.Diagnostic[] {
getDiagnostics(template: TmplAstNode[]): NgTemplateDiagnostic<ErrorCode.INVALID_BANANA_IN_BOX>[] {
this.diagnostics = [];
for (const node of template) {
node.visit(this);
@@ -7,12 +7,12 @@
*/
import * as ts from 'typescript';
import {ErrorCode} from '../../../../../diagnostics';
import {ErrorCode, ngErrorCode} from '../../../../../diagnostics';
import {absoluteFrom, getSourceFileOrError} from '../../../../../file_system';
import {runInEachFileSystem} from '../../../../../file_system/testing';
import {getSourceCodeForDiagnostic} from '../../../../../testing';
import {getClass, setup} from '../../../../testing';
import {getExtendedTemplateDiagnosticsForComponent} from '../../../src/template_checker';
import {ExtendedTemplateCheckerImpl} from '../../../src/extended_template_checker';
import {InvalidBananaInBoxCheck} from '../../../src/template_checks/invalid_banana_in_box/index';
runInEachFileSystem(() => {
@@ -22,19 +22,19 @@ runInEachFileSystem(() => {
const {program, templateTypeChecker} = setup([{
fileName,
templates: {
'TestCmp': '<div ([input])="var1"> </div>',
'TestCmp': '<div ([notARealThing])="var1"> </div>',
},
source: 'export class TestCmp { var1: string = "text"; }'
}]);
const sf = getSourceFileOrError(program, fileName);
const component = getClass(sf, 'TestCmp');
const diags = getExtendedTemplateDiagnosticsForComponent(
component, templateTypeChecker, program.getTypeChecker(),
[new InvalidBananaInBoxCheck()]);
const extendedTemplateChecker = new ExtendedTemplateCheckerImpl(
templateTypeChecker, program.getTypeChecker(), [new InvalidBananaInBoxCheck()]);
const diags = extendedTemplateChecker.getDiagnosticsForComponent(component);
expect(diags.length).toBe(1);
expect(diags[0].category).toBe(ts.DiagnosticCategory.Warning);
expect(diags[0].code).toBe(ErrorCode.INVALID_BANANA_IN_BOX);
expect(getSourceCodeForDiagnostic(diags[0])).toBe('([input])="var1"');
expect(diags[0].code).toBe(ngErrorCode(ErrorCode.INVALID_BANANA_IN_BOX));
expect(getSourceCodeForDiagnostic(diags[0])).toBe('([notARealThing])="var1"');
});
it('should not produce invalid banana in a box warning if written correctly', () => {
@@ -42,15 +42,15 @@ runInEachFileSystem(() => {
const {program, templateTypeChecker} = setup([{
fileName,
templates: {
'TestCmp': '<div [(input)]="var1"> </div>',
'TestCmp': '<div [(notARealThing)]="var1"> </div>',
},
source: 'export class TestCmp { var1: string = "text"; }'
}]);
const sf = getSourceFileOrError(program, fileName);
const component = getClass(sf, 'TestCmp');
const diags = getExtendedTemplateDiagnosticsForComponent(
component, templateTypeChecker, program.getTypeChecker(),
[new InvalidBananaInBoxCheck()]);
const extendedTemplateChecker = new ExtendedTemplateCheckerImpl(
templateTypeChecker, program.getTypeChecker(), [new InvalidBananaInBoxCheck()]);
const diags = extendedTemplateChecker.getDiagnosticsForComponent(component);
expect(diags.length).toBe(0);
});
@@ -60,15 +60,15 @@ runInEachFileSystem(() => {
const {program, templateTypeChecker} = setup([{
fileName,
templates: {
'TestCmp': '<div (abc[123]def)="var1"> </div>',
'TestCmp': '<div (not[AReal]Thing)="var1"> </div>',
},
source: 'export class TestCmp { var1: string = "text"; }'
}]);
const sf = getSourceFileOrError(program, fileName);
const component = getClass(sf, 'TestCmp');
const diags = getExtendedTemplateDiagnosticsForComponent(
component, templateTypeChecker, program.getTypeChecker(),
[new InvalidBananaInBoxCheck()]);
const extendedTemplateChecker = new ExtendedTemplateCheckerImpl(
templateTypeChecker, program.getTypeChecker(), [new InvalidBananaInBoxCheck()]);
const diags = extendedTemplateChecker.getDiagnosticsForComponent(component);
expect(diags.length).toBe(0);
});
@@ -78,8 +78,8 @@ runInEachFileSystem(() => {
fileName,
templates: {
'TestCmp': `<div>
<div *ngIf="false" ([foo])="var1"> </div>
<ng-template #elseBlock ([bar])="var1">Content to render when condition is false.</ng-template>
<div *ngIf="false" ([notARealThing])="var1"> </div>
<ng-template #elseBlock ([notARealThing2])="var1">Content to render when condition is false.</ng-template>
</div>`,
},
source: `export class TestCmp {
@@ -88,16 +88,16 @@ runInEachFileSystem(() => {
}]);
const sf = getSourceFileOrError(program, fileName);
const component = getClass(sf, 'TestCmp');
const diags = getExtendedTemplateDiagnosticsForComponent(
component, templateTypeChecker, program.getTypeChecker(),
[new InvalidBananaInBoxCheck()]);
const extendedTemplateChecker = new ExtendedTemplateCheckerImpl(
templateTypeChecker, program.getTypeChecker(), [new InvalidBananaInBoxCheck()]);
const diags = extendedTemplateChecker.getDiagnosticsForComponent(component);
expect(diags.length).toBe(2);
expect(diags[0].category).toBe(ts.DiagnosticCategory.Warning);
expect(diags[0].code).toBe(ErrorCode.INVALID_BANANA_IN_BOX);
expect(getSourceCodeForDiagnostic(diags[0])).toBe('([foo])="var1"');
expect(diags[0].code).toBe(ngErrorCode(ErrorCode.INVALID_BANANA_IN_BOX));
expect(getSourceCodeForDiagnostic(diags[0])).toBe('([notARealThing])="var1"');
expect(diags[1].category).toBe(ts.DiagnosticCategory.Warning);
expect(diags[1].code).toBe(ErrorCode.INVALID_BANANA_IN_BOX);
expect(getSourceCodeForDiagnostic(diags[1])).toBe('([bar])="var1"');
expect(diags[1].code).toBe(ngErrorCode(ErrorCode.INVALID_BANANA_IN_BOX));
expect(getSourceCodeForDiagnostic(diags[1])).toBe('([notARealThing2])="var1"');
});
});
});
@@ -9,7 +9,7 @@
import {AST, CssSelector, DomElementSchemaRegistry, LiteralPrimitive, MethodCall, ParseError, ParseSourceSpan, parseTemplate, PropertyRead, SafeMethodCall, SafePropertyRead, TmplAstElement, TmplAstNode, TmplAstReference, TmplAstTemplate, TmplAstVariable} from '@angular/compiler';
import {TextAttribute} from '@angular/compiler/src/render3/r3_ast';
import * as ts from 'typescript';
import {ErrorCode} from '../../diagnostics';
import {ErrorCode, ngErrorCode} from '../../diagnostics';
import {absoluteFrom, absoluteFromSourceFile, AbsoluteFsPath, getSourceFileOrError} from '../../file_system';
import {Reference, ReferenceEmitter} from '../../imports';
@@ -20,8 +20,8 @@ import {ClassDeclaration, isNamedClassDeclaration, ReflectionHost} from '../../r
import {ComponentScopeReader, TypeCheckScopeRegistry} from '../../scope';
import {isShim} from '../../shims';
import {getSourceFileOrNull, isSymbolWithValueDeclaration} from '../../util/src/typescript';
import {DirectiveInScope, ElementSymbol, FullTemplateMapping, GlobalCompletion, OptimizeFor, PipeInScope, ProgramTypeCheckAdapter, ShimLocation, Symbol, TemplateId, TemplateSymbol, TemplateTypeChecker, TypeCheckableDirectiveMeta, TypeCheckingConfig} from '../api';
import {makeTemplateDiagnostic, TemplateDiagnostic} from '../diagnostics';
import {DirectiveInScope, ElementSymbol, FullTemplateMapping, GlobalCompletion, NgTemplateDiagnostic, OptimizeFor, PipeInScope, ProgramTypeCheckAdapter, ShimLocation, Symbol, TemplateDiagnostic, TemplateId, TemplateSymbol, TemplateTypeChecker, TypeCheckableDirectiveMeta, TypeCheckingConfig} from '../api';
import {makeTemplateDiagnostic} from '../diagnostics';
import {CompletionEngine} from './completion';
import {InliningMode, ShimTypeCheckingData, TemplateData, TypeCheckContextImpl, TypeCheckingHost} from './context';
@@ -316,14 +316,18 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
start: number,
end: number,
sourceFile: ts.SourceFile,
}[]): TemplateDiagnostic {
}[]): NgTemplateDiagnostic<T> {
const sfPath = absoluteFromSourceFile(clazz.getSourceFile());
const fileRecord = this.state.get(sfPath)!;
const templateId = fileRecord.sourceManager.getTemplateId(clazz);
const mapping = fileRecord.sourceManager.getSourceMapping(templateId);
return makeTemplateDiagnostic(
templateId, mapping, sourceSpan, category, errorCode, message, relatedInformation);
return {
...makeTemplateDiagnostic(
templateId, mapping, sourceSpan, category, ngErrorCode(errorCode), message,
relatedInformation),
__ngCode: errorCode
};
}
private getOrCreateCompletionEngine(component: ts.ClassDeclaration): CompletionEngine|null {
@@ -16,8 +16,8 @@ import {PerfEvent, PerfRecorder} from '../../perf';
import {FileUpdate} from '../../program_driver';
import {ClassDeclaration, ReflectionHost} from '../../reflection';
import {ImportManager} from '../../translator';
import {TemplateId, TemplateSourceMapping, TypeCheckableDirectiveMeta, TypeCheckBlockMetadata, TypeCheckContext, TypeCheckingConfig, TypeCtorMetadata} from '../api';
import {makeTemplateDiagnostic, TemplateDiagnostic} from '../diagnostics';
import {TemplateDiagnostic, TemplateId, TemplateSourceMapping, TypeCheckableDirectiveMeta, TypeCheckBlockMetadata, TypeCheckContext, TypeCheckingConfig, TypeCtorMetadata} from '../api';
import {makeTemplateDiagnostic} from '../diagnostics';
import {DomSchemaChecker, RegistryDomSchemaChecker} from './dom';
import {Environment} from './environment';
@@ -7,8 +7,8 @@
*/
import {AbsoluteSourceSpan, ParseSourceSpan} from '@angular/compiler';
import * as ts from 'typescript';
import {TemplateId} from '../api';
import {makeTemplateDiagnostic, TemplateDiagnostic} from '../diagnostics';
import {TemplateDiagnostic, TemplateId} from '../api';
import {makeTemplateDiagnostic} from '../diagnostics';
import {getTemplateMapping, TemplateSourceResolver} from './tcb_util';
@@ -10,8 +10,8 @@ import {DomElementSchemaRegistry, ParseSourceSpan, SchemaMetadata, TmplAstElemen
import * as ts from 'typescript';
import {ErrorCode, ngErrorCode} from '../../diagnostics';
import {TemplateId} from '../api';
import {makeTemplateDiagnostic, TemplateDiagnostic} from '../diagnostics';
import {TemplateDiagnostic, TemplateId} from '../api';
import {makeTemplateDiagnostic} from '../diagnostics';
import {TemplateSourceResolver} from './tcb_util';
@@ -12,8 +12,8 @@ import * as ts from 'typescript';
import {ErrorCode, makeDiagnostic, makeRelatedInformation, ngErrorCode} from '../../diagnostics';
import {ClassDeclaration} from '../../reflection';
import {TemplateId} from '../api';
import {makeTemplateDiagnostic, TemplateDiagnostic} from '../diagnostics';
import {TemplateDiagnostic, TemplateId} from '../api';
import {makeTemplateDiagnostic} from '../diagnostics';
import {TemplateSourceResolver} from './tcb_util';
@@ -21,7 +21,6 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/testing",
"//packages/compiler-cli/src/ngtsc/typecheck",
"//packages/compiler-cli/src/ngtsc/typecheck/api",
"//packages/compiler-cli/src/ngtsc/typecheck/diagnostics",
"//packages/compiler-cli/src/ngtsc/util",
"@npm//typescript",
],
@@ -20,9 +20,8 @@ import {ClassDeclaration, isNamedClassDeclaration, TypeScriptReflectionHost} fro
import {ComponentScopeReader, LocalModuleScope, ScopeData, TypeCheckScopeRegistry} from '../../scope';
import {makeProgram} from '../../testing';
import {getRootDirs} from '../../util/src/typescript';
import {ProgramTypeCheckAdapter, TemplateTypeChecker, TypeCheckContext} from '../api';
import {ProgramTypeCheckAdapter, TemplateDiagnostic, TemplateTypeChecker, TypeCheckContext} from '../api';
import {TemplateId, TemplateSourceMapping, TypeCheckableDirectiveMeta, TypeCheckBlockMetadata, TypeCheckingConfig} from '../api/api';
import {TemplateDiagnostic} from '../diagnostics';
import {TemplateTypeCheckerImpl} from '../src/checker';
import {DomSchemaChecker} from '../src/dom';
import {Environment} from '../src/environment';
@@ -0,0 +1,107 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as ts from 'typescript';
import {ErrorCode, ngErrorCode} from '../../src/ngtsc/diagnostics';
import {runInEachFileSystem} from '../../src/ngtsc/file_system/testing';
import {getSourceCodeForDiagnostic, loadStandardTestFiles} from '../../src/ngtsc/testing';
import {NgtscTestEnvironment} from './env';
const testFiles = loadStandardTestFiles({fakeCore: true, fakeCommon: true});
runInEachFileSystem(() => {
describe('ngtsc extended template checks', () => {
let env!: NgtscTestEnvironment;
beforeEach(() => {
env = NgtscTestEnvironment.setup(testFiles);
env.tsconfig({_extendedTemplateDiagnostics: true, strictTemplates: true});
});
it('should produce invalid banana in box warning', () => {
env.write('test.ts', `
import {Component} from '@angular/core';
@Component({
selector: 'test',
template: '<div ([notARealThing])="bar"></div>',
})
class TestCmp {
bar: string = "text";
}
`);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(1);
expect(diags[0].category).toBe(ts.DiagnosticCategory.Warning);
expect(diags[0].code).toBe(ngErrorCode(ErrorCode.INVALID_BANANA_IN_BOX));
expect(getSourceCodeForDiagnostic(diags[0])).toBe('([notARealThing])="bar"');
});
it('should produce invalid banana in box warning with external html file', () => {
env.write('test.ts', `
import {Component} from '@angular/core';
@Component({
selector: 'test',
templateUrl: './test.html',
})
class TestCmp {
bar: string = "text";
}
`);
env.write('test.html', `
<div ([notARealThing])="bar"></div>
`);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(1);
expect(diags[0].category).toBe(ts.DiagnosticCategory.Warning);
expect(diags[0].code).toBe(ngErrorCode(ErrorCode.INVALID_BANANA_IN_BOX));
expect(getSourceCodeForDiagnostic(diags[0])).toBe('([notARealThing])="bar"');
});
it('should not produce extended diagnostics if flag is disabled', () => {
env.tsconfig({_extendedTemplateDiagnostics: false});
env.write('test.ts', `
import {Component} from '@angular/core';
@Component({
selector: 'test',
template: '<div ([notARealThing])="bar"></div>',
})
class TestCmp {
bar: string = "text";
}
`);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(0);
});
it('should throw error if _extendedTemplateDiagnostics option is enabled and strictTemplates disabled',
() => {
env.tsconfig({_extendedTemplateDiagnostics: true, strictTemplates: false});
env.write('test.ts', `
import {Component} from '@angular/core';
@Component({
selector: 'test',
template: '<div ([notARealThing])="bar"></div>',
})
class TestCmp {
bar: string = "text";
}
`);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(1);
expect(diags[0].messageText)
.toMatch(
/Error: The '_extendedTemplateDiagnostics' option requires 'strictTemplates' to also be enabled./);
});
});
});
@@ -68,7 +68,6 @@ export class LanguageService {
getSemanticDiagnostics(fileName: string): ts.Diagnostic[] {
return this.withCompilerAndPerfTracing(PerfPhase.LsDiagnostics, (compiler) => {
const ttc = compiler.getTemplateTypeChecker();
const diagnostics: ts.Diagnostic[] = [];
if (isTypeScriptFile(fileName)) {
const program = compiler.getCurrentProgram();
@@ -104,7 +103,7 @@ export class LanguageService {
const components = compiler.getComponentsWithTemplateFile(fileName);
for (const component of components) {
if (ts.isClassDeclaration(component)) {
diagnostics.push(...ttc.getDiagnosticsForComponent(component));
diagnostics.push(...compiler.getDiagnosticsForComponent(component));
}
}
}
@@ -361,6 +361,91 @@ describe('getSemanticDiagnostics', () => {
expect(diag.category).toBe(ts.DiagnosticCategory.Error);
expect(getTextOfDiagnostic(diag)).toBe(`'./missing.css'`);
});
it('should produce invalid banana in box warning', () => {
const files = {
'app.ts': `
import {Component} from '@angular/core';
@Component({
selector: 'test',
template: '<div ([notARealThing])="bar"></div>',
})
export class TestCmp {
bar: string = "text";
}
`
};
const project = createModuleAndProjectWithDeclarations(
env, 'test', files, {strictTemplates: true, _extendedTemplateDiagnostics: true});
const diags = project.getDiagnosticsForFile('app.ts');
expect(diags.length).toEqual(1);
expect(diags[0].code).toEqual(ngErrorCode(ErrorCode.INVALID_BANANA_IN_BOX));
expect(diags[0].category).toEqual(ts.DiagnosticCategory.Warning);
});
it('should not produce invalid banana in box warning without the required flags', () => {
const files = {
'app.ts': `
import {Component} from '@angular/core';
@Component({
selector: 'test',
template: '<div ([notARealThing])="bar"></div>',
})
export class TestCmp {
bar: string = "text";
}
`
};
const project = createModuleAndProjectWithDeclarations(env, 'test', files);
const diags = project.getDiagnosticsForFile('app.ts');
expect(diags.length).toEqual(0);
});
it('should produce invalid banana in box warning in external html file', () => {
const files = {
'app.ts': `
import {Component} from '@angular/core';
@Component({
selector: 'test',
templateUrl: './app.html',
})
export class TestCmp {
bar: string = "text";
}
`,
'app.html': `<div ([foo])="bar"></div>`
};
const project = createModuleAndProjectWithDeclarations(
env, 'test', files, {strictTemplates: true, _extendedTemplateDiagnostics: true});
const diags = project.getDiagnosticsForFile('app.html');
expect(diags.length).toEqual(1);
expect(diags[0].code).toEqual(ngErrorCode(ErrorCode.INVALID_BANANA_IN_BOX));
expect(diags[0].category).toEqual(ts.DiagnosticCategory.Warning);
});
it('should not produce invalid banana in box warning in external html file without the required flags',
() => {
const files = {
'app.ts': `
import {Component} from '@angular/core';
@Component({
selector: 'test',
templateUrl: './app.html',
})
export class TestCmp {
bar: string = "text";
}
`,
'app.html': `<div ([foo])="bar"></div>`
};
const project = createModuleAndProjectWithDeclarations(env, 'test', files);
const diags = project.getDiagnosticsForFile('app.html');
expect(diags.length).toEqual(0);
});
});
function getTextOfDiagnostic(diag: ts.Diagnostic): string {
@@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {LegacyNgcOptions, StrictTemplateOptions} from '@angular/compiler-cli/src/ngtsc/core/api';
import {InternalOptions, LegacyNgcOptions, StrictTemplateOptions} from '@angular/compiler-cli/src/ngtsc/core/api';
import {absoluteFrom, AbsoluteFsPath, FileSystem, getFileSystem, getSourceFileOrError} from '@angular/compiler-cli/src/ngtsc/file_system';
import {OptimizeFor, TemplateTypeChecker} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
import * as ts from 'typescript/lib/tsserverlibrary';
@@ -44,7 +44,8 @@ function writeTsconfig(
null, 2));
}
export type TestableOptions = StrictTemplateOptions&Pick<LegacyNgcOptions, 'fullTemplateTypeCheck'>;
export type TestableOptions =
StrictTemplateOptions&InternalOptions&Pick<LegacyNgcOptions, 'fullTemplateTypeCheck'>;
export class Project {
private tsProject: ts.server.Project;
+1 -1
View File
@@ -354,7 +354,7 @@ export class Driver implements Debuggable, UpdateSource {
const notificationAction = action === '' || action === undefined ? 'default' : action;
const onActionClick = notification?.data?.onActionClick[notificationAction];
const onActionClick = notification?.data?.onActionClick?.[notificationAction];
const urlToOpen = new URL(onActionClick?.url ?? '', this.scope.registration.scope).href;
@@ -1055,6 +1055,21 @@ describe('Driver', () => {
});
});
describe('no onActionClick field', () => {
it('has no client interaction', async () => {
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
spyOn(scope.clients, 'openWindow');
await driver.initialized;
await scope.handleClick(
{title: 'This is a test without action', body: 'Test body without action', data: {}});
await scope.handleClick(
{title: 'This is a test with an action', body: 'Test body with an action', data: {}},
'someAction');
expect(scope.clients.openWindow).not.toHaveBeenCalled();
});
});
describe('URL resolution', () => {
it('should resolve relative to service worker scope', async () => {
(scope.registration.scope as string) = 'http://localhost/foo/bar/';