mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b8183614f5 | |||
| b46ce7d42f | |||
| 36bf0ae985 | |||
| 3b317dcf87 | |||
| 3ad527d86e | |||
| b6ff762e36 | |||
| e196311e5c | |||
| 3cf8f66f3a | |||
| d7b6416fcf | |||
| fc7f92159d | |||
| cb8e51f8cf | |||
| e6b2bedd70 | |||
| 58a19a3021 | |||
| f70d2a2981 | |||
| 3bbf00fdd4 | |||
| 19fa033f79 | |||
| 513b17043e |
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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';
|
||||
+33
-30
@@ -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) {
|
||||
+2
-1
@@ -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",
|
||||
],
|
||||
|
||||
+11
-10
@@ -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);
|
||||
|
||||
+25
-25
@@ -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;
|
||||
|
||||
@@ -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/';
|
||||
|
||||
Reference in New Issue
Block a user