Compare commits
48 Commits
zone.js-0.
...
10.1.0-rc.
Author | SHA1 | Date | |
---|---|---|---|
fb06903237 | |||
ecc6fd0d28 | |||
80cab26023 | |||
841dfa68f9 | |||
f0af387f6c | |||
14e90bef58 | |||
db3a21b382 | |||
b8351f3b10 | |||
81053d3160 | |||
bdba1a062d | |||
23f855b300 | |||
94a3e0e81d | |||
9cbde86534 | |||
18e474f522 | |||
cb3db0d31b | |||
d36828a7a1 | |||
f18e2d5898 | |||
375f0a6f67 | |||
281b647f15 | |||
0fc44e0436 | |||
201a546af8 | |||
6e643d9874 | |||
4985267211 | |||
b48cc6ead5 | |||
874792dc43 | |||
e7da4040d6 | |||
2a643e1ab6 | |||
364284b0dc | |||
956b25a100 | |||
8017ca4db3 | |||
22f1ac3e37 | |||
4ee5e730ab | |||
6442875c99 | |||
8f24bc9443 | |||
ac461e1efd | |||
f245c6bb15 | |||
68a9a01a64 | |||
8cd4099db9 | |||
0b54c0c6b4 | |||
1ec609946f | |||
7ad32649c0 | |||
9ad69c1503 | |||
9af2de821c | |||
0270020ac2 | |||
6b662d10c1 | |||
55fd725e74 | |||
f77fd5e02a | |||
63ba74fe4e |
72
CHANGELOG.md
72
CHANGELOG.md
@ -1,3 +1,75 @@
|
||||
<a name="10.1.0-rc.0"></a>
|
||||
# 10.1.0-rc.0 (2020-08-26)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **localize:** ensure required XLIFF parameters are serialized ([#38575](https://github.com/angular/angular/issues/38575)) ([f0af387](https://github.com/angular/angular/commit/f0af387)), closes [#38570](https://github.com/angular/angular/issues/38570)
|
||||
* **localize:** render text of extracted placeholders ([#38536](https://github.com/angular/angular/issues/38536)) ([14e90be](https://github.com/angular/angular/commit/14e90be))
|
||||
|
||||
|
||||
|
||||
<a name="10.0.14"></a>
|
||||
## 10.0.14 (2020-08-26)
|
||||
|
||||
|
||||
|
||||
<a name="10.1.0-next.8"></a>
|
||||
# 10.1.0-next.8 (2020-08-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **localize:** extract the correct message ids ([#38498](https://github.com/angular/angular/issues/38498)) ([ac461e1](https://github.com/angular/angular/commit/ac461e1))
|
||||
* **router:** support lazy loading for empty path named outlets ([#38379](https://github.com/angular/angular/issues/38379)) ([7ad3264](https://github.com/angular/angular/commit/7ad3264)), closes [#12842](https://github.com/angular/angular/issues/12842)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **compiler:** support unary operators for more accurate type checking ([#37918](https://github.com/angular/angular/issues/37918)) ([874792d](https://github.com/angular/angular/commit/874792d)), closes [#20845](https://github.com/angular/angular/issues/20845) [#36178](https://github.com/angular/angular/issues/36178)
|
||||
* **compiler-cli:** add support for TypeScript 4.0 ([#38076](https://github.com/angular/angular/issues/38076)) ([0fc44e0](https://github.com/angular/angular/commit/0fc44e0))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **forms:** use internal `ngDevMode` flag to tree-shake error messages in prod builds ([#37821](https://github.com/angular/angular/issues/37821)) ([201a546](https://github.com/angular/angular/commit/201a546)), closes [#37697](https://github.com/angular/angular/issues/37697)
|
||||
|
||||
|
||||
|
||||
<a name="10.0.12"></a>
|
||||
## 10.0.12 (2020-08-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **compiler-cli:** adding references to const enums in runtime code ([#38542](https://github.com/angular/angular/issues/38542)) ([814b436](https://github.com/angular/angular/commit/814b436)), closes [#38513](https://github.com/angular/angular/issues/38513)
|
||||
* **core:** remove closing body tag from inert DOM builder ([#38454](https://github.com/angular/angular/issues/38454)) ([5528536](https://github.com/angular/angular/commit/5528536))
|
||||
* **localize:** include the last placeholder in parsed translation text ([#38452](https://github.com/angular/angular/issues/38452)) ([57d1a48](https://github.com/angular/angular/commit/57d1a48))
|
||||
* **localize:** parse all parts of a translation with nested HTML ([#38452](https://github.com/angular/angular/issues/38452)) ([07b99f5](https://github.com/angular/angular/commit/07b99f5)), closes [#38422](https://github.com/angular/angular/issues/38422)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **language-service:** introduce hybrid visitor to locate AST node ([#38540](https://github.com/angular/angular/issues/38540)) ([66d8c22](https://github.com/angular/angular/commit/66d8c22))
|
||||
|
||||
|
||||
|
||||
<a name="10.1.0-next.7"></a>
|
||||
# 10.1.0-next.7 (2020-08-19)
|
||||
|
||||
This release contains various API docs improvements.
|
||||
|
||||
|
||||
<a name="10.0.11"></a>
|
||||
## 10.0.11 (2020-08-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **router:** ensure routerLinkActive updates when associated routerLinks change (resubmit of [#38349](https://github.com/angular/angular/issues/38349)) ([#38511](https://github.com/angular/angular/issues/38511)) ([0af9533](https://github.com/angular/angular/commit/0af9533)), closes [#18469](https://github.com/angular/angular/issues/18469)
|
||||
|
||||
|
||||
|
||||
<a name="10.1.0-next.6"></a>
|
||||
# 10.1.0-next.6 (2020-08-17)
|
||||
|
||||
|
@ -17,7 +17,7 @@ An NgModule is defined by a class decorated with `@NgModule()`. The `@NgModule()
|
||||
|
||||
* `imports`: Other modules whose exported classes are needed by component templates declared in *this* NgModule.
|
||||
|
||||
* `providers`: Creators of [services](guide/architecture-services) that this NgModule contributes to the global collection of services; they become accessible in all parts of the app. (You can also specify providers at the component level, which is often preferred.)
|
||||
* `providers`: Creators of [services](guide/architecture-services) that this NgModule contributes to the global collection of services; they become accessible in all parts of the app. (You can also specify providers at the component level.)
|
||||
|
||||
* `bootstrap`: The main application view, called the *root component*, which hosts all other app views. Only the *root NgModule* should set the `bootstrap` property.
|
||||
|
||||
|
@ -154,7 +154,7 @@ Attributes can be changed by `setAttribute()`, which re-initializes correspondin
|
||||
</div>
|
||||
|
||||
For more information, see the [MDN Interfaces documentation](https://developer.mozilla.org/en-US/docs/Web/API#Interfaces) which has API docs for all the standard DOM elements and their properties.
|
||||
Comparing the [`<td>` attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/td) attributes to the [`<td>` properties](https://developer.mozilla.org/en-US/docs/Web/API/HTMLTableCellElement) provides a helpful example for differentiation.
|
||||
Comparing the [`<td>` attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/td) to the [`<td>` properties](https://developer.mozilla.org/en-US/docs/Web/API/HTMLTableCellElement) provides a helpful example for differentiation.
|
||||
In particular, you can navigate from the attributes page to the properties via "DOM interface" link, and navigate the inheritance hierarchy up to `HTMLTableCellElement`.
|
||||
|
||||
|
||||
@ -195,7 +195,7 @@ To control the state of the button, set the `disabled` *property*,
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
Though you could technically set the `[attr.disabled]` attribute binding, the values are different in that the property binding requires to a boolean value, while its corresponding attribute binding relies on whether the value is `null` or not. Consider the following:
|
||||
Though you could technically set the `[attr.disabled]` attribute binding, the values are different in that the property binding requires to be a boolean value, while its corresponding attribute binding relies on whether the value is `null` or not. Consider the following:
|
||||
|
||||
```html
|
||||
<input [disabled]="condition ? true : false">
|
||||
|
@ -12,7 +12,7 @@ Every application has at least one Angular module, the _root_ module,
|
||||
which must be present for bootstrapping the application on launch.
|
||||
By convention and by default, this NgModule is named `AppModule`.
|
||||
|
||||
When you use the [Angular CLI](cli) command `ng new` to generate an app, the default `AppModule` is as follows.
|
||||
When you use the [Angular CLI](cli) command `ng new` to generate an app, the default `AppModule` looks like the following:
|
||||
|
||||
```typescript
|
||||
/* JavaScript imports */
|
||||
@ -90,8 +90,6 @@ A declarable can only belong to one module, so only declare it in
|
||||
one `@NgModule`. When you need it elsewhere,
|
||||
import the module that has the declarable you need in it.
|
||||
|
||||
**Only `@NgModule` references** go in the `imports` array.
|
||||
|
||||
|
||||
### Using directives with `@NgModule`
|
||||
|
||||
@ -133,7 +131,7 @@ The module's `imports` array appears exclusively in the `@NgModule` metadata obj
|
||||
It tells Angular about other NgModules that this particular module needs to function properly.
|
||||
|
||||
This list of modules are those that export components, directives, or pipes
|
||||
that the component templates in this module reference. In this case, the component is
|
||||
that component templates in this module reference. In this case, the component is
|
||||
`AppComponent`, which references components, directives, or pipes in `BrowserModule`,
|
||||
`FormsModule`, or `HttpClientModule`.
|
||||
A component template can reference another component, directive,
|
||||
|
@ -79,7 +79,7 @@ To incorporate the feature module into your app, you have to let the root module
|
||||
<code-example path="feature-modules/src/app/app.module.ts" region="app-module" header="src/app/app.module.ts"></code-example>
|
||||
|
||||
|
||||
Now the `AppModule` knows about the feature module. If you were to add any service providers to the feature module, `AppModule` would know about those too, as would any other feature modules. However, NgModules don’t expose their components.
|
||||
Now the `AppModule` knows about the feature module. If you were to add any service providers to the feature module, `AppModule` would know about those too, as would any other feature modules. However, NgModules don’t expose their components by default.
|
||||
|
||||
|
||||
## Rendering a feature module’s component template
|
||||
|
@ -766,8 +766,10 @@ The HTML `base` tag with the `href` attribute specifies the base URI, or URL, fo
|
||||
"i18n": {
|
||||
"sourceLocale": "en-US",
|
||||
"locales": {
|
||||
"fr": "src/locale/messages.fr.xlf"
|
||||
"baseHref": ""
|
||||
"fr": {
|
||||
"translation": "src/locale/messages.fr.xlf",
|
||||
"baseHref": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"architect": {
|
||||
|
@ -208,7 +208,7 @@ about the event and gives that data to the parent.
|
||||
The child's template has two controls. The first is an HTML `<input>` with a
|
||||
[template reference variable](guide/template-reference-variables) , `#newItem`,
|
||||
where the user types in an item name. Whatever the user types
|
||||
into the `<input>` gets stored in the `#newItem` variable.
|
||||
into the `<input>` gets stored in the `value` property of the `#newItem` variable.
|
||||
|
||||
<code-example path="inputs-outputs/src/app/item-output/item-output.component.html" region="child-output" header="src/app/item-output/item-output.component.html"></code-example>
|
||||
|
||||
@ -218,7 +218,7 @@ an event binding because the part to the left of the equal
|
||||
sign is in parentheses, `(click)`.
|
||||
|
||||
The `(click)` event is bound to the `addNewItem()` method in the child component class which
|
||||
takes as its argument whatever the value of `#newItem` is.
|
||||
takes as its argument whatever the value of `#newItem.value` property is.
|
||||
|
||||
Now the child component has an `@Output()`
|
||||
for sending data to the parent and a method for raising an event.
|
||||
|
@ -101,7 +101,7 @@ should import `BrowserModule` from `@angular/platform-browser`.
|
||||
`BrowserModule` provides services that are essential to launch and run a browser app.
|
||||
|
||||
`BrowserModule` also re-exports `CommonModule` from `@angular/common`,
|
||||
which means that components in the `AppModule` module also have access to
|
||||
which means that components in the `AppModule` also have access to
|
||||
the Angular directives every app needs, such as `NgIf` and `NgFor`.
|
||||
|
||||
Do not import `BrowserModule` in any other module.
|
||||
@ -140,7 +140,7 @@ declared in this NgModule.
|
||||
You _can_ export any declarable class—components, directives, and pipes—whether
|
||||
it's declared in this NgModule or in an imported NgModule.
|
||||
|
||||
You _can_ re-export entire imported NgModules, which effectively re-exports all of their exported classes.
|
||||
You _can_ re-export entire imported NgModules, which effectively re-export all of their exported classes.
|
||||
An NgModule can even export a module that it doesn't import.
|
||||
|
||||
<hr/>
|
||||
@ -192,7 +192,7 @@ Its only purpose is to add http service providers to the application as a whole.
|
||||
|
||||
The `forRoot()` static method is a convention that makes it easy for developers to configure services and providers that are intended to be singletons. A good example of `forRoot()` is the `RouterModule.forRoot()` method.
|
||||
|
||||
Apps pass a `Routes` object to `RouterModule.forRoot()` in order to configure the app-wide `Router` service with routes.
|
||||
Apps pass a `Routes` array to `RouterModule.forRoot()` in order to configure the app-wide `Router` service with routes.
|
||||
`RouterModule.forRoot()` returns a [ModuleWithProviders](api/core/ModuleWithProviders).
|
||||
You add that result to the `imports` list of the root `AppModule`.
|
||||
|
||||
|
@ -36,7 +36,7 @@ NgModule metadata does the following:
|
||||
* Declares which components, directives, and pipes belong to the module.
|
||||
* Makes some of those components, directives, and pipes public so that other module's component templates can use them.
|
||||
* Imports other modules with the components, directives, and pipes that components in the current module need.
|
||||
* Provides services that the other application components can use.
|
||||
* Provides services that other application components can use.
|
||||
|
||||
Every Angular app has at least one module, the root module.
|
||||
You [bootstrap](guide/bootstrapping) that module to launch the application.
|
||||
|
@ -37,9 +37,9 @@ by HTML.
|
||||
|
||||
<code-example path="template-reference-variables/src/app/app.component.html" region="ngForm" header="src/app/hero-form.component.html"></code-example>
|
||||
|
||||
The reference value of itemForm, without the ngForm attribute value, would be
|
||||
The reference value of `itemForm`, without the `ngForm` attribute value, would be
|
||||
the [HTMLFormElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement).
|
||||
There is, however, a difference between a Component and a Directive in that a `Component`
|
||||
There is, however, a difference between a `Component` and a `Directive` in that a `Component`
|
||||
will be referenced without specifying the attribute value, and a `Directive` will not
|
||||
change the implicit reference (that is, the element).
|
||||
|
||||
|
@ -375,6 +375,5 @@ Some noteworthy observations:
|
||||
When you're filtering by CSS selector and only testing properties of a browser's _native element_, the `By.css` approach may be overkill.
|
||||
|
||||
It's often easier and more clear to filter with a standard `HTMLElement` method
|
||||
such as `querySelector()` or `querySelectorAll()`,
|
||||
as you'll see in the next set of tests.
|
||||
such as `querySelector()` or `querySelectorAll()`.
|
||||
|
||||
|
@ -190,7 +190,7 @@ It knows who the user is based on a property of the injected `UserService`:
|
||||
<code-example path="testing/src/app/welcome/welcome.component.ts" header="app/welcome/welcome.component.ts"></code-example>
|
||||
|
||||
The `WelcomeComponent` has decision logic that interacts with the service, logic that makes this component worth testing.
|
||||
Here's the testing module configuration for the spec file, `app/welcome/welcome.component.spec.ts`:
|
||||
Here's the testing module configuration for the spec file:
|
||||
|
||||
<code-example path="testing/src/app/welcome/welcome.component.spec.ts" region="config-test-module" header="app/welcome/welcome.component.spec.ts"></code-example>
|
||||
|
||||
@ -415,7 +415,7 @@ You do have to call [tick()](api/core/testing/tick) to advance the (virtual) clo
|
||||
Calling [tick()](api/core/testing/tick) simulates the passage of time until all pending asynchronous activities finish.
|
||||
In this case, it waits for the error handler's `setTimeout()`.
|
||||
|
||||
The [tick()](api/core/testing/tick) function accepts milliseconds and tickOptions as parameters, the millisecond (defaults to 0 if not provided) parameter represents how much the virtual clock advances. For example, if you have a `setTimeout(fn, 100)` in a `fakeAsync()` test, you need to use tick(100) to trigger the fn callback. The tickOptions is an optional parameter with a property called `processNewMacroTasksSynchronously` (defaults to true) represents whether to invoke new generated macro tasks when ticking.
|
||||
The [tick()](api/core/testing/tick) function accepts milliseconds and tickOptions as parameters, the millisecond (defaults to 0 if not provided) parameter represents how much the virtual clock advances. For example, if you have a `setTimeout(fn, 100)` in a `fakeAsync()` test, you need to use tick(100) to trigger the fn callback. The tickOptions is an optional parameter with a property called `processNewMacroTasksSynchronously` (defaults to true) that represents whether to invoke new generated macro tasks when ticking.
|
||||
|
||||
<code-example
|
||||
path="testing/src/app/demo/async-helper.spec.ts"
|
||||
@ -594,11 +594,6 @@ Then you can assert that the quote element displays the expected text.
|
||||
To use `waitForAsync()` functionality, you must import `zone.js/dist/zone-testing` in your test setup file.
|
||||
If you created your project with the Angular CLI, `zone-testing` is already imported in `src/test.ts`.
|
||||
|
||||
The `fakeAsync()` utility function has a few limitations.
|
||||
In particular, it won't work if the test body makes an `XMLHttpRequest` (XHR) call.
|
||||
XHR calls within a test are rare so you can generally stick with [`fakeAsync()`](#fake-async).
|
||||
But if you ever do need to call `XMLHttpRequest`, you'll want to know about `waitForAsync()`.
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
The `TestBed.compileComponents()` method (see [below](#compile-components)) calls `XHR`
|
||||
@ -1231,7 +1226,7 @@ and provide for _all_ services injected in _any_ component in the tree.
|
||||
That's too much effort just to answer a few simple questions about links.
|
||||
|
||||
This section describes two techniques for minimizing the setup.
|
||||
Use them, alone or in combination, to stay focused on the testing the primary component.
|
||||
Use them, alone or in combination, to stay focused on testing the primary component.
|
||||
|
||||
{@a stub-component}
|
||||
|
||||
@ -1340,7 +1335,7 @@ The `HostListener` wires the click event of the host element
|
||||
Clicking the anchor should trigger the `onClick()` method,
|
||||
which sets the stub's telltale `navigatedTo` property.
|
||||
Tests inspect `navigatedTo` to confirm that clicking the anchor
|
||||
set the expected route definition.
|
||||
sets the expected route definition.
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
@ -1573,7 +1568,7 @@ calls to other `TestBed` static methods such as `compileComponents()`.
|
||||
In this example, the `BannerComponent` is the only component to compile.
|
||||
Other examples configure the testing module with multiple components
|
||||
and may import application modules that hold yet more components.
|
||||
Any of them could be require external files.
|
||||
Any of them could require external files.
|
||||
|
||||
The `TestBed.compileComponents` method asynchronously compiles all components configured in the testing module.
|
||||
|
||||
|
@ -19,7 +19,7 @@ Here's a summary of the stand-alone functions, in order of likely utility:
|
||||
|
||||
<tr>
|
||||
<td style="vertical-align: top">
|
||||
<code>async</code>
|
||||
<code>waitForAsync</code>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
|
@ -75,7 +75,7 @@ The tests run again, the browser refreshes, and the new test results appear.
|
||||
|
||||
The CLI takes care of Jasmine and Karma configuration for you.
|
||||
|
||||
You can fine-tune many options by editing the `karma.conf.js` and
|
||||
You can fine-tune many options by editing the `karma.conf.js` in the root folder of the project and
|
||||
the `test.ts` files in the `src/` folder.
|
||||
|
||||
The `karma.conf.js` file is a partial Karma configuration file.
|
||||
|
@ -713,9 +713,9 @@
|
||||
"santosh": {
|
||||
"name": "Santosh Yadav",
|
||||
"picture": "santoshyadav.jpg",
|
||||
"twitter": "Santosh19742211",
|
||||
"twitter": "SantoshYadavDev",
|
||||
"website": "https://www.santoshyadav.dev",
|
||||
"bio": "Santosh is a GDE for Angular and Web Technologies and loves to contribute to Open Source. He is the creator of ng deploy for netlify and core team member for NestJS Addons. He writes for AngularInDepth, mentors for DotNetTricks, organizes Pune Tech Meetup, and conducts free workshops on Angular.",
|
||||
"bio": "Santosh is a GDE for Angular and loves to contribute to Open Source. He is the creator of ng deploy for netlify and core team member for NestJS Addons. He writes for AngularInDepth, mentors for DotNetTricks, organizes Pune Tech Meetup, and conducts free workshops on Angular.",
|
||||
"groups": ["GDE"]
|
||||
},
|
||||
"josephperrott": {
|
||||
|
@ -23,7 +23,7 @@
|
||||
"build-local-with-viewengine": "yarn ~~build",
|
||||
"prebuild-local-with-viewengine-ci": "node scripts/switch-to-viewengine && yarn setup-local-ci",
|
||||
"build-local-with-viewengine-ci": "yarn ~~build --progress=false",
|
||||
"extract-cli-command-docs": "node tools/transforms/cli-docs-package/extract-cli-commands.js b0b27361d",
|
||||
"extract-cli-command-docs": "node tools/transforms/cli-docs-package/extract-cli-commands.js ef770f1cb",
|
||||
"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",
|
||||
|
@ -214,7 +214,7 @@ code {
|
||||
margin-left: 2px;
|
||||
position: relative;
|
||||
@include line-height(24);
|
||||
vertical-align: bottom;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@ load("@npm_bazel_typescript//:index.bzl", "ts_library")
|
||||
ts_library(
|
||||
name = "commit-message",
|
||||
srcs = [
|
||||
"builder.ts",
|
||||
"cli.ts",
|
||||
"commit-message-draft.ts",
|
||||
"config.ts",
|
||||
@ -12,14 +13,17 @@ ts_library(
|
||||
"validate.ts",
|
||||
"validate-file.ts",
|
||||
"validate-range.ts",
|
||||
"wizard.ts",
|
||||
],
|
||||
module_name = "@angular/dev-infra-private/commit-message",
|
||||
visibility = ["//dev-infra:__subpackages__"],
|
||||
deps = [
|
||||
"//dev-infra/utils",
|
||||
"@npm//@types/inquirer",
|
||||
"@npm//@types/node",
|
||||
"@npm//@types/shelljs",
|
||||
"@npm//@types/yargs",
|
||||
"@npm//inquirer",
|
||||
"@npm//shelljs",
|
||||
"@npm//yargs",
|
||||
],
|
||||
@ -29,6 +33,7 @@ ts_library(
|
||||
name = "test_lib",
|
||||
testonly = True,
|
||||
srcs = [
|
||||
"builder.spec.ts",
|
||||
"parse.spec.ts",
|
||||
"validate.spec.ts",
|
||||
],
|
||||
|
46
dev-infra/commit-message/builder.spec.ts
Normal file
46
dev-infra/commit-message/builder.spec.ts
Normal file
@ -0,0 +1,46 @@
|
||||
/**
|
||||
* @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 config from '../utils/config';
|
||||
import * as console from '../utils/console';
|
||||
|
||||
import {buildCommitMessage} from './builder';
|
||||
|
||||
|
||||
describe('commit message building:', () => {
|
||||
beforeEach(() => {
|
||||
// stub logging calls to prevent noise in test log
|
||||
spyOn(console, 'info').and.stub();
|
||||
// provide a configuration for DevInfra when loaded
|
||||
spyOn(config, 'getConfig').and.returnValue({
|
||||
commitMessage: {
|
||||
scopes: ['core'],
|
||||
}
|
||||
} as any);
|
||||
});
|
||||
|
||||
it('creates a commit message with a scope', async () => {
|
||||
buildPromptResponseSpies('fix', 'core', 'This is a summary');
|
||||
|
||||
expect(await buildCommitMessage()).toMatch(/^fix\(core\): This is a summary/);
|
||||
});
|
||||
|
||||
it('creates a commit message without a scope', async () => {
|
||||
buildPromptResponseSpies('build', false, 'This is a summary');
|
||||
|
||||
expect(await buildCommitMessage()).toMatch(/^build: This is a summary/);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
/** Create spies to return the mocked selections from prompts. */
|
||||
function buildPromptResponseSpies(type: string, scope: string|false, summary: string) {
|
||||
spyOn(console, 'promptAutocomplete')
|
||||
.and.returnValues(Promise.resolve(type), Promise.resolve(scope));
|
||||
spyOn(console, 'promptInput').and.returnValue(Promise.resolve(summary));
|
||||
}
|
70
dev-infra/commit-message/builder.ts
Normal file
70
dev-infra/commit-message/builder.ts
Normal file
@ -0,0 +1,70 @@
|
||||
/**
|
||||
* @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 {ListChoiceOptions} from 'inquirer';
|
||||
|
||||
import {info, promptAutocomplete, promptInput} from '../utils/console';
|
||||
|
||||
import {COMMIT_TYPES, CommitType, getCommitMessageConfig, ScopeRequirement} from './config';
|
||||
|
||||
/** Validate commit message at the provided file path. */
|
||||
export async function buildCommitMessage() {
|
||||
// TODO(josephperrott): Add support for skipping wizard with local untracked config file
|
||||
// TODO(josephperrott): Add default commit message information/commenting into generated messages
|
||||
info('Just a few questions to start building the commit message!');
|
||||
|
||||
/** The commit message type. */
|
||||
const type = await promptForCommitMessageType();
|
||||
/** The commit message scope. */
|
||||
const scope = await promptForCommitMessageScopeForType(type);
|
||||
/** The commit message summary. */
|
||||
const summary = await promptForCommitMessageSummary();
|
||||
|
||||
return `${type.name}${scope ? '(' + scope + ')' : ''}: ${summary}\n\n`;
|
||||
}
|
||||
|
||||
/** Prompts in the terminal for the commit message's type. */
|
||||
async function promptForCommitMessageType(): Promise<CommitType> {
|
||||
info('The type of change in the commit. Allows a reader to know the effect of the change,');
|
||||
info('whether it brings a new feature, adds additional testing, documents the `project, etc.');
|
||||
|
||||
/** List of commit type options for the autocomplete prompt. */
|
||||
const typeOptions: ListChoiceOptions[] =
|
||||
Object.values(COMMIT_TYPES).map(({description, name}) => {
|
||||
return {
|
||||
name: `${name} - ${description}`,
|
||||
value: name,
|
||||
short: name,
|
||||
};
|
||||
});
|
||||
/** The key of a commit message type, selected by the user via prompt. */
|
||||
const typeName = await promptAutocomplete('Select a type for the commit:', typeOptions);
|
||||
|
||||
return COMMIT_TYPES[typeName];
|
||||
}
|
||||
|
||||
/** Prompts in the terminal for the commit message's scope. */
|
||||
async function promptForCommitMessageScopeForType(type: CommitType): Promise<string|false> {
|
||||
// If the commit type's scope requirement is forbidden, return early.
|
||||
if (type.scope === ScopeRequirement.Forbidden) {
|
||||
info(`Skipping scope selection as the '${type.name}' type does not allow scopes`);
|
||||
return false;
|
||||
}
|
||||
/** Commit message configuration */
|
||||
const config = getCommitMessageConfig();
|
||||
|
||||
info('The area of the repository the changes in this commit most affects.');
|
||||
return await promptAutocomplete(
|
||||
'Select a scope for the commit:', config.commitMessage.scopes,
|
||||
type.scope === ScopeRequirement.Optional ? '<no scope>' : '');
|
||||
}
|
||||
|
||||
/** Prompts in the terminal for the commit message's summary. */
|
||||
async function promptForCommitMessageSummary(): Promise<string> {
|
||||
info('Provide a short summary of what the changes in the commit do');
|
||||
return await promptInput('Provide a short summary of the commit');
|
||||
}
|
@ -12,6 +12,7 @@ import {info} from '../utils/console';
|
||||
import {restoreCommitMessage} from './restore-commit-message';
|
||||
import {validateFile} from './validate-file';
|
||||
import {validateCommitRange} from './validate-range';
|
||||
import {runWizard} from './wizard';
|
||||
|
||||
/** Build the parser for the commit-message commands. */
|
||||
export function buildCommitMessageParser(localYargs: yargs.Argv) {
|
||||
@ -41,6 +42,23 @@ export function buildCommitMessageParser(localYargs: yargs.Argv) {
|
||||
args => {
|
||||
restoreCommitMessage(args['file-env-variable'][0], args['file-env-variable'][1] as any);
|
||||
})
|
||||
.command(
|
||||
'wizard <filePath> [source] [commitSha]', '', ((args: any) => {
|
||||
return args
|
||||
.positional(
|
||||
'filePath',
|
||||
{description: 'The file path to write the generated commit message into'})
|
||||
.positional('source', {
|
||||
choices: ['message', 'template', 'merge', 'squash', 'commit'],
|
||||
description: 'The source of the commit message as described here: ' +
|
||||
'https://git-scm.com/docs/githooks#_prepare_commit_msg'
|
||||
})
|
||||
.positional(
|
||||
'commitSha', {description: 'The commit sha if source is set to `commit`'});
|
||||
}),
|
||||
async (args: any) => {
|
||||
await runWizard(args);
|
||||
})
|
||||
.command(
|
||||
'pre-commit-validate', 'Validate the most recent commit message', {
|
||||
'file': {
|
||||
|
@ -39,36 +39,56 @@ export enum ScopeRequirement {
|
||||
|
||||
/** A commit type */
|
||||
export interface CommitType {
|
||||
description: string;
|
||||
name: string;
|
||||
scope: ScopeRequirement;
|
||||
}
|
||||
|
||||
/** The valid commit types for Angular commit messages. */
|
||||
export const COMMIT_TYPES: {[key: string]: CommitType} = {
|
||||
build: {
|
||||
name: 'build',
|
||||
description: 'Changes to local repository build system and tooling',
|
||||
scope: ScopeRequirement.Forbidden,
|
||||
},
|
||||
ci: {
|
||||
name: 'ci',
|
||||
description: 'Changes to CI configuration and CI specific tooling',
|
||||
scope: ScopeRequirement.Forbidden,
|
||||
},
|
||||
docs: {
|
||||
name: 'docs',
|
||||
description: 'Changes which exclusively affects documentation.',
|
||||
scope: ScopeRequirement.Optional,
|
||||
},
|
||||
feat: {
|
||||
name: 'feat',
|
||||
description: 'Creates a new feature',
|
||||
scope: ScopeRequirement.Required,
|
||||
},
|
||||
fix: {
|
||||
name: 'fix',
|
||||
description: 'Fixes a previously discovered failure/bug',
|
||||
scope: ScopeRequirement.Required,
|
||||
},
|
||||
perf: {
|
||||
name: 'perf',
|
||||
description: 'Improves performance without any change in functionality or API',
|
||||
scope: ScopeRequirement.Required,
|
||||
},
|
||||
refactor: {
|
||||
name: 'refactor',
|
||||
description: 'Refactor without any change in functionality or API (includes style changes)',
|
||||
scope: ScopeRequirement.Required,
|
||||
},
|
||||
release: {
|
||||
name: 'release',
|
||||
description: 'A release point in the repository',
|
||||
scope: ScopeRequirement.Forbidden,
|
||||
},
|
||||
test: {
|
||||
name: 'test',
|
||||
description: 'Improvements or corrections made to the project\'s test suite',
|
||||
scope: ScopeRequirement.Required,
|
||||
},
|
||||
};
|
||||
|
@ -82,4 +82,24 @@ describe('commit message parsing:', () => {
|
||||
const message2 = buildCommitMessage({prefix: 'squash! '});
|
||||
expect(parseCommitMessage(message2).isSquash).toBe(true);
|
||||
});
|
||||
|
||||
it('ignores comment lines', () => {
|
||||
const message = buildCommitMessage({
|
||||
prefix: '# This is a comment line before the header.\n' +
|
||||
'## This is another comment line before the headers.\n',
|
||||
body: '# This is a comment line befor the body.\n' +
|
||||
'This is line 1 of the actual body.\n' +
|
||||
'## This is another comment line inside the body.\n' +
|
||||
'This is line 2 of the actual body (and it also contains a # but it not a comment).\n' +
|
||||
'### This is yet another comment line after the body.\n',
|
||||
});
|
||||
const parsedMessage = parseCommitMessage(message);
|
||||
|
||||
expect(parsedMessage.header)
|
||||
.toBe(`${commitValues.type}(${commitValues.scope}): ${commitValues.summary}`);
|
||||
expect(parsedMessage.body)
|
||||
.toBe(
|
||||
'This is line 1 of the actual body.\n' +
|
||||
'This is line 2 of the actual body (and it also contains a # but it not a comment).\n');
|
||||
});
|
||||
});
|
||||
|
@ -36,6 +36,10 @@ const COMMIT_BODY_RE = /^.*\n\n([\s\S]*)$/;
|
||||
|
||||
/** Parse a full commit message into its composite parts. */
|
||||
export function parseCommitMessage(commitMsg: string): ParsedCommitMessage {
|
||||
// Ignore comments (i.e. lines starting with `#`). Comments are automatically removed by git and
|
||||
// should not be considered part of the final commit message.
|
||||
commitMsg = commitMsg.split('\n').filter(line => !line.startsWith('#')).join('\n');
|
||||
|
||||
let header = '';
|
||||
let body = '';
|
||||
let bodyWithoutLinking = '';
|
||||
|
43
dev-infra/commit-message/wizard.ts
Normal file
43
dev-infra/commit-message/wizard.ts
Normal file
@ -0,0 +1,43 @@
|
||||
/**
|
||||
* @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 {writeFileSync} from 'fs';
|
||||
|
||||
import {info} from '../utils/console';
|
||||
|
||||
import {buildCommitMessage} from './builder';
|
||||
|
||||
/**
|
||||
* The source triggering the git commit message creation.
|
||||
* As described in: https://git-scm.com/docs/githooks#_prepare_commit_msg
|
||||
*/
|
||||
export type PrepareCommitMsgHookSource = 'message'|'template'|'merge'|'squash'|'commit';
|
||||
|
||||
/** The default commit message used if the wizard does not procude a commit message. */
|
||||
const defaultCommitMessage = `<type>(<scope>): <summary>
|
||||
|
||||
# <Describe the motivation behind this change - explain WHY you are making this change. Wrap all
|
||||
# lines at 100 characters.>\n\n`;
|
||||
|
||||
export async function runWizard(
|
||||
args: {filePath: string, source?: PrepareCommitMsgHookSource, commitSha?: string}) {
|
||||
// TODO(josephperrott): Add support for skipping wizard with local untracked config file
|
||||
|
||||
if (args.source !== undefined) {
|
||||
info(`Skipping commit message wizard due because the commit was created via '${
|
||||
args.source}' source`);
|
||||
process.exitCode = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the default commit message to be updated if the user cancels out of the wizard in progress
|
||||
writeFileSync(args.filePath, defaultCommitMessage);
|
||||
|
||||
/** The generated commit message. */
|
||||
const commitMessage = await buildCommitMessage();
|
||||
writeFileSync(args.filePath, commitMessage);
|
||||
}
|
@ -6,6 +6,7 @@ ts_library(
|
||||
module_name = "@angular/dev-infra-private/pr",
|
||||
visibility = ["//dev-infra:__subpackages__"],
|
||||
deps = [
|
||||
"//dev-infra/pr/checkout",
|
||||
"//dev-infra/pr/discover-new-conflicts",
|
||||
"//dev-infra/pr/merge",
|
||||
"//dev-infra/pr/rebase",
|
||||
|
13
dev-infra/pr/checkout/BUILD.bazel
Normal file
13
dev-infra/pr/checkout/BUILD.bazel
Normal file
@ -0,0 +1,13 @@
|
||||
load("@npm_bazel_typescript//:index.bzl", "ts_library")
|
||||
|
||||
ts_library(
|
||||
name = "checkout",
|
||||
srcs = glob(["*.ts"]),
|
||||
module_name = "@angular/dev-infra-private/pr/checkout",
|
||||
visibility = ["//dev-infra:__subpackages__"],
|
||||
deps = [
|
||||
"//dev-infra/pr/common",
|
||||
"//dev-infra/utils",
|
||||
"@npm//@types/yargs",
|
||||
],
|
||||
)
|
50
dev-infra/pr/checkout/cli.ts
Normal file
50
dev-infra/pr/checkout/cli.ts
Normal file
@ -0,0 +1,50 @@
|
||||
/**
|
||||
* @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 {Arguments, Argv, CommandModule} from 'yargs';
|
||||
|
||||
import {error} from '../../utils/console';
|
||||
import {checkOutPullRequestLocally} from '../common/checkout-pr';
|
||||
|
||||
export interface CheckoutOptions {
|
||||
prNumber: number;
|
||||
'github-token'?: string;
|
||||
}
|
||||
|
||||
/** URL to the Github page where personal access tokens can be generated. */
|
||||
export const GITHUB_TOKEN_GENERATE_URL = `https://github.com/settings/tokens`;
|
||||
|
||||
/** Builds the checkout pull request command. */
|
||||
function builder(yargs: Argv) {
|
||||
return yargs.positional('prNumber', {type: 'number', demandOption: true}).option('github-token', {
|
||||
type: 'string',
|
||||
description: 'Github token. If not set, token is retrieved from the environment variables.'
|
||||
});
|
||||
}
|
||||
|
||||
/** Handles the checkout pull request command. */
|
||||
async function handler({prNumber, 'github-token': token}: Arguments<CheckoutOptions>) {
|
||||
const githubToken = token || process.env.GITHUB_TOKEN || process.env.TOKEN;
|
||||
if (!githubToken) {
|
||||
error('No Github token set. Please set the `GITHUB_TOKEN` environment variable.');
|
||||
error('Alternatively, pass the `--github-token` command line flag.');
|
||||
error(`You can generate a token here: ${GITHUB_TOKEN_GENERATE_URL}`);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
const prCheckoutOptions = {allowIfMaintainerCannotModify: true, branchName: `pr-${prNumber}`};
|
||||
await checkOutPullRequestLocally(prNumber, githubToken, prCheckoutOptions);
|
||||
}
|
||||
|
||||
/** yargs command module for checking out a PR */
|
||||
export const CheckoutCommandModule: CommandModule<{}, CheckoutOptions> = {
|
||||
handler,
|
||||
builder,
|
||||
command: 'checkout <pr-number>',
|
||||
describe: 'Checkout a PR from the upstream repo',
|
||||
};
|
@ -8,6 +8,7 @@
|
||||
|
||||
import * as yargs from 'yargs';
|
||||
|
||||
import {CheckoutCommandModule} from './checkout/cli';
|
||||
import {buildDiscoverNewConflictsCommand, handleDiscoverNewConflictsCommand} from './discover-new-conflicts/cli';
|
||||
import {buildMergeCommand, handleMergeCommand} from './merge/cli';
|
||||
import {buildRebaseCommand, handleRebaseCommand} from './rebase/cli';
|
||||
@ -24,7 +25,8 @@ export function buildPrParser(localYargs: yargs.Argv) {
|
||||
buildDiscoverNewConflictsCommand, handleDiscoverNewConflictsCommand)
|
||||
.command(
|
||||
'rebase <pr-number>', 'Rebase a pending PR and push the rebased commits back to Github',
|
||||
buildRebaseCommand, handleRebaseCommand);
|
||||
buildRebaseCommand, handleRebaseCommand)
|
||||
.command(CheckoutCommandModule);
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
|
12
dev-infra/pr/common/BUILD.bazel
Normal file
12
dev-infra/pr/common/BUILD.bazel
Normal file
@ -0,0 +1,12 @@
|
||||
load("@npm_bazel_typescript//:index.bzl", "ts_library")
|
||||
|
||||
ts_library(
|
||||
name = "common",
|
||||
srcs = glob(["*.ts"]),
|
||||
visibility = ["//dev-infra:__subpackages__"],
|
||||
deps = [
|
||||
"//dev-infra/utils",
|
||||
"@npm//@types/node",
|
||||
"@npm//typed-graphqlify",
|
||||
],
|
||||
)
|
135
dev-infra/pr/common/checkout-pr.ts
Normal file
135
dev-infra/pr/common/checkout-pr.ts
Normal file
@ -0,0 +1,135 @@
|
||||
/**
|
||||
* @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 {types as graphQLTypes} from 'typed-graphqlify';
|
||||
import {URL} from 'url';
|
||||
|
||||
import {info} from '../../utils/console';
|
||||
import {GitClient} from '../../utils/git';
|
||||
import {getPr} from '../../utils/github';
|
||||
|
||||
/* GraphQL schema for the response body for a pending PR. */
|
||||
const PR_SCHEMA = {
|
||||
state: graphQLTypes.string,
|
||||
maintainerCanModify: graphQLTypes.boolean,
|
||||
viewerDidAuthor: graphQLTypes.boolean,
|
||||
headRefOid: graphQLTypes.string,
|
||||
headRef: {
|
||||
name: graphQLTypes.string,
|
||||
repository: {
|
||||
url: graphQLTypes.string,
|
||||
nameWithOwner: graphQLTypes.string,
|
||||
},
|
||||
},
|
||||
baseRef: {
|
||||
name: graphQLTypes.string,
|
||||
repository: {
|
||||
url: graphQLTypes.string,
|
||||
nameWithOwner: graphQLTypes.string,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
export class UnexpectedLocalChangesError extends Error {
|
||||
constructor(m: string) {
|
||||
super(m);
|
||||
Object.setPrototypeOf(this, UnexpectedLocalChangesError.prototype);
|
||||
}
|
||||
}
|
||||
|
||||
export class MaintainerModifyAccessError extends Error {
|
||||
constructor(m: string) {
|
||||
super(m);
|
||||
Object.setPrototypeOf(this, MaintainerModifyAccessError.prototype);
|
||||
}
|
||||
}
|
||||
|
||||
/** Options for checking out a PR */
|
||||
export interface PullRequestCheckoutOptions {
|
||||
/** Whether the PR should be checked out if the maintainer cannot modify. */
|
||||
allowIfMaintainerCannotModify?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebase the provided PR onto its merge target branch, and push up the resulting
|
||||
* commit to the PRs repository.
|
||||
*/
|
||||
export async function checkOutPullRequestLocally(
|
||||
prNumber: number, githubToken: string, opts: PullRequestCheckoutOptions = {}) {
|
||||
/** Authenticated Git client for git and Github interactions. */
|
||||
const git = new GitClient(githubToken);
|
||||
|
||||
// In order to preserve local changes, checkouts cannot occur if local changes are present in the
|
||||
// git environment. Checked before retrieving the PR to fail fast.
|
||||
if (git.hasLocalChanges()) {
|
||||
throw new UnexpectedLocalChangesError('Unable to checkout PR due to uncommitted changes.');
|
||||
}
|
||||
|
||||
/**
|
||||
* The branch or revision originally checked out before this method performed
|
||||
* any Git operations that may change the working branch.
|
||||
*/
|
||||
const previousBranchOrRevision = git.getCurrentBranchOrRevision();
|
||||
/* The PR information from Github. */
|
||||
const pr = await getPr(PR_SCHEMA, prNumber, git);
|
||||
/** The branch name of the PR from the repository the PR came from. */
|
||||
const headRefName = pr.headRef.name;
|
||||
/** The full ref for the repository and branch the PR came from. */
|
||||
const fullHeadRef = `${pr.headRef.repository.nameWithOwner}:${headRefName}`;
|
||||
/** The full URL path of the repository the PR came from with github token as authentication. */
|
||||
const headRefUrl = addAuthenticationToUrl(pr.headRef.repository.url, githubToken);
|
||||
// Note: Since we use a detached head for rebasing the PR and therefore do not have
|
||||
// remote-tracking branches configured, we need to set our expected ref and SHA. This
|
||||
// allows us to use `--force-with-lease` for the detached head while ensuring that we
|
||||
// never accidentally override upstream changes that have been pushed in the meanwhile.
|
||||
// See:
|
||||
// https://git-scm.com/docs/git-push#Documentation/git-push.txt---force-with-leaseltrefnamegtltexpectgt
|
||||
/** Flag for a force push with leage back to upstream. */
|
||||
const forceWithLeaseFlag = `--force-with-lease=${headRefName}:${pr.headRefOid}`;
|
||||
|
||||
// If the PR does not allow maintainers to modify it, exit as the rebased PR cannot
|
||||
// be pushed up.
|
||||
if (!pr.maintainerCanModify && !pr.viewerDidAuthor && !opts.allowIfMaintainerCannotModify) {
|
||||
throw new MaintainerModifyAccessError('PR is not set to allow maintainers to modify the PR');
|
||||
}
|
||||
|
||||
try {
|
||||
// Fetch the branch at the commit of the PR, and check it out in a detached state.
|
||||
info(`Checking out PR #${prNumber} from ${fullHeadRef}`);
|
||||
git.run(['fetch', headRefUrl, headRefName]);
|
||||
git.run(['checkout', '--detach', 'FETCH_HEAD']);
|
||||
} catch (e) {
|
||||
git.checkout(previousBranchOrRevision, true);
|
||||
throw e;
|
||||
}
|
||||
|
||||
return {
|
||||
/**
|
||||
* Pushes the current local branch to the PR on the upstream repository.
|
||||
*
|
||||
* @returns true If the command did not fail causing a GitCommandError to be thrown.
|
||||
* @throws GitCommandError Thrown when the push back to upstream fails.
|
||||
*/
|
||||
pushToUpstream: (): true => {
|
||||
git.run(['push', headRefUrl, `HEAD:${headRefName}`, forceWithLeaseFlag]);
|
||||
return true;
|
||||
},
|
||||
/** Restores the state of the local repository to before the PR checkout occured. */
|
||||
resetGitState: (): boolean => {
|
||||
return git.checkout(previousBranchOrRevision, true);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** Adds the provided token as username to the provided url. */
|
||||
function addAuthenticationToUrl(urlString: string, token: string) {
|
||||
const url = new URL(urlString);
|
||||
url.username = token;
|
||||
return url.toString();
|
||||
}
|
@ -72,7 +72,7 @@ export async function discoverNewConflictsForPr(
|
||||
|
||||
info(`Requesting pending PRs from Github`);
|
||||
/** List of PRs from github currently known as mergable. */
|
||||
const allPendingPRs = (await getPendingPrs(PR_SCHEMA, config.github)).map(processPr);
|
||||
const allPendingPRs = (await getPendingPrs(PR_SCHEMA, git)).map(processPr);
|
||||
/** The PR which is being checked against. */
|
||||
const requestedPr = allPendingPRs.find(pr => pr.number === newPrNumber);
|
||||
if (requestedPr === undefined) {
|
||||
|
@ -55,7 +55,7 @@ export async function rebasePr(
|
||||
*/
|
||||
const previousBranchOrRevision = git.getCurrentBranchOrRevision();
|
||||
/* Get the PR information from Github. */
|
||||
const pr = await getPr(PR_SCHEMA, prNumber, config.github);
|
||||
const pr = await getPr(PR_SCHEMA, prNumber, git);
|
||||
|
||||
const headRefName = pr.headRef.name;
|
||||
const baseRefName = pr.baseRef.name;
|
||||
|
@ -17,6 +17,7 @@ ts_library(
|
||||
"@npm//@types/shelljs",
|
||||
"@npm//chalk",
|
||||
"@npm//inquirer",
|
||||
"@npm//inquirer-autocomplete-prompt",
|
||||
"@npm//shelljs",
|
||||
"@npm//tslib",
|
||||
"@npm//typed-graphqlify",
|
||||
|
@ -7,7 +7,8 @@
|
||||
*/
|
||||
|
||||
import chalk from 'chalk';
|
||||
import {prompt} from 'inquirer';
|
||||
import {createPromptModule, ListChoiceOptions, prompt} from 'inquirer';
|
||||
import * as inquirerAutocomplete from 'inquirer-autocomplete-prompt';
|
||||
|
||||
|
||||
/** Reexport of chalk colors for convenient access. */
|
||||
@ -26,6 +27,52 @@ export async function promptConfirm(message: string, defaultValue = false): Prom
|
||||
.result;
|
||||
}
|
||||
|
||||
/** Prompts the user to select an option from a filterable autocomplete list. */
|
||||
export async function promptAutocomplete(
|
||||
message: string, choices: (string|ListChoiceOptions)[]): Promise<string>;
|
||||
/**
|
||||
* Prompts the user to select an option from a filterable autocomplete list, with an option to
|
||||
* choose no value.
|
||||
*/
|
||||
export async function promptAutocomplete(
|
||||
message: string, choices: (string|ListChoiceOptions)[],
|
||||
noChoiceText?: string): Promise<string|false>;
|
||||
export async function promptAutocomplete(
|
||||
message: string, choices: (string|ListChoiceOptions)[],
|
||||
noChoiceText?: string): Promise<string|false> {
|
||||
// Creates a local prompt module with an autocomplete prompt type.
|
||||
const prompt = createPromptModule({}).registerPrompt('autocomplete', inquirerAutocomplete);
|
||||
if (noChoiceText) {
|
||||
choices = [noChoiceText, ...choices];
|
||||
}
|
||||
// `prompt` must be cast as `any` as the autocomplete typings are not available.
|
||||
const result = (await (prompt as any)({
|
||||
type: 'autocomplete',
|
||||
name: 'result',
|
||||
message,
|
||||
source: (_: any, input: string) => {
|
||||
if (!input) {
|
||||
return Promise.resolve(choices);
|
||||
}
|
||||
return Promise.resolve(choices.filter(choice => {
|
||||
if (typeof choice === 'string') {
|
||||
return choice.includes(input);
|
||||
}
|
||||
return choice.name!.includes(input);
|
||||
}));
|
||||
}
|
||||
})).result;
|
||||
if (result === noChoiceText) {
|
||||
return false;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Prompts the user for one line of input. */
|
||||
export async function promptInput(message: string): Promise<string> {
|
||||
return (await prompt<{result: string}>({type: 'input', name: 'result', message})).result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Supported levels for logging functions.
|
||||
*
|
||||
|
@ -26,7 +26,7 @@ export class GithubApiRequestError extends Error {
|
||||
**/
|
||||
export class GithubClient extends Octokit {
|
||||
/** The Github GraphQL (v4) API. */
|
||||
graqhql: GithubGraphqlClient;
|
||||
graphql: GithubGraphqlClient;
|
||||
|
||||
/** The current user based on checking against the Github API. */
|
||||
private _currentUser: string|null = null;
|
||||
@ -42,7 +42,7 @@ export class GithubClient extends Octokit {
|
||||
});
|
||||
|
||||
// Create authenticated graphql client.
|
||||
this.graqhql = new GithubGraphqlClient(token);
|
||||
this.graphql = new GithubGraphqlClient(token);
|
||||
}
|
||||
|
||||
/** Retrieve the login of the current user from Github. */
|
||||
@ -51,7 +51,7 @@ export class GithubClient extends Octokit {
|
||||
if (this._currentUser !== null) {
|
||||
return this._currentUser;
|
||||
}
|
||||
const result = await this.graqhql.query({
|
||||
const result = await this.graphql.query({
|
||||
viewer: {
|
||||
login: types.string,
|
||||
}
|
||||
@ -80,7 +80,7 @@ class GithubGraphqlClient {
|
||||
// Set the default headers to include authorization with the provided token for all
|
||||
// graphQL calls.
|
||||
if (token) {
|
||||
this.graqhql.defaults({headers: {authorization: `token ${token}`}});
|
||||
this.graqhql = this.graqhql.defaults({headers: {authorization: `token ${token}`}});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -150,6 +150,25 @@ export class GitClient {
|
||||
return value.replace(this._githubTokenRegex, '<TOKEN>');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks out a requested branch or revision, optionally cleaning the state of the repository
|
||||
* before attempting the checking. Returns a boolean indicating whether the branch or revision
|
||||
* was cleanly checked out.
|
||||
*/
|
||||
checkout(branchOrRevision: string, cleanState: boolean): boolean {
|
||||
if (cleanState) {
|
||||
// Abort any outstanding ams.
|
||||
this.runGraceful(['am', '--abort'], {stdio: 'ignore'});
|
||||
// Abort any outstanding cherry-picks.
|
||||
this.runGraceful(['cherry-pick', '--abort'], {stdio: 'ignore'});
|
||||
// Abort any outstanding rebases.
|
||||
this.runGraceful(['rebase', '--abort'], {stdio: 'ignore'});
|
||||
// Clear any changes in the current repo.
|
||||
this.runGraceful(['reset', '--hard'], {stdio: 'ignore'});
|
||||
}
|
||||
return this.runGraceful(['checkout', branchOrRevision], {stdio: 'ignore'}).status === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the GitClient instance is using a token with permissions for the all of the
|
||||
* provided OAuth scopes.
|
||||
|
@ -6,29 +6,15 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {graphql as unauthenticatedGraphql} from '@octokit/graphql';
|
||||
import {params, types} from 'typed-graphqlify';
|
||||
|
||||
import {params, query as graphqlQuery, types} from 'typed-graphqlify';
|
||||
import {NgDevConfig} from './config';
|
||||
|
||||
/** The configuration required for github interactions. */
|
||||
type GithubConfig = NgDevConfig['github'];
|
||||
|
||||
/**
|
||||
* Authenticated instance of Github GraphQl API service, relies on a
|
||||
* personal access token being available in the TOKEN environment variable.
|
||||
*/
|
||||
const graphql = unauthenticatedGraphql.defaults({
|
||||
headers: {
|
||||
// TODO(josephperrott): Remove reference to TOKEN environment variable as part of larger
|
||||
// effort to migrate to expecting tokens via GITHUB_ACCESS_TOKEN environment variables.
|
||||
authorization: `token ${process.env.TOKEN || process.env.GITHUB_ACCESS_TOKEN}`,
|
||||
}
|
||||
});
|
||||
import {GitClient} from './git';
|
||||
|
||||
/** Get a PR from github */
|
||||
export async function getPr<PrSchema>(
|
||||
prSchema: PrSchema, prNumber: number, {owner, name}: GithubConfig) {
|
||||
export async function getPr<PrSchema>(prSchema: PrSchema, prNumber: number, git: GitClient) {
|
||||
/** The owner and name of the repository */
|
||||
const {owner, name} = git.remoteConfig;
|
||||
/** The GraphQL query object to get a the PR */
|
||||
const PR_QUERY = params(
|
||||
{
|
||||
$number: 'Int!', // The PR number
|
||||
@ -41,14 +27,15 @@ export async function getPr<PrSchema>(
|
||||
})
|
||||
});
|
||||
|
||||
const result =
|
||||
await graphql(graphqlQuery(PR_QUERY), {number: prNumber, owner, name}) as typeof PR_QUERY;
|
||||
const result = (await git.github.graphql.query(PR_QUERY, {number: prNumber, owner, name}));
|
||||
return result.repository.pullRequest;
|
||||
}
|
||||
|
||||
/** Get all pending PRs from github */
|
||||
export async function getPendingPrs<PrSchema>(prSchema: PrSchema, {owner, name}: GithubConfig) {
|
||||
// The GraphQL query object to get a page of pending PRs
|
||||
export async function getPendingPrs<PrSchema>(prSchema: PrSchema, git: GitClient) {
|
||||
/** The owner and name of the repository */
|
||||
const {owner, name} = git.remoteConfig;
|
||||
/** The GraphQL query object to get a page of pending PRs */
|
||||
const PRS_QUERY = params(
|
||||
{
|
||||
$first: 'Int', // How many entries to get with each request
|
||||
@ -73,36 +60,22 @@ export async function getPendingPrs<PrSchema>(prSchema: PrSchema, {owner, name}:
|
||||
}),
|
||||
})
|
||||
});
|
||||
const query = graphqlQuery('members', PRS_QUERY);
|
||||
|
||||
/**
|
||||
* Gets the query and queryParams for a specific page of entries.
|
||||
*/
|
||||
const queryBuilder = (count: number, cursor?: string) => {
|
||||
return {
|
||||
query,
|
||||
params: {
|
||||
after: cursor || null,
|
||||
first: count,
|
||||
owner,
|
||||
name,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// The current cursor
|
||||
/** The current cursor */
|
||||
let cursor: string|undefined;
|
||||
// If an additional page of members is expected
|
||||
/** If an additional page of members is expected */
|
||||
let hasNextPage = true;
|
||||
// Array of pending PRs
|
||||
/** Array of pending PRs */
|
||||
const prs: Array<PrSchema> = [];
|
||||
|
||||
// For each page of the response, get the page and add it to the
|
||||
// list of PRs
|
||||
// For each page of the response, get the page and add it to the list of PRs
|
||||
while (hasNextPage) {
|
||||
const {query, params} = queryBuilder(100, cursor);
|
||||
const results = await graphql(query, params) as typeof PRS_QUERY;
|
||||
|
||||
const params = {
|
||||
after: cursor || null,
|
||||
first: 100,
|
||||
owner,
|
||||
name,
|
||||
};
|
||||
const results = await git.github.graphql.query(PRS_QUERY, params) as typeof PRS_QUERY;
|
||||
prs.push(...results.repository.pullRequests.nodes);
|
||||
hasNextPage = results.repository.pullRequests.pageInfo.hasNextPage;
|
||||
cursor = results.repository.pullRequests.pageInfo.endCursor;
|
||||
|
17
dev-infra/utils/inquirer-autocomplete-typings.d.ts
vendored
Normal file
17
dev-infra/utils/inquirer-autocomplete-typings.d.ts
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
// inquirer-autocomplete-prompt doesn't provide types and no types are made available via
|
||||
// DefinitelyTyped.
|
||||
declare module "inquirer-autocomplete-prompt" {
|
||||
|
||||
import {registerPrompt} from 'inquirer';
|
||||
|
||||
let AutocompletePrompt: Parameters<typeof registerPrompt>[1];
|
||||
export = AutocompletePrompt;
|
||||
}
|
2
goldens/public-api/common/common.d.ts
vendored
2
goldens/public-api/common/common.d.ts
vendored
@ -216,7 +216,7 @@ export declare class NgComponentOutlet implements OnChanges, OnDestroy {
|
||||
}
|
||||
|
||||
export declare class NgForOf<T, U extends NgIterable<T> = NgIterable<T>> implements DoCheck {
|
||||
set ngForOf(ngForOf: (U & NgIterable<T>) | undefined | null);
|
||||
set ngForOf(ngForOf: U & NgIterable<T> | undefined | null);
|
||||
set ngForTemplate(value: TemplateRef<NgForOfContext<T, U>>);
|
||||
set ngForTrackBy(fn: TrackByFunction<T>);
|
||||
get ngForTrackBy(): TrackByFunction<T>;
|
||||
|
2
goldens/public-api/elements/elements.d.ts
vendored
2
goldens/public-api/elements/elements.d.ts
vendored
@ -2,7 +2,7 @@ export declare function createCustomElement<P>(component: Type<any>, config: NgE
|
||||
|
||||
export declare abstract class NgElement extends HTMLElement {
|
||||
protected ngElementEventsSubscription: Subscription | null;
|
||||
protected ngElementStrategy: NgElementStrategy;
|
||||
protected abstract ngElementStrategy: NgElementStrategy;
|
||||
abstract attributeChangedCallback(attrName: string, oldValue: string | null, newValue: string, namespace?: string): void;
|
||||
abstract connectedCallback(): void;
|
||||
abstract disconnectedCallback(): void;
|
||||
|
@ -85,6 +85,12 @@ INTEGRATION_TESTS = {
|
||||
# root @npm//typescript package.
|
||||
"pinned_npm_packages": ["typescript"],
|
||||
},
|
||||
"typings_test_ts40": {
|
||||
# Special case for `typings_test_ts40` test as we want to pin
|
||||
# `typescript` at version 4.0.x for that test and not link to the
|
||||
# root @npm//typescript package.
|
||||
"pinned_npm_packages": ["typescript"],
|
||||
},
|
||||
}
|
||||
|
||||
[
|
||||
|
71
integration/typings_test_ts40/include-all.ts
Normal file
71
integration/typings_test_ts40/include-all.ts
Normal file
@ -0,0 +1,71 @@
|
||||
/**
|
||||
* @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 animations from '@angular/animations';
|
||||
import * as animationsBrowser from '@angular/animations/browser';
|
||||
import * as animationsBrowserTesting from '@angular/animations/browser/testing';
|
||||
import * as common from '@angular/common';
|
||||
import * as commonHttp from '@angular/common/http';
|
||||
import * as commonTesting from '@angular/common/testing';
|
||||
import * as commonHttpTesting from '@angular/common/testing';
|
||||
import * as compiler from '@angular/compiler';
|
||||
import * as compilerTesting from '@angular/compiler/testing';
|
||||
import * as core from '@angular/core';
|
||||
import * as coreTesting from '@angular/core/testing';
|
||||
import * as elements from '@angular/elements';
|
||||
import * as forms from '@angular/forms';
|
||||
import * as platformBrowser from '@angular/platform-browser';
|
||||
import * as platformBrowserDynamic from '@angular/platform-browser-dynamic';
|
||||
import * as platformBrowserDynamicTesting from '@angular/platform-browser-dynamic/testing';
|
||||
import * as platformBrowserAnimations from '@angular/platform-browser/animations';
|
||||
import * as platformBrowserTesting from '@angular/platform-browser/testing';
|
||||
import * as platformServer from '@angular/platform-server';
|
||||
import * as platformServerTesting from '@angular/platform-server/testing';
|
||||
import * as platformWebworker from '@angular/platform-webworker';
|
||||
import * as platformWebworkerDynamic from '@angular/platform-webworker-dynamic';
|
||||
import * as router from '@angular/router';
|
||||
import * as routerTesting from '@angular/router/testing';
|
||||
import * as routerUpgrade from '@angular/router/upgrade';
|
||||
import * as serviceWorker from '@angular/service-worker';
|
||||
import * as upgrade from '@angular/upgrade';
|
||||
import * as upgradeStatic from '@angular/upgrade/static';
|
||||
import * as upgradeTesting from '@angular/upgrade/static/testing';
|
||||
|
||||
export default {
|
||||
animations,
|
||||
animationsBrowser,
|
||||
animationsBrowserTesting,
|
||||
common,
|
||||
commonTesting,
|
||||
commonHttp,
|
||||
commonHttpTesting,
|
||||
compiler,
|
||||
compilerTesting,
|
||||
core,
|
||||
coreTesting,
|
||||
elements,
|
||||
forms,
|
||||
platformBrowser,
|
||||
platformBrowserTesting,
|
||||
platformBrowserDynamic,
|
||||
platformBrowserDynamicTesting,
|
||||
platformBrowserAnimations,
|
||||
platformServer,
|
||||
platformServerTesting,
|
||||
platformWebworker,
|
||||
platformWebworkerDynamic,
|
||||
router,
|
||||
routerTesting,
|
||||
routerUpgrade,
|
||||
serviceWorker,
|
||||
upgrade,
|
||||
upgradeStatic,
|
||||
upgradeTesting,
|
||||
};
|
30
integration/typings_test_ts40/package.json
Normal file
30
integration/typings_test_ts40/package.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "angular-integration",
|
||||
"description": "Assert that users with TypeScript 4.0 can type-check an Angular application",
|
||||
"version": "0.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@angular/animations": "file:../../dist/packages-dist/animations",
|
||||
"@angular/common": "file:../../dist/packages-dist/common",
|
||||
"@angular/compiler": "file:../../dist/packages-dist/compiler",
|
||||
"@angular/compiler-cli": "file:../../dist/packages-dist/compiler-cli",
|
||||
"@angular/core": "file:../../dist/packages-dist/core",
|
||||
"@angular/elements": "file:../../dist/packages-dist/elements",
|
||||
"@angular/forms": "file:../../dist/packages-dist/forms",
|
||||
"@angular/platform-browser": "file:../../dist/packages-dist/platform-browser",
|
||||
"@angular/platform-browser-dynamic": "file:../../dist/packages-dist/platform-browser-dynamic",
|
||||
"@angular/platform-server": "file:../../dist/packages-dist/platform-server",
|
||||
"@angular/platform-webworker": "file:../../dist/packages-dist/platform-webworker",
|
||||
"@angular/platform-webworker-dynamic": "file:../../dist/packages-dist/platform-webworker-dynamic",
|
||||
"@angular/router": "file:../../dist/packages-dist/router",
|
||||
"@angular/service-worker": "file:../../dist/packages-dist/service-worker",
|
||||
"@angular/upgrade": "file:../../dist/packages-dist/upgrade",
|
||||
"@types/jasmine": "file:../../node_modules/@types/jasmine",
|
||||
"rxjs": "file:../../node_modules/rxjs",
|
||||
"typescript": "4.0.2",
|
||||
"zone.js": "file:../../dist/zone.js-dist/zone.js"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "tsc"
|
||||
}
|
||||
}
|
26
integration/typings_test_ts40/tsconfig.json
Normal file
26
integration/typings_test_ts40/tsconfig.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"experimentalDecorators": true,
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"outDir": "./dist/out-tsc",
|
||||
"rootDir": ".",
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"es5",
|
||||
"dom",
|
||||
"es2015.collection",
|
||||
"es2015.iterable",
|
||||
"es2015.promise"
|
||||
],
|
||||
"types": [],
|
||||
},
|
||||
"files": [
|
||||
"include-all.ts",
|
||||
"node_modules/@types/jasmine/index.d.ts"
|
||||
]
|
||||
}
|
13
package.json
13
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "angular-srcs",
|
||||
"version": "10.1.0-next.6",
|
||||
"version": "10.1.0-rc.0",
|
||||
"private": true,
|
||||
"description": "Angular - a web framework for modern web apps",
|
||||
"homepage": "https://github.com/angular/angular",
|
||||
@ -76,7 +76,7 @@
|
||||
"@types/diff": "^3.5.1",
|
||||
"@types/fs-extra": "4.0.2",
|
||||
"@types/hammerjs": "2.0.35",
|
||||
"@types/inquirer": "^6.5.0",
|
||||
"@types/inquirer": "^7.3.0",
|
||||
"@types/jasmine": "3.5.10",
|
||||
"@types/jasmine-ajax": "^3.3.1",
|
||||
"@types/jasminewd2": "^2.0.8",
|
||||
@ -149,8 +149,8 @@
|
||||
"terser": "^4.4.0",
|
||||
"tsickle": "0.38.1",
|
||||
"tslib": "^2.0.0",
|
||||
"tslint": "6.0.0",
|
||||
"typescript": "~3.9.5",
|
||||
"tslint": "6.1.3",
|
||||
"typescript": "~4.0.2",
|
||||
"xhr2": "0.2.0",
|
||||
"yaml": "^1.7.2",
|
||||
"yargs": "^15.4.1"
|
||||
@ -179,8 +179,9 @@
|
||||
"glob": "7.1.2",
|
||||
"gulp": "3.9.1",
|
||||
"gulp-conventional-changelog": "^2.0.3",
|
||||
"husky": "^4.2.3",
|
||||
"inquirer": "^7.1.0",
|
||||
"husky": "^4.2.5",
|
||||
"inquirer": "^7.3.3",
|
||||
"inquirer-autocomplete-prompt": "^1.0.2",
|
||||
"jpm": "1.3.1",
|
||||
"karma-browserstack-launcher": "^1.3.0",
|
||||
"karma-sauce-launcher": "^2.0.2",
|
||||
|
@ -34,7 +34,7 @@
|
||||
"@angular/compiler-cli": "0.0.0-PLACEHOLDER",
|
||||
"@bazel/typescript": ">=1.0.0",
|
||||
"terser": "^4.3.1",
|
||||
"typescript": ">=3.9 <4.0",
|
||||
"typescript": ">=3.9 <4.1",
|
||||
"rollup": ">=1.20.0",
|
||||
"rollup-plugin-commonjs": ">=9.0.0",
|
||||
"rollup-plugin-node-resolve": ">=4.2.0",
|
||||
|
@ -27,7 +27,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/compiler": "0.0.0-PLACEHOLDER",
|
||||
"typescript": ">=3.9 <4.0"
|
||||
"typescript": ">=3.9 <4.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0"
|
||||
|
@ -476,13 +476,16 @@ export class Evaluator {
|
||||
return recordEntry(typeReference, node);
|
||||
case ts.SyntaxKind.UnionType:
|
||||
const unionType = <ts.UnionTypeNode>node;
|
||||
|
||||
// Remove null and undefined from the list of unions.
|
||||
const references = unionType.types
|
||||
.filter(
|
||||
n => n.kind != ts.SyntaxKind.NullKeyword &&
|
||||
n.kind != ts.SyntaxKind.UndefinedKeyword)
|
||||
.map(n => this.evaluateNode(n));
|
||||
// TODO(alan-agius4): remove `n.kind !== ts.SyntaxKind.NullKeyword` when
|
||||
// TS 3.9 support is dropped. In TS 4.0 NullKeyword is a child of LiteralType.
|
||||
const references =
|
||||
unionType.types
|
||||
.filter(
|
||||
n => n.kind !== ts.SyntaxKind.NullKeyword &&
|
||||
n.kind !== ts.SyntaxKind.UndefinedKeyword &&
|
||||
!(ts.isLiteralTypeNode(n) && n.literal.kind === ts.SyntaxKind.NullKeyword))
|
||||
.map(n => this.evaluateNode(n));
|
||||
|
||||
// The remmaining reference must be the same. If two have type arguments consider them
|
||||
// different even if the type arguments are the same.
|
||||
|
@ -62,8 +62,8 @@ export class Symbols {
|
||||
// even if the `SourceFile` was not type checked (which looks for `SourceFile`
|
||||
// in the parent chain). This doesn't damage the node as the binder unconditionally
|
||||
// sets the parent.
|
||||
externalReference.expression.parent = externalReference;
|
||||
externalReference.parent = this.sourceFile as any;
|
||||
(externalReference.expression.parent as ts.Node) = externalReference;
|
||||
(externalReference.parent as ts.Node) = this.sourceFile;
|
||||
}
|
||||
const from = stripQuotes(externalReference.expression.getText());
|
||||
symbols.set(
|
||||
@ -83,8 +83,8 @@ export class Symbols {
|
||||
}
|
||||
if (!importDecl.moduleSpecifier.parent) {
|
||||
// See note above in the `ImportEqualDeclaration` case.
|
||||
importDecl.moduleSpecifier.parent = importDecl;
|
||||
importDecl.parent = this.sourceFile;
|
||||
(importDecl.moduleSpecifier.parent as ts.Node) = importDecl;
|
||||
(importDecl.parent as ts.Node) = this.sourceFile;
|
||||
}
|
||||
const from = stripQuotes(importDecl.moduleSpecifier.getText());
|
||||
if (importDecl.importClause.name) {
|
||||
|
@ -28,7 +28,7 @@ export function generateSetClassMetadataCall(
|
||||
if (!reflection.isClass(clazz)) {
|
||||
return null;
|
||||
}
|
||||
const id = ts.updateIdentifier(reflection.getAdjacentNameOfClass(clazz));
|
||||
const id = reflection.getAdjacentNameOfClass(clazz);
|
||||
|
||||
// Reflect over the class decorators. If none are present, or those that are aren't from
|
||||
// Angular, then return null. Otherwise, turn them into metadata.
|
||||
|
@ -69,7 +69,7 @@ runInEachFileSystem(() => {
|
||||
module: ts.ModuleKind.CommonJS,
|
||||
});
|
||||
const fooClause = getDeclaration(program, _('/test.ts'), 'Foo', ts.isImportClause);
|
||||
const fooId = ts.updateIdentifier(fooClause.name!);
|
||||
const fooId = fooClause.name!;
|
||||
const fooDecl = fooClause.parent;
|
||||
|
||||
const tracker = new DefaultImportTracker();
|
||||
|
@ -20,22 +20,26 @@ export function extractReferencesFromType(
|
||||
if (!ts.isTupleTypeNode(def)) {
|
||||
return [];
|
||||
}
|
||||
return def.elementTypes.map(element => {
|
||||
if (!ts.isTypeQueryNode(element)) {
|
||||
throw new Error(`Expected TypeQueryNode: ${nodeDebugInfo(element)}`);
|
||||
}
|
||||
const type = element.exprName;
|
||||
const {node, from} = reflectTypeEntityToDeclaration(type, checker);
|
||||
if (!isNamedClassDeclaration(node)) {
|
||||
throw new Error(`Expected named ClassDeclaration: ${nodeDebugInfo(node)}`);
|
||||
}
|
||||
const specifier = (from !== null && !from.startsWith('.') ? from : ngModuleImportedFrom);
|
||||
if (specifier !== null) {
|
||||
return new Reference(node, {specifier, resolutionContext});
|
||||
} else {
|
||||
return new Reference(node);
|
||||
}
|
||||
});
|
||||
|
||||
// TODO(alan-agius4): remove `def.elementTypes` and casts when TS 3.9 support is dropped and G3 is
|
||||
// using TS 4.0.
|
||||
return (((def as any).elements || (def as any).elementTypes) as ts.NodeArray<ts.TypeNode>)
|
||||
.map(element => {
|
||||
if (!ts.isTypeQueryNode(element)) {
|
||||
throw new Error(`Expected TypeQueryNode: ${nodeDebugInfo(element)}`);
|
||||
}
|
||||
const type = element.exprName;
|
||||
const {node, from} = reflectTypeEntityToDeclaration(type, checker);
|
||||
if (!isNamedClassDeclaration(node)) {
|
||||
throw new Error(`Expected named ClassDeclaration: ${nodeDebugInfo(node)}`);
|
||||
}
|
||||
const specifier = (from !== null && !from.startsWith('.') ? from : ngModuleImportedFrom);
|
||||
if (specifier !== null) {
|
||||
return new Reference(node, {specifier, resolutionContext});
|
||||
} else {
|
||||
return new Reference(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function readStringType(type: ts.TypeNode): string|null {
|
||||
@ -69,12 +73,15 @@ export function readStringArrayType(type: ts.TypeNode): string[] {
|
||||
return [];
|
||||
}
|
||||
const res: string[] = [];
|
||||
type.elementTypes.forEach(el => {
|
||||
if (!ts.isLiteralTypeNode(el) || !ts.isStringLiteral(el.literal)) {
|
||||
return;
|
||||
}
|
||||
res.push(el.literal.text);
|
||||
});
|
||||
// TODO(alan-agius4): remove `def.elementTypes` and casts when TS 3.9 support is dropped and G3 is
|
||||
// using TS 4.0.
|
||||
(((type as any).elements || (type as any).elementTypes) as ts.NodeArray<ts.TypeNode>)
|
||||
.forEach(el => {
|
||||
if (!ts.isLiteralTypeNode(el) || !ts.isStringLiteral(el.literal)) {
|
||||
return;
|
||||
}
|
||||
res.push(el.literal.text);
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
|
@ -35,8 +35,9 @@ export function typeToValue(
|
||||
|
||||
const {local, decl} = symbols;
|
||||
// It's only valid to convert a type reference to a value reference if the type actually
|
||||
// has a value declaration associated with it.
|
||||
if (decl.valueDeclaration === undefined) {
|
||||
// has a value declaration associated with it. Note that const enums are an exception,
|
||||
// because while they do have a value declaration, they don't exist at runtime.
|
||||
if (decl.valueDeclaration === undefined || decl.flags & ts.SymbolFlags.ConstEnum) {
|
||||
return noValueDeclaration(typeNode, decl.declarations[0]);
|
||||
}
|
||||
|
||||
@ -58,9 +59,7 @@ export function typeToValue(
|
||||
|
||||
return {
|
||||
kind: TypeValueReferenceKind.LOCAL,
|
||||
// Copying the name here ensures the generated references will be correctly transformed
|
||||
// along with the import.
|
||||
expression: ts.updateIdentifier(firstDecl.name),
|
||||
expression: firstDecl.name,
|
||||
defaultImportStatement: firstDecl.parent,
|
||||
};
|
||||
} else if (ts.isImportSpecifier(firstDecl)) {
|
||||
|
@ -64,7 +64,11 @@ export class TypeScriptReflectionHost implements ReflectionHost {
|
||||
// optional tokes that don't have providers.
|
||||
if (typeNode && ts.isUnionTypeNode(typeNode)) {
|
||||
let childTypeNodes = typeNode.types.filter(
|
||||
childTypeNode => childTypeNode.kind !== ts.SyntaxKind.NullKeyword);
|
||||
// TODO(alan-agius4): remove `childTypeNode.kind !== ts.SyntaxKind.NullKeyword` when
|
||||
// TS 3.9 support is dropped. In TS 4.0 NullKeyword is a child of LiteralType.
|
||||
childTypeNode => childTypeNode.kind !== ts.SyntaxKind.NullKeyword &&
|
||||
!(ts.isLiteralTypeNode(childTypeNode) &&
|
||||
childTypeNode.literal.kind === ts.SyntaxKind.NullKeyword));
|
||||
|
||||
if (childTypeNodes.length === 1) {
|
||||
typeNode = childTypeNodes[0];
|
||||
|
@ -128,8 +128,6 @@ function transformFactorySourceFile(
|
||||
|
||||
const {moduleSymbols, sourceFilePath} = factoryMap.get(file.fileName)!;
|
||||
|
||||
file = ts.getMutableClone(file);
|
||||
|
||||
// Not every exported factory statement is valid. They were generated before the program was
|
||||
// analyzed, and before ngtsc knew which symbols were actually NgModules. factoryMap contains
|
||||
// that knowledge now, so this transform filters the statement list and removes exported factories
|
||||
@ -221,7 +219,8 @@ function transformFactorySourceFile(
|
||||
// satisfy closure compiler.
|
||||
transformedStatements.push(nonEmptyExport);
|
||||
}
|
||||
file.statements = ts.createNodeArray(transformedStatements);
|
||||
|
||||
file = ts.updateSourceFileNode(file, transformedStatements);
|
||||
|
||||
// If any imports to @angular/core were detected and rewritten (which happens when compiling
|
||||
// @angular/core), go through the SourceFile and rewrite references to symbols imported from core.
|
||||
|
@ -42,8 +42,7 @@ function flipIvySwitchInFile(sf: ts.SourceFile): ts.SourceFile {
|
||||
|
||||
// Only update the statements in the SourceFile if any have changed.
|
||||
if (newStatements !== undefined) {
|
||||
sf = ts.getMutableClone(sf);
|
||||
sf.statements = ts.createNodeArray(newStatements);
|
||||
return ts.updateSourceFileNode(sf, newStatements);
|
||||
}
|
||||
return sf;
|
||||
}
|
||||
@ -105,16 +104,12 @@ function flipIvySwitchesInVariableStatement(
|
||||
|
||||
// Find the post-switch variable identifier. If one can't be found, it's an error. This is
|
||||
// reported as a thrown error and not a diagnostic as transformers cannot output diagnostics.
|
||||
let newIdentifier = findPostSwitchIdentifier(statements, postSwitchName);
|
||||
const newIdentifier = findPostSwitchIdentifier(statements, postSwitchName);
|
||||
if (newIdentifier === null) {
|
||||
throw new Error(`Unable to find identifier ${postSwitchName} in ${
|
||||
stmt.getSourceFile().fileName} for the Ivy switch.`);
|
||||
}
|
||||
|
||||
// Copy the identifier with updateIdentifier(). This copies the internal information which
|
||||
// allows TS to write a correct reference to the identifier.
|
||||
newIdentifier = ts.updateIdentifier(newIdentifier);
|
||||
|
||||
newDeclarations[i] = ts.updateVariableDeclaration(
|
||||
/* node */ decl,
|
||||
/* name */ decl.name,
|
||||
|
@ -28,9 +28,7 @@ export function aliasTransformFactory(exportStatements: Map<string, Map<string,
|
||||
statements.push(stmt);
|
||||
});
|
||||
|
||||
file = ts.getMutableClone(file);
|
||||
file.statements = ts.createNodeArray(statements);
|
||||
return file;
|
||||
return ts.updateSourceFileNode(file, statements);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
@ -239,32 +239,48 @@ export class ReturnTypeTransform implements DtsTransform {
|
||||
}
|
||||
|
||||
transformClassElement(element: ts.ClassElement, imports: ImportManager): ts.ClassElement {
|
||||
if (!ts.isMethodSignature(element)) {
|
||||
return element;
|
||||
// // TODO(alan-agius4): Remove when we no longer support TS 3.9
|
||||
// TS <= 3.9
|
||||
if (ts.isMethodSignature(element)) {
|
||||
const original = ts.getOriginalNode(element) as ts.MethodDeclaration;
|
||||
if (!this.typeReplacements.has(original)) {
|
||||
return element;
|
||||
}
|
||||
const returnType = this.typeReplacements.get(original)!;
|
||||
const tsReturnType = translateType(returnType, imports);
|
||||
const methodSignature = ts.updateMethodSignature(
|
||||
/* node */ element,
|
||||
/* typeParameters */ element.typeParameters,
|
||||
/* parameters */ element.parameters,
|
||||
/* type */ tsReturnType,
|
||||
/* name */ element.name,
|
||||
/* questionToken */ element.questionToken);
|
||||
|
||||
// Copy over any modifiers, these cannot be set during the `ts.updateMethodSignature` call.
|
||||
(methodSignature.modifiers as ts.ModifiersArray | undefined) = element.modifiers;
|
||||
|
||||
// A bug in the TypeScript declaration causes `ts.MethodSignature` not to be assignable to
|
||||
// `ts.ClassElement`. Since `element` was a `ts.MethodSignature` already, transforming it into
|
||||
// this type is actually correct.
|
||||
return methodSignature as unknown as ts.ClassElement;
|
||||
}
|
||||
|
||||
const original = ts.getOriginalNode(element) as ts.MethodDeclaration;
|
||||
if (!this.typeReplacements.has(original)) {
|
||||
return element;
|
||||
// TS 4.0 +
|
||||
if (ts.isMethodDeclaration(element)) {
|
||||
const original = ts.getOriginalNode(element, ts.isMethodDeclaration);
|
||||
if (!this.typeReplacements.has(original)) {
|
||||
return element;
|
||||
}
|
||||
const returnType = this.typeReplacements.get(original)!;
|
||||
const tsReturnType = translateType(returnType, imports);
|
||||
|
||||
return ts.updateMethod(
|
||||
element, element.decorators, element.modifiers, element.asteriskToken, element.name,
|
||||
element.questionToken, element.typeParameters, element.parameters, tsReturnType,
|
||||
element.body);
|
||||
}
|
||||
const returnType = this.typeReplacements.get(original)!;
|
||||
const tsReturnType = translateType(returnType, imports);
|
||||
|
||||
const methodSignature = ts.updateMethodSignature(
|
||||
/* node */ element,
|
||||
/* typeParameters */ element.typeParameters,
|
||||
/* parameters */ element.parameters,
|
||||
/* type */ tsReturnType,
|
||||
/* name */ element.name,
|
||||
/* questionToken */ element.questionToken);
|
||||
|
||||
// Copy over any modifiers, these cannot be set during the `ts.updateMethodSignature` call.
|
||||
methodSignature.modifiers = element.modifiers;
|
||||
|
||||
// A bug in the TypeScript declaration causes `ts.MethodSignature` not to be assignable to
|
||||
// `ts.ClassElement`. Since `element` was a `ts.MethodSignature` already, transforming it into
|
||||
// this type is actually correct.
|
||||
return methodSignature as unknown as ts.ClassElement;
|
||||
return element;
|
||||
}
|
||||
|
||||
transformFunctionDeclaration(element: ts.FunctionDeclaration, imports: ImportManager):
|
||||
|
@ -187,8 +187,8 @@ class IvyTransformationVisitor extends Visitor {
|
||||
|
||||
// Create a new `NodeArray` with the filtered decorators that sourcemaps back to the original.
|
||||
const array = ts.createNodeArray(filtered);
|
||||
array.pos = node.decorators.pos;
|
||||
array.end = node.decorators.end;
|
||||
(array.pos as number) = node.decorators.pos;
|
||||
(array.end as number) = node.decorators.end;
|
||||
return array;
|
||||
}
|
||||
|
||||
|
@ -40,8 +40,9 @@ export function addImports(
|
||||
// for @fileoverview Closure annotation. If there is no @fileoverview annotations, this
|
||||
// statement would be a noop.
|
||||
const fileoverviewAnchorStmt = ts.createNotEmittedStatement(sf);
|
||||
sf.statements = ts.createNodeArray(
|
||||
[fileoverviewAnchorStmt, ...existingImports, ...addedImports, ...extraStatements, ...body]);
|
||||
return ts.updateSourceFileNode(sf, ts.createNodeArray([
|
||||
fileoverviewAnchorStmt, ...existingImports, ...addedImports, ...extraStatements, ...body
|
||||
]));
|
||||
}
|
||||
|
||||
return sf;
|
||||
|
@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import {ArrayType, AssertNotNull, BinaryOperator, BinaryOperatorExpr, BuiltinType, BuiltinTypeName, CastExpr, ClassStmt, CommaExpr, CommentStmt, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, Expression, ExpressionStatement, ExpressionType, ExpressionVisitor, ExternalExpr, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, JSDocCommentStmt, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, MapType, NotExpr, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, Statement, StatementVisitor, StmtModifier, ThrowStmt, TryCatchStmt, Type, TypeofExpr, TypeVisitor, WrappedNodeExpr, WriteKeyExpr, WritePropExpr, WriteVarExpr} from '@angular/compiler';
|
||||
import {LocalizedString} from '@angular/compiler/src/output/output_ast';
|
||||
import {LocalizedString, UnaryOperator, UnaryOperatorExpr} from '@angular/compiler/src/output/output_ast';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {DefaultImportRecorder, ImportRewriter, NOOP_DEFAULT_IMPORT_RECORDER, NoopImportRewriter} from '../../imports';
|
||||
@ -24,6 +24,11 @@ export class Context {
|
||||
}
|
||||
}
|
||||
|
||||
const UNARY_OPERATORS = new Map<UnaryOperator, ts.PrefixUnaryOperator>([
|
||||
[UnaryOperator.Minus, ts.SyntaxKind.MinusToken],
|
||||
[UnaryOperator.Plus, ts.SyntaxKind.PlusToken],
|
||||
]);
|
||||
|
||||
const BINARY_OPERATORS = new Map<BinaryOperator, ts.BinaryOperator>([
|
||||
[BinaryOperator.And, ts.SyntaxKind.AmpersandAmpersandToken],
|
||||
[BinaryOperator.Bigger, ts.SyntaxKind.GreaterThanToken],
|
||||
@ -361,6 +366,14 @@ class ExpressionTranslatorVisitor implements ExpressionVisitor, StatementVisitor
|
||||
undefined, ts.createBlock(ast.statements.map(stmt => stmt.visitStatement(this, context))));
|
||||
}
|
||||
|
||||
visitUnaryOperatorExpr(ast: UnaryOperatorExpr, context: Context): ts.Expression {
|
||||
if (!UNARY_OPERATORS.has(ast.operator)) {
|
||||
throw new Error(`Unknown unary operator: ${UnaryOperator[ast.operator]}`);
|
||||
}
|
||||
return ts.createPrefix(
|
||||
UNARY_OPERATORS.get(ast.operator)!, ast.expr.visitExpression(this, context));
|
||||
}
|
||||
|
||||
visitBinaryOperatorExpr(ast: BinaryOperatorExpr, context: Context): ts.Expression {
|
||||
if (!BINARY_OPERATORS.has(ast.operator)) {
|
||||
throw new Error(`Unknown binary operator: ${BinaryOperator[ast.operator]}`);
|
||||
@ -513,7 +526,11 @@ export class TypeTranslatorVisitor implements ExpressionVisitor, TypeVisitor {
|
||||
|
||||
visitLiteralExpr(ast: LiteralExpr, context: Context): ts.TypeNode {
|
||||
if (ast.value === null) {
|
||||
return ts.createKeywordTypeNode(ts.SyntaxKind.NullKeyword);
|
||||
// TODO(alan-agius4): Remove when we no longer support TS 3.9
|
||||
// Use: return ts.createLiteralTypeNode(ts.createNull()) directly.
|
||||
return ts.versionMajorMinor.charAt(0) === '4' ?
|
||||
ts.createLiteralTypeNode(ts.createNull() as any) :
|
||||
ts.createKeywordTypeNode(ts.SyntaxKind.NullKeyword as any);
|
||||
} else if (ast.value === undefined) {
|
||||
return ts.createKeywordTypeNode(ts.SyntaxKind.UndefinedKeyword);
|
||||
} else if (typeof ast.value === 'boolean') {
|
||||
@ -567,6 +584,10 @@ export class TypeTranslatorVisitor implements ExpressionVisitor, TypeVisitor {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
visitUnaryOperatorExpr(ast: UnaryOperatorExpr, context: Context) {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
visitBinaryOperatorExpr(ast: BinaryOperatorExpr, context: Context) {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
@ -679,7 +700,7 @@ function createLocalizedStringTaggedTemplate(
|
||||
// Revert once https://github.com/microsoft/TypeScript/issues/35374 is fixed
|
||||
function createTemplateMiddle(cooked: string, raw: string): ts.TemplateMiddle {
|
||||
const node: ts.TemplateLiteralLikeNode = ts.createTemplateHead(cooked, raw);
|
||||
node.kind = ts.SyntaxKind.TemplateMiddle;
|
||||
(node.kind as ts.SyntaxKind) = ts.SyntaxKind.TemplateMiddle;
|
||||
return node as ts.TemplateMiddle;
|
||||
}
|
||||
|
||||
@ -687,7 +708,7 @@ function createTemplateMiddle(cooked: string, raw: string): ts.TemplateMiddle {
|
||||
// Revert once https://github.com/microsoft/TypeScript/issues/35374 is fixed
|
||||
function createTemplateTail(cooked: string, raw: string): ts.TemplateTail {
|
||||
const node: ts.TemplateLiteralLikeNode = ts.createTemplateHead(cooked, raw);
|
||||
node.kind = ts.SyntaxKind.TemplateTail;
|
||||
(node.kind as ts.SyntaxKind) = ts.SyntaxKind.TemplateTail;
|
||||
return node as ts.TemplateTail;
|
||||
}
|
||||
|
||||
|
@ -28,6 +28,14 @@ export interface TemplateTypeChecker {
|
||||
*/
|
||||
resetOverrides(): void;
|
||||
|
||||
/**
|
||||
* Retrieve the template in use for the given component.
|
||||
*
|
||||
* If the template has been overridden via `overrideComponentTemplate`, this will retrieve the
|
||||
* overridden template nodes.
|
||||
*/
|
||||
getTemplate(component: ts.ClassDeclaration): TmplAstNode[]|null;
|
||||
|
||||
/**
|
||||
* Provide a new template string that will be used in place of the user-defined template when
|
||||
* checking or operating on the given component.
|
||||
|
@ -48,6 +48,29 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
|
||||
}
|
||||
}
|
||||
|
||||
getTemplate(component: ts.ClassDeclaration): TmplAstNode[]|null {
|
||||
this.ensureShimForComponent(component);
|
||||
|
||||
const sf = component.getSourceFile();
|
||||
const sfPath = absoluteFromSourceFile(sf);
|
||||
const shimPath = this.typeCheckingStrategy.shimPathForComponent(component);
|
||||
|
||||
const fileRecord = this.getFileData(sfPath);
|
||||
|
||||
if (!fileRecord.shimData.has(shimPath)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const templateId = fileRecord.sourceManager.getTemplateId(component);
|
||||
const shimRecord = fileRecord.shimData.get(shimPath)!;
|
||||
|
||||
if (!shimRecord.templates.has(templateId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return shimRecord.templates.get(templateId)!.template;
|
||||
}
|
||||
|
||||
overrideComponentTemplate(component: ts.ClassDeclaration, template: string):
|
||||
{nodes: TmplAstNode[], errors?: ParseError[]} {
|
||||
const {nodes, errors} = parseTemplate(template, 'override.html', {
|
||||
|
@ -6,14 +6,14 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {ParseSourceFile, R3TargetBinder, SchemaMetadata, TmplAstNode} from '@angular/compiler';
|
||||
import {BoundTarget, ParseSourceFile, R3TargetBinder, SchemaMetadata, TmplAstNode} from '@angular/compiler';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {absoluteFromSourceFile, AbsoluteFsPath} from '../../file_system';
|
||||
import {NoopImportRewriter, Reference, ReferenceEmitter} from '../../imports';
|
||||
import {ClassDeclaration, ReflectionHost} from '../../reflection';
|
||||
import {ImportManager} from '../../translator';
|
||||
import {ComponentToShimMappingStrategy, TemplateSourceMapping, TypeCheckableDirectiveMeta, TypeCheckBlockMetadata, TypeCheckContext, TypeCheckingConfig, TypeCtorMetadata} from '../api';
|
||||
import {ComponentToShimMappingStrategy, TemplateId, TemplateSourceMapping, TypeCheckableDirectiveMeta, TypeCheckBlockMetadata, TypeCheckContext, TypeCheckingConfig, TypeCtorMetadata} from '../api';
|
||||
|
||||
import {TemplateDiagnostic} from './diagnostics';
|
||||
import {DomSchemaChecker, RegistryDomSchemaChecker} from './dom';
|
||||
@ -41,6 +41,28 @@ export interface ShimTypeCheckingData {
|
||||
* Whether any inline operations for the input file were required to generate this shim.
|
||||
*/
|
||||
hasInlines: boolean;
|
||||
|
||||
/**
|
||||
* Map of `TemplateId` to information collected about the template during the template
|
||||
* type-checking process.
|
||||
*/
|
||||
templates: Map<TemplateId, TemplateData>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Data tracked for each template processed by the template type-checking system.
|
||||
*/
|
||||
export interface TemplateData {
|
||||
/**
|
||||
* Template nodes for which the TCB was generated.
|
||||
*/
|
||||
template: TmplAstNode[];
|
||||
|
||||
/**
|
||||
* `BoundTarget` which was used to generate the TCB, and contains bindings for the associated
|
||||
* template nodes.
|
||||
*/
|
||||
boundTarget: BoundTarget<TypeCheckableDirectiveMeta>;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -79,6 +101,12 @@ export interface PendingShimData {
|
||||
* Shim file in the process of being generated.
|
||||
*/
|
||||
file: TypeCheckFile;
|
||||
|
||||
|
||||
/**
|
||||
* Map of `TemplateId` to information collected about the template as it's ingested.
|
||||
*/
|
||||
templates: Map<TemplateId, TemplateData>;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -195,6 +223,7 @@ export class TypeCheckContextImpl implements TypeCheckContext {
|
||||
const fileData = this.dataForFile(ref.node.getSourceFile());
|
||||
const shimData = this.pendingShimForComponent(ref.node);
|
||||
const boundTarget = binder.bind({template});
|
||||
|
||||
// Get all of the directives used in the template and record type constructors for all of them.
|
||||
for (const dir of boundTarget.getUsedDirectives()) {
|
||||
const dirRef = dir.ref as Reference<ClassDeclaration<ts.ClassDeclaration>>;
|
||||
@ -221,6 +250,11 @@ export class TypeCheckContextImpl implements TypeCheckContext {
|
||||
});
|
||||
}
|
||||
}
|
||||
const templateId = fileData.sourceManager.getTemplateId(ref.node);
|
||||
shimData.templates.set(templateId, {
|
||||
template,
|
||||
boundTarget,
|
||||
});
|
||||
|
||||
const tcbRequiresInline = requiresInlineTypeCheckBlock(ref.node);
|
||||
|
||||
@ -231,7 +265,6 @@ export class TypeCheckContextImpl implements TypeCheckContext {
|
||||
// and inlining would be required.
|
||||
|
||||
// Record diagnostics to indicate the issues with this template.
|
||||
const templateId = fileData.sourceManager.getTemplateId(ref.node);
|
||||
if (tcbRequiresInline) {
|
||||
shimData.oobRecorder.requiresInlineTcb(templateId, ref.node);
|
||||
}
|
||||
@ -348,6 +381,7 @@ export class TypeCheckContextImpl implements TypeCheckContext {
|
||||
],
|
||||
hasInlines: pendingFileData.hasInlines,
|
||||
path: pendingShimData.file.fileName,
|
||||
templates: pendingShimData.templates,
|
||||
});
|
||||
updates.set(pendingShimData.file.fileName, pendingShimData.file.render());
|
||||
}
|
||||
@ -380,6 +414,7 @@ export class TypeCheckContextImpl implements TypeCheckContext {
|
||||
oobRecorder: new OutOfBandDiagnosticRecorderImpl(fileData.sourceManager),
|
||||
file: new TypeCheckFile(
|
||||
shimPath, this.config, this.refEmitter, this.reflector, this.compilerHost),
|
||||
templates: new Map<TemplateId, TemplateData>(),
|
||||
});
|
||||
}
|
||||
return fileData.shimData.get(shimPath)!;
|
||||
|
@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {AST, AstVisitor, ASTWithSource, Binary, BindingPipe, Chain, Conditional, EmptyExpr, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, NonNullAssert, PrefixNot, PropertyRead, PropertyWrite, Quote, SafeMethodCall, SafePropertyRead} from '@angular/compiler';
|
||||
import {AST, AstVisitor, ASTWithSource, Binary, BindingPipe, Chain, Conditional, EmptyExpr, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, NonNullAssert, PrefixNot, PropertyRead, PropertyWrite, Quote, SafeMethodCall, SafePropertyRead, Unary} from '@angular/compiler';
|
||||
import * as ts from 'typescript';
|
||||
import {TypeCheckingConfig} from '../api';
|
||||
|
||||
@ -17,7 +17,12 @@ export const NULL_AS_ANY =
|
||||
ts.createAsExpression(ts.createNull(), ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword));
|
||||
const UNDEFINED = ts.createIdentifier('undefined');
|
||||
|
||||
const BINARY_OPS = new Map<string, ts.SyntaxKind>([
|
||||
const UNARY_OPS = new Map<string, ts.PrefixUnaryOperator>([
|
||||
['+', ts.SyntaxKind.PlusToken],
|
||||
['-', ts.SyntaxKind.MinusToken],
|
||||
]);
|
||||
|
||||
const BINARY_OPS = new Map<string, ts.BinaryOperator>([
|
||||
['+', ts.SyntaxKind.PlusToken],
|
||||
['-', ts.SyntaxKind.MinusToken],
|
||||
['<', ts.SyntaxKind.LessThanToken],
|
||||
@ -74,6 +79,17 @@ class AstTranslator implements AstVisitor {
|
||||
return ast.visit(this);
|
||||
}
|
||||
|
||||
visitUnary(ast: Unary): ts.Expression {
|
||||
const expr = this.translate(ast.expr);
|
||||
const op = UNARY_OPS.get(ast.operator);
|
||||
if (op === undefined) {
|
||||
throw new Error(`Unsupported Unary.operator: ${ast.operator}`);
|
||||
}
|
||||
const node = wrapForDiagnostics(ts.createPrefix(op, expr));
|
||||
addParseSpanInfo(node, ast.sourceSpan);
|
||||
return node;
|
||||
}
|
||||
|
||||
visitBinary(ast: Binary): ts.Expression {
|
||||
const lhs = wrapForDiagnostics(this.translate(ast.left));
|
||||
const rhs = wrapForDiagnostics(this.translate(ast.right));
|
||||
@ -81,7 +97,7 @@ class AstTranslator implements AstVisitor {
|
||||
if (op === undefined) {
|
||||
throw new Error(`Unsupported Binary.operation: ${ast.operation}`);
|
||||
}
|
||||
const node = ts.createBinary(lhs, op as any, rhs);
|
||||
const node = ts.createBinary(lhs, op, rhs);
|
||||
addParseSpanInfo(node, ast.sourceSpan);
|
||||
return node;
|
||||
}
|
||||
@ -314,6 +330,9 @@ class VeSafeLhsInferenceBugDetector implements AstVisitor {
|
||||
return ast.receiver.visit(VeSafeLhsInferenceBugDetector.SINGLETON);
|
||||
}
|
||||
|
||||
visitUnary(ast: Unary): boolean {
|
||||
return ast.expr.visit(this);
|
||||
}
|
||||
visitBinary(ast: Binary): boolean {
|
||||
return ast.left.visit(this) || ast.right.visit(this);
|
||||
}
|
||||
|
@ -36,6 +36,7 @@ export function canEmitType(type: ts.TypeNode, resolver: TypeReferenceResolver):
|
||||
visitTypeReferenceNode: type => canEmitTypeReference(type),
|
||||
visitArrayTypeNode: type => canEmitTypeWorker(type.elementType),
|
||||
visitKeywordType: () => true,
|
||||
visitLiteralType: () => true,
|
||||
visitOtherType: () => false,
|
||||
});
|
||||
}
|
||||
@ -111,6 +112,7 @@ export class TypeEmitter {
|
||||
visitTypeReferenceNode: type => this.emitTypeReference(type),
|
||||
visitArrayTypeNode: type => ts.updateArrayTypeNode(type, this.emitType(type.elementType)),
|
||||
visitKeywordType: type => type,
|
||||
visitLiteralType: type => type,
|
||||
visitOtherType: () => {
|
||||
throw new Error('Unable to emit a complex type');
|
||||
},
|
||||
@ -159,6 +161,7 @@ interface TypeEmitterVisitor<R> {
|
||||
visitTypeReferenceNode(type: ts.TypeReferenceNode): R;
|
||||
visitArrayTypeNode(type: ts.ArrayTypeNode): R;
|
||||
visitKeywordType(type: ts.KeywordTypeNode): R;
|
||||
visitLiteralType(type: ts.LiteralTypeNode): R;
|
||||
visitOtherType(type: ts.TypeNode): R;
|
||||
}
|
||||
|
||||
@ -167,6 +170,8 @@ function visitTypeNode<R>(type: ts.TypeNode, visitor: TypeEmitterVisitor<R>): R
|
||||
return visitor.visitTypeReferenceNode(type);
|
||||
} else if (ts.isArrayTypeNode(type)) {
|
||||
return visitor.visitArrayTypeNode(type);
|
||||
} else if (ts.isLiteralTypeNode(type)) {
|
||||
return visitor.visitLiteralType(type);
|
||||
}
|
||||
|
||||
switch (type.kind) {
|
||||
|
@ -248,6 +248,17 @@ runInEachFileSystem(() => {
|
||||
expect(messages).toEqual([]);
|
||||
});
|
||||
|
||||
it('should treat unary operators as literal types', () => {
|
||||
const messages = diagnose(`{{ test(-1) + test(+1) + test(-2) }}`, `
|
||||
class TestComponent {
|
||||
test(value: -1 | 1): number { return value; }
|
||||
}`);
|
||||
|
||||
expect(messages).toEqual([
|
||||
`TestComponent.html(1, 31): Argument of type '-2' is not assignable to parameter of type '1 | -1'.`,
|
||||
]);
|
||||
});
|
||||
|
||||
describe('outputs', () => {
|
||||
it('should produce a diagnostic for directive outputs', () => {
|
||||
const messages = diagnose(
|
||||
@ -445,7 +456,7 @@ class TestComponent {
|
||||
}`);
|
||||
|
||||
expect(messages).toEqual(
|
||||
[`TestComponent.html(1, 15): Type '2' is not assignable to type 'string'.`]);
|
||||
[`TestComponent.html(1, 15): Type 'number' is not assignable to type 'string'.`]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -10,6 +10,10 @@ import {tcb, TestDeclaration} from './test_utils';
|
||||
|
||||
describe('type check blocks diagnostics', () => {
|
||||
describe('parse spans', () => {
|
||||
it('should annotate unary ops', () => {
|
||||
expect(tcbWithSpans('{{ -a }}')).toContain('(-((ctx).a /*4,5*/) /*4,5*/) /*3,5*/');
|
||||
});
|
||||
|
||||
it('should annotate binary ops', () => {
|
||||
expect(tcbWithSpans('{{ a + b }}'))
|
||||
.toContain('(((ctx).a /*3,4*/) /*3,4*/) + (((ctx).b /*7,8*/) /*7,8*/) /*3,8*/');
|
||||
|
@ -31,6 +31,11 @@ describe('type check blocks', () => {
|
||||
expect(tcb(TEMPLATE)).toContain('((((ctx).a))!);');
|
||||
});
|
||||
|
||||
it('should handle unary - operator', () => {
|
||||
const TEMPLATE = `{{-1}}`;
|
||||
expect(tcb(TEMPLATE)).toContain('(-1);');
|
||||
});
|
||||
|
||||
it('should handle keyed property access', () => {
|
||||
const TEMPLATE = `{{a[b]}}`;
|
||||
expect(tcb(TEMPLATE)).toContain('(((ctx).a))[((ctx).b)];');
|
||||
|
@ -353,5 +353,40 @@ runInEachFileSystem(os => {
|
||||
expect(diags2[0].messageText).toContain('invalid-element-b');
|
||||
expect(diags2[0].messageText).not.toContain('invalid-element-a');
|
||||
});
|
||||
|
||||
describe('getTemplateOfComponent()', () => {
|
||||
it('should provide access to a component\'s real template', () => {
|
||||
const fileName = absoluteFrom('/main.ts');
|
||||
const {program, templateTypeChecker} = setup([{
|
||||
fileName,
|
||||
templates: {
|
||||
'Cmp': '<div>Template</div>',
|
||||
},
|
||||
}]);
|
||||
const cmp = getClass(getSourceFileOrError(program, fileName), 'Cmp');
|
||||
|
||||
const nodes = templateTypeChecker.getTemplate(cmp)!;
|
||||
expect(nodes).not.toBeNull();
|
||||
expect(nodes[0].sourceSpan.start.file.content).toBe('<div>Template</div>');
|
||||
});
|
||||
|
||||
it('should provide access to an overridden template', () => {
|
||||
const fileName = absoluteFrom('/main.ts');
|
||||
const {program, templateTypeChecker} = setup([{
|
||||
fileName,
|
||||
templates: {
|
||||
'Cmp': '<div>Template</div>',
|
||||
},
|
||||
}]);
|
||||
const cmp = getClass(getSourceFileOrError(program, fileName), 'Cmp');
|
||||
|
||||
templateTypeChecker.overrideComponentTemplate(cmp, '<div>Overridden</div>');
|
||||
templateTypeChecker.getDiagnosticsForComponent(cmp);
|
||||
|
||||
const nodes = templateTypeChecker.getTemplate(cmp)!;
|
||||
expect(nodes).not.toBeNull();
|
||||
expect(nodes[0].sourceSpan.start.file.content).toBe('<div>Overridden</div>');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -140,10 +140,16 @@ function createCtorParametersClassPropertyType(): ts.TypeNode {
|
||||
undefined),
|
||||
])),
|
||||
undefined));
|
||||
return ts.createFunctionTypeNode(
|
||||
undefined, [],
|
||||
ts.createArrayTypeNode(
|
||||
ts.createUnionTypeNode([ts.createTypeLiteralNode(typeElements), ts.createNull()])));
|
||||
|
||||
// TODO(alan-agius4): Remove when we no longer support TS 3.9
|
||||
const nullLiteral = ts.createNull() as any;
|
||||
const nullType = ts.versionMajorMinor.charAt(0) === '4' ?
|
||||
ts.createLiteralTypeNode(nullLiteral as any) :
|
||||
nullLiteral;
|
||||
return ts.createFunctionTypeNode(undefined, [], ts.createArrayTypeNode(ts.createUnionTypeNode([
|
||||
ts.createTypeLiteralNode(typeElements),
|
||||
nullType,
|
||||
])));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -287,8 +293,13 @@ function typeReferenceToExpression(
|
||||
// Ignore any generic types, just return the base type.
|
||||
return entityNameToExpression(typeRef.typeName);
|
||||
case ts.SyntaxKind.UnionType:
|
||||
// TODO(alan-agius4): remove `t.kind !== ts.SyntaxKind.NullKeyword` when
|
||||
// TS 3.9 support is dropped. In TS 4.0 NullKeyword is a child of LiteralType.
|
||||
const childTypeNodes =
|
||||
(node as ts.UnionTypeNode).types.filter(t => t.kind !== ts.SyntaxKind.NullKeyword);
|
||||
(node as ts.UnionTypeNode)
|
||||
.types.filter(
|
||||
t => t.kind !== ts.SyntaxKind.NullKeyword &&
|
||||
!(ts.isLiteralTypeNode(t) && t.literal.kind === ts.SyntaxKind.NullKeyword));
|
||||
return childTypeNodes.length === 1 ?
|
||||
typeReferenceToExpression(entityNameToExpression, childTypeNodes[0]) :
|
||||
undefined;
|
||||
@ -298,15 +309,20 @@ function typeReferenceToExpression(
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given symbol refers to a value (as distinct from a type).
|
||||
* Checks whether a given symbol refers to a value that exists at runtime (as distinct from a type).
|
||||
*
|
||||
* Expands aliases, which is important for the case where
|
||||
* import * as x from 'some-module';
|
||||
* and x is now a value (the module object).
|
||||
*/
|
||||
function symbolIsValue(tc: ts.TypeChecker, sym: ts.Symbol): boolean {
|
||||
if (sym.flags & ts.SymbolFlags.Alias) sym = tc.getAliasedSymbol(sym);
|
||||
return (sym.flags & ts.SymbolFlags.Value) !== 0;
|
||||
function symbolIsRuntimeValue(typeChecker: ts.TypeChecker, symbol: ts.Symbol): boolean {
|
||||
if (symbol.flags & ts.SymbolFlags.Alias) {
|
||||
symbol = typeChecker.getAliasedSymbol(symbol);
|
||||
}
|
||||
|
||||
// Note that const enums are a special case, because
|
||||
// while they have a value, they don't exist at runtime.
|
||||
return (symbol.flags & ts.SymbolFlags.Value & ts.SymbolFlags.ConstEnumExcludes) !== 0;
|
||||
}
|
||||
|
||||
/** ParameterDecorationInfo describes the information for a single constructor parameter. */
|
||||
@ -351,7 +367,7 @@ export function getDownlevelDecoratorsTransform(
|
||||
const symbol = typeChecker.getSymbolAtLocation(name);
|
||||
// Check if the entity name references a symbol that is an actual value. If it is not, it
|
||||
// cannot be referenced by an expression, so return undefined.
|
||||
if (!symbol || !symbolIsValue(typeChecker, symbol) || !symbol.declarations ||
|
||||
if (!symbol || !symbolIsRuntimeValue(typeChecker, symbol) || !symbol.declarations ||
|
||||
symbol.declarations.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
@ -429,7 +445,7 @@ export function getDownlevelDecoratorsTransform(
|
||||
|
||||
const name = (element.name as ts.Identifier).text;
|
||||
const mutable = ts.getMutableClone(element);
|
||||
mutable.decorators = decoratorsToKeep.length ?
|
||||
(mutable as any).decorators = decoratorsToKeep.length ?
|
||||
ts.setTextRange(ts.createNodeArray(decoratorsToKeep), mutable.decorators) :
|
||||
undefined;
|
||||
return [name, mutable, toLower];
|
||||
@ -546,8 +562,6 @@ export function getDownlevelDecoratorsTransform(
|
||||
}
|
||||
}
|
||||
|
||||
const newClassDeclaration = ts.getMutableClone(classDecl);
|
||||
|
||||
if (decoratorsToLower.length) {
|
||||
newMembers.push(createDecoratorClassProperty(decoratorsToLower));
|
||||
}
|
||||
@ -562,12 +576,13 @@ export function getDownlevelDecoratorsTransform(
|
||||
if (decoratedProperties.size) {
|
||||
newMembers.push(createPropDecoratorsClassProperty(diagnostics, decoratedProperties));
|
||||
}
|
||||
newClassDeclaration.members = ts.setTextRange(
|
||||
ts.createNodeArray(newMembers, newClassDeclaration.members.hasTrailingComma),
|
||||
classDecl.members);
|
||||
newClassDeclaration.decorators =
|
||||
decoratorsToKeep.length ? ts.createNodeArray(decoratorsToKeep) : undefined;
|
||||
return newClassDeclaration;
|
||||
|
||||
const members = ts.setTextRange(
|
||||
ts.createNodeArray(newMembers, classDecl.members.hasTrailingComma), classDecl.members);
|
||||
|
||||
return ts.updateClassDeclaration(
|
||||
classDecl, decoratorsToKeep.length ? decoratorsToKeep : undefined, classDecl.modifiers,
|
||||
classDecl.name, classDecl.typeParameters, classDecl.heritageClauses, members);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -167,14 +167,13 @@ function transformSourceFile(
|
||||
|
||||
newStatements = tmpStatements;
|
||||
}
|
||||
// Note: We cannot use ts.updateSourcefile here as
|
||||
// it does not work well with decorators.
|
||||
// See https://github.com/Microsoft/TypeScript/issues/17384
|
||||
const newSf = ts.getMutableClone(sourceFile);
|
||||
|
||||
const newSf = ts.updateSourceFileNode(
|
||||
sourceFile, ts.setTextRange(ts.createNodeArray(newStatements), sourceFile.statements));
|
||||
if (!(sourceFile.flags & ts.NodeFlags.Synthesized)) {
|
||||
newSf.flags &= ~ts.NodeFlags.Synthesized;
|
||||
(newSf.flags as ts.NodeFlags) &= ~ts.NodeFlags.Synthesized;
|
||||
}
|
||||
newSf.statements = ts.setTextRange(ts.createNodeArray(newStatements), sourceFile.statements);
|
||||
|
||||
return newSf;
|
||||
}
|
||||
|
||||
@ -209,11 +208,6 @@ export interface RequestsMap {
|
||||
getRequests(sourceFile: ts.SourceFile): RequestLocationMap;
|
||||
}
|
||||
|
||||
interface MetadataAndLoweringRequests {
|
||||
metadata: ModuleMetadata|undefined;
|
||||
requests: RequestLocationMap;
|
||||
}
|
||||
|
||||
function isEligibleForLowering(node: ts.Node|undefined): boolean {
|
||||
if (node) {
|
||||
switch (node.kind) {
|
||||
|
@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import {AssertNotNull, BinaryOperator, BinaryOperatorExpr, BuiltinMethod, BuiltinVar, CastExpr, ClassStmt, CommaExpr, CommentStmt, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, ExpressionStatement, ExpressionVisitor, ExternalExpr, ExternalReference, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, JSDocCommentStmt, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, NotExpr, ParseSourceFile, ParseSourceSpan, PartialModule, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, Statement, StatementVisitor, StmtModifier, ThrowStmt, TryCatchStmt, TypeofExpr, WrappedNodeExpr, WriteKeyExpr, WritePropExpr, WriteVarExpr} from '@angular/compiler';
|
||||
import {LocalizedString} from '@angular/compiler/src/output/output_ast';
|
||||
import {LocalizedString, UnaryOperator, UnaryOperatorExpr} from '@angular/compiler/src/output/output_ast';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {error} from './util';
|
||||
@ -622,6 +622,23 @@ export class NodeEmitterVisitor implements StatementVisitor, ExpressionVisitor {
|
||||
/* type */ undefined, this._visitStatements(expr.statements)));
|
||||
}
|
||||
|
||||
visitUnaryOperatorExpr(expr: UnaryOperatorExpr):
|
||||
RecordedNode<ts.UnaryExpression|ts.ParenthesizedExpression> {
|
||||
let unaryOperator: ts.BinaryOperator;
|
||||
switch (expr.operator) {
|
||||
case UnaryOperator.Minus:
|
||||
unaryOperator = ts.SyntaxKind.MinusToken;
|
||||
break;
|
||||
case UnaryOperator.Plus:
|
||||
unaryOperator = ts.SyntaxKind.PlusToken;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown operator: ${expr.operator}`);
|
||||
}
|
||||
const binary = ts.createPrefix(unaryOperator, expr.expr.visitExpression(this, null));
|
||||
return this.record(expr, expr.parens ? ts.createParen(binary) : binary);
|
||||
}
|
||||
|
||||
visitBinaryOperatorExpr(expr: BinaryOperatorExpr):
|
||||
RecordedNode<ts.BinaryExpression|ts.ParenthesizedExpression> {
|
||||
let binaryOperator: ts.BinaryOperator;
|
||||
|
@ -19,7 +19,7 @@ const MIN_TS_VERSION = '3.9.2';
|
||||
* ∀ supported typescript version v, v < MAX_TS_VERSION
|
||||
* MAX_TS_VERSION is not considered as a supported TypeScript version
|
||||
*/
|
||||
const MAX_TS_VERSION = '4.0.0';
|
||||
const MAX_TS_VERSION = '4.1.0';
|
||||
|
||||
/**
|
||||
* The currently used version of TypeScript, which can be adjusted for testing purposes using
|
||||
|
@ -787,7 +787,7 @@ describe('ng type checker', () => {
|
||||
it('should report an invalid call to a pipe', () => {
|
||||
rejectOnlyWithFullTemplateTypeCheck(
|
||||
'<div>{{"hello" | aPipe}}</div>',
|
||||
`Argument of type '"hello"' is not assignable to parameter of type 'number'.`, '0:5');
|
||||
`Argument of type 'string' is not assignable to parameter of type 'number'.`, '0:5');
|
||||
});
|
||||
it('should report an invalid property on an exportAs directive', () => {
|
||||
rejectOnlyWithFullTemplateTypeCheck(
|
||||
|
@ -4369,6 +4369,53 @@ runInEachFileSystem(os => {
|
||||
expect(jsContents).toMatch(setClassMetadataRegExp('type: undefined'));
|
||||
});
|
||||
|
||||
it('should use `undefined` in setClassMetadata for const enums', () => {
|
||||
env.write(`keycodes.ts`, `
|
||||
export const enum KeyCodes {A, B};
|
||||
`);
|
||||
env.write(`test.ts`, `
|
||||
import {Component, Inject} from '@angular/core';
|
||||
import {KeyCodes} from './keycodes';
|
||||
|
||||
@Component({
|
||||
selector: 'some-comp',
|
||||
template: '...',
|
||||
})
|
||||
export class SomeComp {
|
||||
constructor(@Inject('arg-token') arg: KeyCodes) {}
|
||||
}
|
||||
`);
|
||||
|
||||
env.driveMain();
|
||||
const jsContents = trim(env.getContents('test.js'));
|
||||
expect(jsContents).not.toContain(`import { KeyCodes } from './keycodes';`);
|
||||
// Note: `type: undefined` below, since KeyCodes can't be represented as a value
|
||||
expect(jsContents).toMatch(setClassMetadataRegExp('type: undefined'));
|
||||
});
|
||||
|
||||
it('should preserve the types of non-const enums in setClassMetadata', () => {
|
||||
env.write(`keycodes.ts`, `
|
||||
export enum KeyCodes {A, B};
|
||||
`);
|
||||
env.write(`test.ts`, `
|
||||
import {Component, Inject} from '@angular/core';
|
||||
import {KeyCodes} from './keycodes';
|
||||
|
||||
@Component({
|
||||
selector: 'some-comp',
|
||||
template: '...',
|
||||
})
|
||||
export class SomeComp {
|
||||
constructor(@Inject('arg-token') arg: KeyCodes) {}
|
||||
}
|
||||
`);
|
||||
|
||||
env.driveMain();
|
||||
const jsContents = trim(env.getContents('test.js'));
|
||||
expect(jsContents).toContain(`import { KeyCodes } from './keycodes';`);
|
||||
expect(jsContents).toMatch(setClassMetadataRegExp('type: i1.KeyCodes'));
|
||||
});
|
||||
|
||||
it('should use `undefined` in setClassMetadata if types originate from type-only imports',
|
||||
() => {
|
||||
env.write(`types.ts`, `
|
||||
|
@ -136,7 +136,7 @@ export declare class AnimationEvent {
|
||||
|
||||
const diags = env.driveDiagnostics();
|
||||
expect(diags.length).toBe(1);
|
||||
expect(diags[0].messageText).toEqual(`Type '"2"' is not assignable to type 'number'.`);
|
||||
expect(diags[0].messageText).toEqual(`Type 'string' is not assignable to type 'number'.`);
|
||||
// The reported error code should be in the TS error space, not a -99 "NG" code.
|
||||
expect(diags[0].code).toBeGreaterThan(0);
|
||||
});
|
||||
@ -168,8 +168,8 @@ export declare class AnimationEvent {
|
||||
|
||||
const diags = env.driveDiagnostics();
|
||||
expect(diags.length).toBe(2);
|
||||
expect(diags[0].messageText).toEqual(`Type '"2"' is not assignable to type 'number'.`);
|
||||
expect(diags[1].messageText).toEqual(`Type '"2"' is not assignable to type 'number'.`);
|
||||
expect(diags[0].messageText).toEqual(`Type 'string' is not assignable to type 'number'.`);
|
||||
expect(diags[1].messageText).toEqual(`Type 'string' is not assignable to type 'number'.`);
|
||||
});
|
||||
|
||||
it('should support inputs and outputs with names that are not JavaScript identifiers', () => {
|
||||
@ -204,7 +204,7 @@ export declare class AnimationEvent {
|
||||
|
||||
const diags = env.driveDiagnostics();
|
||||
expect(diags.length).toBe(2);
|
||||
expect(diags[0].messageText).toEqual(`Type '2' is not assignable to type 'string'.`);
|
||||
expect(diags[0].messageText).toEqual(`Type 'number' is not assignable to type 'string'.`);
|
||||
expect(diags[1].messageText)
|
||||
.toEqual(`Argument of type 'string' is not assignable to parameter of type 'number'.`);
|
||||
});
|
||||
@ -380,7 +380,7 @@ export declare class AnimationEvent {
|
||||
|
||||
const diags = env.driveDiagnostics();
|
||||
expect(diags.length).toBe(2);
|
||||
expect(diags[0].messageText).toEqual(`Type '1' is not assignable to type 'string'.`);
|
||||
expect(diags[0].messageText).toEqual(`Type 'number' is not assignable to type 'string'.`);
|
||||
expect(diags[1].messageText)
|
||||
.toEqual(`Property 'invalid' does not exist on type 'TestCmp'.`);
|
||||
});
|
||||
@ -390,7 +390,7 @@ export declare class AnimationEvent {
|
||||
|
||||
const diags = env.driveDiagnostics();
|
||||
expect(diags.length).toBe(2);
|
||||
expect(diags[0].messageText).toEqual(`Type '1' is not assignable to type 'string'.`);
|
||||
expect(diags[0].messageText).toEqual(`Type 'number' is not assignable to type 'string'.`);
|
||||
expect(diags[1].messageText)
|
||||
.toEqual(`Property 'invalid' does not exist on type 'TestCmp'.`);
|
||||
});
|
||||
@ -716,8 +716,8 @@ export declare class AnimationEvent {
|
||||
|
||||
const diags = env.driveDiagnostics();
|
||||
expect(diags.length).toBe(2);
|
||||
expect(diags[0].messageText).toEqual(`Type '""' is not assignable to type 'boolean'.`);
|
||||
expect(diags[1].messageText).toEqual(`Type '"3"' is not assignable to type 'number'.`);
|
||||
expect(diags[0].messageText).toEqual(`Type 'string' is not assignable to type 'boolean'.`);
|
||||
expect(diags[1].messageText).toEqual(`Type 'string' is not assignable to type 'number'.`);
|
||||
});
|
||||
|
||||
it('should produce an error for text attributes when overall strictness is enabled', () => {
|
||||
@ -725,8 +725,8 @@ export declare class AnimationEvent {
|
||||
|
||||
const diags = env.driveDiagnostics();
|
||||
expect(diags.length).toBe(2);
|
||||
expect(diags[0].messageText).toEqual(`Type '""' is not assignable to type 'boolean'.`);
|
||||
expect(diags[1].messageText).toEqual(`Type '"3"' is not assignable to type 'number'.`);
|
||||
expect(diags[0].messageText).toEqual(`Type 'string' is not assignable to type 'boolean'.`);
|
||||
expect(diags[1].messageText).toEqual(`Type 'string' is not assignable to type 'number'.`);
|
||||
});
|
||||
|
||||
it('should not produce an error for text attributes when not enabled', () => {
|
||||
@ -1119,7 +1119,7 @@ export declare class AnimationEvent {
|
||||
const allErrors = [
|
||||
`'does_not_exist' does not exist on type '{ name: string; }'`,
|
||||
`Expected 2 arguments, but got 3.`,
|
||||
`Argument of type '"test"' is not assignable to parameter of type 'number'`,
|
||||
`Argument of type 'string' is not assignable to parameter of type 'number'`,
|
||||
`Argument of type '{ name: string; }' is not assignable to parameter of type 'unknown[]'`,
|
||||
];
|
||||
|
||||
@ -1241,11 +1241,11 @@ export declare class AnimationEvent {
|
||||
|
||||
const diags = env.driveDiagnostics();
|
||||
expect(diags.length).toBe(3);
|
||||
expect(diags[0].messageText).toBe(`Type 'true' is not assignable to type 'number'.`);
|
||||
expect(diags[0].messageText).toBe(`Type 'boolean' is not assignable to type 'number'.`);
|
||||
expect(getSourceCodeForDiagnostic(diags[0])).toEqual('[fromAbstract]="true"');
|
||||
expect(diags[1].messageText).toBe(`Type '3' is not assignable to type 'string'.`);
|
||||
expect(diags[1].messageText).toBe(`Type 'number' is not assignable to type 'string'.`);
|
||||
expect(getSourceCodeForDiagnostic(diags[1])).toEqual('[fromBase]="3"');
|
||||
expect(diags[2].messageText).toBe(`Type '4' is not assignable to type 'boolean'.`);
|
||||
expect(diags[2].messageText).toBe(`Type 'number' is not assignable to type 'boolean'.`);
|
||||
expect(getSourceCodeForDiagnostic(diags[2])).toEqual('[fromChild]="4"');
|
||||
});
|
||||
|
||||
@ -1298,11 +1298,11 @@ export declare class AnimationEvent {
|
||||
|
||||
const diags = env.driveDiagnostics();
|
||||
expect(diags.length).toBe(3);
|
||||
expect(diags[0].messageText).toBe(`Type 'true' is not assignable to type 'number'.`);
|
||||
expect(diags[0].messageText).toBe(`Type 'boolean' is not assignable to type 'number'.`);
|
||||
expect(getSourceCodeForDiagnostic(diags[0])).toEqual('[fromAbstract]="true"');
|
||||
expect(diags[1].messageText).toBe(`Type '3' is not assignable to type 'string'.`);
|
||||
expect(diags[1].messageText).toBe(`Type 'number' is not assignable to type 'string'.`);
|
||||
expect(getSourceCodeForDiagnostic(diags[1])).toEqual('[fromBase]="3"');
|
||||
expect(diags[2].messageText).toBe(`Type '4' is not assignable to type 'boolean'.`);
|
||||
expect(diags[2].messageText).toBe(`Type 'number' is not assignable to type 'boolean'.`);
|
||||
expect(getSourceCodeForDiagnostic(diags[2])).toEqual('[fromChild]="4"');
|
||||
});
|
||||
|
||||
|
@ -192,7 +192,7 @@ describe('downlevel decorator transform', () => {
|
||||
it('should downlevel Angular-decorated class member', () => {
|
||||
const {output} = transform(`
|
||||
import {Input} from '@angular/core';
|
||||
|
||||
|
||||
export class MyDir {
|
||||
@Input() disabled: boolean = false;
|
||||
}
|
||||
@ -231,7 +231,7 @@ describe('downlevel decorator transform', () => {
|
||||
const {output} = transform(`
|
||||
import {Input} from '@angular/core';
|
||||
import {MyOtherClass} from './other-file';
|
||||
|
||||
|
||||
export class MyDir {
|
||||
@Input() trigger: HTMLElement;
|
||||
@Input() fromOtherFile: MyOtherClass;
|
||||
@ -255,7 +255,7 @@ describe('downlevel decorator transform', () => {
|
||||
`
|
||||
import {Directive} from '@angular/core';
|
||||
import {MyOtherClass} from './other-file';
|
||||
|
||||
|
||||
@Directive()
|
||||
export class MyDir {
|
||||
constructor(other: MyOtherClass) {}
|
||||
@ -281,7 +281,7 @@ describe('downlevel decorator transform', () => {
|
||||
`
|
||||
import {Directive} from '@angular/core';
|
||||
import {MyOtherClass} from './other-file';
|
||||
|
||||
|
||||
@Directive()
|
||||
export class MyDir {
|
||||
constructor(other: MyOtherClass) {}
|
||||
@ -307,7 +307,7 @@ describe('downlevel decorator transform', () => {
|
||||
const {output} = transform(`
|
||||
import {Directive} from '@angular/core';
|
||||
import * as externalFile from './other-file';
|
||||
|
||||
|
||||
@Directive()
|
||||
export class MyDir {
|
||||
constructor(other: externalFile.MyOtherClass) {}
|
||||
@ -329,11 +329,11 @@ describe('downlevel decorator transform', () => {
|
||||
it('should properly serialize constructor parameter with local qualified name type', () => {
|
||||
const {output} = transform(`
|
||||
import {Directive} from '@angular/core';
|
||||
|
||||
|
||||
namespace other {
|
||||
export class OtherClass {}
|
||||
};
|
||||
|
||||
|
||||
@Directive()
|
||||
export class MyDir {
|
||||
constructor(other: other.OtherClass) {}
|
||||
@ -355,7 +355,7 @@ describe('downlevel decorator transform', () => {
|
||||
it('should properly downlevel constructor parameter decorators', () => {
|
||||
const {output} = transform(`
|
||||
import {Inject, Directive, DOCUMENT} from '@angular/core';
|
||||
|
||||
|
||||
@Directive()
|
||||
export class MyDir {
|
||||
constructor(@Inject(DOCUMENT) document: Document) {}
|
||||
@ -376,7 +376,7 @@ describe('downlevel decorator transform', () => {
|
||||
it('should properly downlevel constructor parameters with union type', () => {
|
||||
const {output} = transform(`
|
||||
import {Optional, Directive, NgZone} from '@angular/core';
|
||||
|
||||
|
||||
@Directive()
|
||||
export class MyDir {
|
||||
constructor(@Optional() ngZone: NgZone|null) {}
|
||||
@ -546,18 +546,20 @@ describe('downlevel decorator transform', () => {
|
||||
export default interface {
|
||||
hello: false;
|
||||
}
|
||||
export const enum KeyCodes {A, B}
|
||||
`);
|
||||
const {output} = transform(`
|
||||
import {Directive, Inject} from '@angular/core';
|
||||
import * as angular from './external';
|
||||
import {IOverlay} from './external';
|
||||
import {IOverlay, KeyCodes} from './external';
|
||||
import TypeFromDefaultImport from './external';
|
||||
|
||||
@Directive()
|
||||
export class MyDir {
|
||||
constructor(@Inject('$state') param: angular.IState,
|
||||
@Inject('$overlay') other: IOverlay,
|
||||
@Inject('$default') default: TypeFromDefaultImport) {}
|
||||
@Inject('$default') default: TypeFromDefaultImport,
|
||||
@Inject('$keyCodes') keyCodes: KeyCodes) {}
|
||||
}
|
||||
`);
|
||||
|
||||
@ -570,7 +572,8 @@ describe('downlevel decorator transform', () => {
|
||||
MyDir.ctorParameters = () => [
|
||||
{ type: undefined, decorators: [{ type: core_1.Inject, args: ['$state',] }] },
|
||||
{ type: undefined, decorators: [{ type: core_1.Inject, args: ['$overlay',] }] },
|
||||
{ type: undefined, decorators: [{ type: core_1.Inject, args: ['$default',] }] }
|
||||
{ type: undefined, decorators: [{ type: core_1.Inject, args: ['$default',] }] },
|
||||
{ type: undefined, decorators: [{ type: core_1.Inject, args: ['$keyCodes',] }] }
|
||||
];
|
||||
`);
|
||||
});
|
||||
@ -581,7 +584,7 @@ describe('downlevel decorator transform', () => {
|
||||
const visitNode = (node: ts.Node): ts.Node => {
|
||||
if (ts.isClassDeclaration(node) || ts.isClassElement(node)) {
|
||||
const cloned = ts.getMutableClone(node);
|
||||
cloned.decorators = undefined;
|
||||
(cloned.decorators as undefined) = undefined;
|
||||
return cloned;
|
||||
}
|
||||
return ts.visitEachChild(node, visitNode, context);
|
||||
@ -593,7 +596,7 @@ describe('downlevel decorator transform', () => {
|
||||
const {output} = transform(
|
||||
`
|
||||
import {Directive} from '@angular/core';
|
||||
|
||||
|
||||
export class MyInjectedClass {}
|
||||
|
||||
@Directive()
|
||||
@ -609,13 +612,36 @@ describe('downlevel decorator transform', () => {
|
||||
expect(output).not.toContain('tslib');
|
||||
});
|
||||
|
||||
it('should capture a non-const enum used as a constructor type', () => {
|
||||
const {output} = transform(`
|
||||
import {Component} from '@angular/core';
|
||||
|
||||
export enum Values {A, B};
|
||||
|
||||
@Component({template: 'hello'})
|
||||
export class MyComp {
|
||||
constructor(v: Values) {}
|
||||
}
|
||||
`);
|
||||
|
||||
expect(diagnostics.length).toBe(0);
|
||||
expect(output).toContain(dedent`
|
||||
MyComp.decorators = [
|
||||
{ type: core_1.Component, args: [{ template: 'hello' },] }
|
||||
];
|
||||
MyComp.ctorParameters = () => [
|
||||
{ type: Values }
|
||||
];`);
|
||||
expect(output).not.toContain('tslib');
|
||||
});
|
||||
|
||||
describe('class decorators skipped', () => {
|
||||
beforeEach(() => skipClassDecorators = true);
|
||||
|
||||
it('should not downlevel Angular class decorators', () => {
|
||||
const {output} = transform(`
|
||||
import {Injectable} from '@angular/core';
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class MyService {}
|
||||
`);
|
||||
@ -632,10 +658,10 @@ describe('downlevel decorator transform', () => {
|
||||
it('should downlevel constructor parameters', () => {
|
||||
const {output} = transform(`
|
||||
import {Injectable} from '@angular/core';
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class InjectClass {}
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class MyService {
|
||||
constructor(dep: InjectClass) {}
|
||||
@ -658,10 +684,10 @@ describe('downlevel decorator transform', () => {
|
||||
it('should downlevel constructor parameter decorators', () => {
|
||||
const {output} = transform(`
|
||||
import {Injectable, Inject} from '@angular/core';
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class InjectClass {}
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class MyService {
|
||||
constructor(@Inject('test') dep: InjectClass) {}
|
||||
@ -684,7 +710,7 @@ describe('downlevel decorator transform', () => {
|
||||
it('should downlevel class member Angular decorators', () => {
|
||||
const {output} = transform(`
|
||||
import {Injectable, Input} from '@angular/core';
|
||||
|
||||
|
||||
export class MyService {
|
||||
@Input() disabled: boolean;
|
||||
}
|
||||
|
@ -492,11 +492,11 @@ describe('ng program', () => {
|
||||
.toBe(true);
|
||||
switch (checks.shouldBe) {
|
||||
case ShouldBe.Empty:
|
||||
expect(writeData!.data).toMatch(/^(\s*\/\*([^*]|\*[^/])*\*\/\s*)?$/);
|
||||
expect(writeData!.data).toMatch(/^(\s*\/\*([^*]|\*[^\/])*\*\/\s*)?$/);
|
||||
break;
|
||||
case ShouldBe.EmptyExport:
|
||||
expect(writeData!.data)
|
||||
.toMatch(/^((\s*\/\*([^*]|\*[^/])*\*\/\s*)|(\s*export\s*{\s*}\s*;\s*)|())$/);
|
||||
.toMatch(/^((\s*\/\*([^*]|\*[^\/])*\*\/\s*)|(\s*export\s*{\s*};\s*))$/m);
|
||||
break;
|
||||
case ShouldBe.NoneEmpty:
|
||||
expect(writeData!.data).not.toBe('');
|
||||
@ -505,12 +505,14 @@ describe('ng program', () => {
|
||||
}
|
||||
|
||||
assertGenFile(
|
||||
'built/src/util.ngfactory.js', {originalFileName: 'src/util.ts', shouldBe: ShouldBe.Empty});
|
||||
'built/src/util.ngfactory.js',
|
||||
{originalFileName: 'src/util.ts', shouldBe: ShouldBe.EmptyExport});
|
||||
assertGenFile(
|
||||
'built/src/util.ngfactory.d.ts',
|
||||
{originalFileName: 'src/util.ts', shouldBe: ShouldBe.EmptyExport});
|
||||
assertGenFile(
|
||||
'built/src/util.ngsummary.js', {originalFileName: 'src/util.ts', shouldBe: ShouldBe.Empty});
|
||||
'built/src/util.ngsummary.js',
|
||||
{originalFileName: 'src/util.ts', shouldBe: ShouldBe.EmptyExport});
|
||||
assertGenFile(
|
||||
'built/src/util.ngsummary.d.ts',
|
||||
{originalFileName: 'src/util.ts', shouldBe: ShouldBe.EmptyExport});
|
||||
@ -987,7 +989,8 @@ describe('ng program', () => {
|
||||
const errorDiags =
|
||||
program1.emit().diagnostics.filter(d => d.category === ts.DiagnosticCategory.Error);
|
||||
expect(stripAnsi(formatDiagnostics(errorDiags)))
|
||||
.toContain(`src/main.ts:5:13 - error TS2322: Type '1' is not assignable to type 'string'.`);
|
||||
.toContain(
|
||||
`src/main.ts:5:13 - error TS2322: Type 'number' is not assignable to type 'string'.`);
|
||||
expect(stripAnsi(formatDiagnostics(errorDiags)))
|
||||
.toContain(
|
||||
`src/main.html:1:1 - error TS100: Property 'nonExistent' does not exist on type 'MyComp'.`);
|
||||
|
@ -330,6 +330,26 @@ class _AstToIrVisitor implements cdAst.AstVisitor {
|
||||
private bindingId: string, private interpolationFunction: InterpolationFunction|undefined,
|
||||
private baseSourceSpan?: ParseSourceSpan, private implicitReceiverAccesses?: Set<string>) {}
|
||||
|
||||
visitUnary(ast: cdAst.Unary, mode: _Mode): any {
|
||||
let op: o.UnaryOperator;
|
||||
switch (ast.operator) {
|
||||
case '+':
|
||||
op = o.UnaryOperator.Plus;
|
||||
break;
|
||||
case '-':
|
||||
op = o.UnaryOperator.Minus;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported operator ${ast.operator}`);
|
||||
}
|
||||
|
||||
return convertToStatementIfNeeded(
|
||||
mode,
|
||||
new o.UnaryOperatorExpr(
|
||||
op, this._visit(ast.expr, _Mode.Expression), undefined,
|
||||
this.convertSourceSpan(ast.span)));
|
||||
}
|
||||
|
||||
visitBinary(ast: cdAst.Binary, mode: _Mode): any {
|
||||
let op: o.BinaryOperator;
|
||||
switch (ast.operation) {
|
||||
@ -710,6 +730,9 @@ class _AstToIrVisitor implements cdAst.AstVisitor {
|
||||
return (this._nodeMap.get(ast) || ast).visit(visitor);
|
||||
};
|
||||
return ast.visit({
|
||||
visitUnary(ast: cdAst.Unary) {
|
||||
return null;
|
||||
},
|
||||
visitBinary(ast: cdAst.Binary) {
|
||||
return null;
|
||||
},
|
||||
@ -784,6 +807,9 @@ class _AstToIrVisitor implements cdAst.AstVisitor {
|
||||
return ast.some(ast => visit(visitor, ast));
|
||||
};
|
||||
return ast.visit({
|
||||
visitUnary(ast: cdAst.Unary): boolean {
|
||||
return visit(this, ast.expr);
|
||||
},
|
||||
visitBinary(ast: cdAst.Binary): boolean {
|
||||
return visit(this, ast.left) || visit(this, ast.right);
|
||||
},
|
||||
|
@ -330,6 +330,7 @@ class KeyVisitor implements o.ExpressionVisitor {
|
||||
visitAssertNotNullExpr = invalid;
|
||||
visitCastExpr = invalid;
|
||||
visitFunctionExpr = invalid;
|
||||
visitUnaryOperatorExpr = invalid;
|
||||
visitBinaryOperatorExpr = invalid;
|
||||
visitReadPropExpr = invalid;
|
||||
visitReadKeyExpr = invalid;
|
||||
|
@ -227,6 +227,52 @@ export class Binary extends AST {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For backwards compatibility reasons, `Unary` inherits from `Binary` and mimics the binary AST
|
||||
* node that was originally used. This inheritance relation can be deleted in some future major,
|
||||
* after consumers have been given a chance to fully support Unary.
|
||||
*/
|
||||
export class Unary extends Binary {
|
||||
// Redeclare the properties that are inherited from `Binary` as `never`, as consumers should not
|
||||
// depend on these fields when operating on `Unary`.
|
||||
left: never;
|
||||
right: never;
|
||||
operation: never;
|
||||
|
||||
/**
|
||||
* Creates a unary minus expression "-x", represented as `Binary` using "0 - x".
|
||||
*/
|
||||
static createMinus(span: ParseSpan, sourceSpan: AbsoluteSourceSpan, expr: AST): Unary {
|
||||
return new Unary(
|
||||
span, sourceSpan, '-', expr, '-', new LiteralPrimitive(span, sourceSpan, 0), expr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a unary plus expression "+x", represented as `Binary` using "x - 0".
|
||||
*/
|
||||
static createPlus(span: ParseSpan, sourceSpan: AbsoluteSourceSpan, expr: AST): Unary {
|
||||
return new Unary(
|
||||
span, sourceSpan, '+', expr, '-', expr, new LiteralPrimitive(span, sourceSpan, 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* During the deprecation period this constructor is private, to avoid consumers from creating
|
||||
* a `Unary` with the fallback properties for `Binary`.
|
||||
*/
|
||||
private constructor(
|
||||
span: ParseSpan, sourceSpan: AbsoluteSourceSpan, public operator: string, public expr: AST,
|
||||
binaryOp: string, binaryLeft: AST, binaryRight: AST) {
|
||||
super(span, sourceSpan, binaryOp, binaryLeft, binaryRight);
|
||||
}
|
||||
|
||||
visit(visitor: AstVisitor, context: any = null): any {
|
||||
if (visitor.visitUnary !== undefined) {
|
||||
return visitor.visitUnary(this, context);
|
||||
}
|
||||
return visitor.visitBinary(this, context);
|
||||
}
|
||||
}
|
||||
|
||||
export class PrefixNot extends AST {
|
||||
constructor(span: ParseSpan, sourceSpan: AbsoluteSourceSpan, public expression: AST) {
|
||||
super(span, sourceSpan);
|
||||
@ -361,6 +407,11 @@ export interface TemplateBindingIdentifier {
|
||||
}
|
||||
|
||||
export interface AstVisitor {
|
||||
/**
|
||||
* The `visitUnary` method is declared as optional for backwards compatibility. In an upcoming
|
||||
* major release, this method will be made required.
|
||||
*/
|
||||
visitUnary?(ast: Unary, context: any): any;
|
||||
visitBinary(ast: Binary, context: any): any;
|
||||
visitChain(ast: Chain, context: any): any;
|
||||
visitConditional(ast: Conditional, context: any): any;
|
||||
@ -398,6 +449,9 @@ export class RecursiveAstVisitor implements AstVisitor {
|
||||
// to selectively visit the specified node.
|
||||
ast.visit(this, context);
|
||||
}
|
||||
visitUnary(ast: Unary, context: any): any {
|
||||
this.visit(ast.expr, context);
|
||||
}
|
||||
visitBinary(ast: Binary, context: any): any {
|
||||
this.visit(ast.left, context);
|
||||
this.visit(ast.right, context);
|
||||
@ -527,6 +581,17 @@ export class AstTransformer implements AstVisitor {
|
||||
return new LiteralMap(ast.span, ast.sourceSpan, ast.keys, this.visitAll(ast.values));
|
||||
}
|
||||
|
||||
visitUnary(ast: Unary, context: any): AST {
|
||||
switch (ast.operator) {
|
||||
case '+':
|
||||
return Unary.createPlus(ast.span, ast.sourceSpan, ast.expr.visit(this));
|
||||
case '-':
|
||||
return Unary.createMinus(ast.span, ast.sourceSpan, ast.expr.visit(this));
|
||||
default:
|
||||
throw new Error(`Unknown unary operator ${ast.operator}`);
|
||||
}
|
||||
}
|
||||
|
||||
visitBinary(ast: Binary, context: any): AST {
|
||||
return new Binary(
|
||||
ast.span, ast.sourceSpan, ast.operation, ast.left.visit(this), ast.right.visit(this));
|
||||
@ -665,6 +730,21 @@ export class AstMemoryEfficientTransformer implements AstVisitor {
|
||||
return ast;
|
||||
}
|
||||
|
||||
visitUnary(ast: Unary, context: any): AST {
|
||||
const expr = ast.expr.visit(this);
|
||||
if (expr !== ast.expr) {
|
||||
switch (ast.operator) {
|
||||
case '+':
|
||||
return Unary.createPlus(ast.span, ast.sourceSpan, expr);
|
||||
case '-':
|
||||
return Unary.createMinus(ast.span, ast.sourceSpan, expr);
|
||||
default:
|
||||
throw new Error(`Unknown unary operator ${ast.operator}`);
|
||||
}
|
||||
}
|
||||
return ast;
|
||||
}
|
||||
|
||||
visitBinary(ast: Binary, context: any): AST {
|
||||
const left = ast.left.visit(this);
|
||||
const right = ast.right.visit(this);
|
||||
|
@ -10,7 +10,7 @@ import * as chars from '../chars';
|
||||
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../ml_parser/interpolation_config';
|
||||
import {escapeRegExp} from '../util';
|
||||
|
||||
import {AbsoluteSourceSpan, AST, AstVisitor, ASTWithSource, Binary, BindingPipe, Chain, Conditional, EmptyExpr, ExpressionBinding, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralMapKey, LiteralPrimitive, MethodCall, NonNullAssert, ParserError, ParseSpan, PrefixNot, PropertyRead, PropertyWrite, Quote, RecursiveAstVisitor, SafeMethodCall, SafePropertyRead, TemplateBinding, TemplateBindingIdentifier, VariableBinding} from './ast';
|
||||
import {AbsoluteSourceSpan, AST, AstVisitor, ASTWithSource, Binary, BindingPipe, Chain, Conditional, EmptyExpr, ExpressionBinding, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralMapKey, LiteralPrimitive, MethodCall, NonNullAssert, ParserError, ParseSpan, PrefixNot, PropertyRead, PropertyWrite, Quote, RecursiveAstVisitor, SafeMethodCall, SafePropertyRead, TemplateBinding, TemplateBindingIdentifier, Unary, VariableBinding} from './ast';
|
||||
import {EOF, isIdentifier, isQuote, Lexer, Token, TokenType} from './lexer';
|
||||
|
||||
export class SplitInterpolation {
|
||||
@ -591,22 +591,16 @@ export class _ParseAST {
|
||||
if (this.next.type == TokenType.Operator) {
|
||||
const start = this.inputIndex;
|
||||
const operator = this.next.strValue;
|
||||
const literalSpan = new ParseSpan(start, start);
|
||||
const literalSourceSpan = literalSpan.toAbsolute(this.absoluteOffset);
|
||||
let result: AST;
|
||||
switch (operator) {
|
||||
case '+':
|
||||
this.advance();
|
||||
result = this.parsePrefix();
|
||||
return new Binary(
|
||||
this.span(start), this.sourceSpan(start), '-', result,
|
||||
new LiteralPrimitive(literalSpan, literalSourceSpan, 0));
|
||||
return Unary.createPlus(this.span(start), this.sourceSpan(start), result);
|
||||
case '-':
|
||||
this.advance();
|
||||
result = this.parsePrefix();
|
||||
return new Binary(
|
||||
this.span(start), this.sourceSpan(start), operator,
|
||||
new LiteralPrimitive(literalSpan, literalSourceSpan, 0), result);
|
||||
return Unary.createMinus(this.span(start), this.sourceSpan(start), result);
|
||||
case '!':
|
||||
this.advance();
|
||||
result = this.parsePrefix();
|
||||
@ -1059,6 +1053,8 @@ class SimpleExpressionChecker implements AstVisitor {
|
||||
this.visitAll(ast.values, context);
|
||||
}
|
||||
|
||||
visitUnary(ast: Unary, context: any) {}
|
||||
|
||||
visitBinary(ast: Binary, context: any) {}
|
||||
|
||||
visitPrefixNot(ast: PrefixNot, context: any) {}
|
||||
|
@ -420,6 +420,25 @@ export abstract class AbstractEmitterVisitor implements o.StatementVisitor, o.Ex
|
||||
abstract visitFunctionExpr(ast: o.FunctionExpr, ctx: EmitterVisitorContext): any;
|
||||
abstract visitDeclareFunctionStmt(stmt: o.DeclareFunctionStmt, context: any): any;
|
||||
|
||||
visitUnaryOperatorExpr(ast: o.UnaryOperatorExpr, ctx: EmitterVisitorContext): any {
|
||||
let opStr: string;
|
||||
switch (ast.operator) {
|
||||
case o.UnaryOperator.Plus:
|
||||
opStr = '+';
|
||||
break;
|
||||
case o.UnaryOperator.Minus:
|
||||
opStr = '-';
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown operator ${ast.operator}`);
|
||||
}
|
||||
if (ast.parens) ctx.print(ast, `(`);
|
||||
ctx.print(ast, opStr);
|
||||
ast.expr.visitExpression(this, ctx);
|
||||
if (ast.parens) ctx.print(ast, `)`);
|
||||
return null;
|
||||
}
|
||||
|
||||
visitBinaryOperatorExpr(ast: o.BinaryOperatorExpr, ctx: EmitterVisitorContext): any {
|
||||
let opStr: string;
|
||||
switch (ast.operator) {
|
||||
|
@ -100,6 +100,11 @@ export interface TypeVisitor {
|
||||
|
||||
///// Expressions
|
||||
|
||||
export enum UnaryOperator {
|
||||
Minus,
|
||||
Plus,
|
||||
}
|
||||
|
||||
export enum BinaryOperator {
|
||||
Equals,
|
||||
NotEquals,
|
||||
@ -753,6 +758,28 @@ export class FunctionExpr extends Expression {
|
||||
}
|
||||
|
||||
|
||||
export class UnaryOperatorExpr extends Expression {
|
||||
constructor(
|
||||
public operator: UnaryOperator, public expr: Expression, type?: Type|null,
|
||||
sourceSpan?: ParseSourceSpan|null, public parens: boolean = true) {
|
||||
super(type || NUMBER_TYPE, sourceSpan);
|
||||
}
|
||||
|
||||
isEquivalent(e: Expression): boolean {
|
||||
return e instanceof UnaryOperatorExpr && this.operator === e.operator &&
|
||||
this.expr.isEquivalent(e.expr);
|
||||
}
|
||||
|
||||
isConstant() {
|
||||
return false;
|
||||
}
|
||||
|
||||
visitExpression(visitor: ExpressionVisitor, context: any): any {
|
||||
return visitor.visitUnaryOperatorExpr(this, context);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class BinaryOperatorExpr extends Expression {
|
||||
public lhs: Expression;
|
||||
constructor(
|
||||
@ -912,6 +939,7 @@ export interface ExpressionVisitor {
|
||||
visitAssertNotNullExpr(ast: AssertNotNull, context: any): any;
|
||||
visitCastExpr(ast: CastExpr, context: any): any;
|
||||
visitFunctionExpr(ast: FunctionExpr, context: any): any;
|
||||
visitUnaryOperatorExpr(ast: UnaryOperatorExpr, context: any): any;
|
||||
visitBinaryOperatorExpr(ast: BinaryOperatorExpr, context: any): any;
|
||||
visitReadPropExpr(ast: ReadPropExpr, context: any): any;
|
||||
visitReadKeyExpr(ast: ReadKeyExpr, context: any): any;
|
||||
@ -1292,6 +1320,13 @@ export class AstTransformer implements StatementVisitor, ExpressionVisitor {
|
||||
context);
|
||||
}
|
||||
|
||||
visitUnaryOperatorExpr(ast: UnaryOperatorExpr, context: any): any {
|
||||
return this.transformExpr(
|
||||
new UnaryOperatorExpr(
|
||||
ast.operator, ast.expr.visitExpression(this, context), ast.type, ast.sourceSpan),
|
||||
context);
|
||||
}
|
||||
|
||||
visitBinaryOperatorExpr(ast: BinaryOperatorExpr, context: any): any {
|
||||
return this.transformExpr(
|
||||
new BinaryOperatorExpr(
|
||||
@ -1517,6 +1552,10 @@ export class RecursiveAstVisitor implements StatementVisitor, ExpressionVisitor
|
||||
this.visitAllStatements(ast.statements, context);
|
||||
return this.visitExpression(ast, context);
|
||||
}
|
||||
visitUnaryOperatorExpr(ast: UnaryOperatorExpr, context: any): any {
|
||||
ast.expr.visitExpression(this, context);
|
||||
return this.visitExpression(ast, context);
|
||||
}
|
||||
visitBinaryOperatorExpr(ast: BinaryOperatorExpr, context: any): any {
|
||||
ast.lhs.visitExpression(this, context);
|
||||
ast.rhs.visitExpression(this, context);
|
||||
@ -1730,6 +1769,12 @@ export function literalMap(
|
||||
values.map(e => new LiteralMapEntry(e.key, e.value, e.quoted)), type, null);
|
||||
}
|
||||
|
||||
export function unary(
|
||||
operator: UnaryOperator, expr: Expression, type?: Type,
|
||||
sourceSpan?: ParseSourceSpan|null): UnaryOperatorExpr {
|
||||
return new UnaryOperatorExpr(operator, expr, type, sourceSpan);
|
||||
}
|
||||
|
||||
export function not(expr: Expression, sourceSpan?: ParseSourceSpan|null): NotExpr {
|
||||
return new NotExpr(expr, sourceSpan);
|
||||
}
|
||||
|
@ -282,6 +282,18 @@ class StatementInterpreter implements o.StatementVisitor, o.ExpressionVisitor {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
visitUnaryOperatorExpr(ast: o.UnaryOperatorExpr, ctx: _ExecutionContext): any {
|
||||
const rhs = () => ast.expr.visitExpression(this, ctx);
|
||||
|
||||
switch (ast.operator) {
|
||||
case o.UnaryOperator.Plus:
|
||||
return +rhs();
|
||||
case o.UnaryOperator.Minus:
|
||||
return -rhs();
|
||||
default:
|
||||
throw new Error(`Unknown operator ${ast.operator}`);
|
||||
}
|
||||
}
|
||||
visitBinaryOperatorExpr(ast: o.BinaryOperatorExpr, ctx: _ExecutionContext): any {
|
||||
const lhs = () => ast.lhs.visitExpression(this, ctx);
|
||||
const rhs = () => ast.rhs.visitExpression(this, ctx);
|
||||
|
@ -34,11 +34,11 @@ describe('parser', () => {
|
||||
checkAction('undefined');
|
||||
});
|
||||
|
||||
it('should parse unary - expressions', () => {
|
||||
checkAction('-1', '0 - 1');
|
||||
checkAction('+1', '1 - 0');
|
||||
checkAction(`-'1'`, `0 - "1"`);
|
||||
checkAction(`+'1'`, `"1" - 0`);
|
||||
it('should parse unary - and + expressions', () => {
|
||||
checkAction('-1', '-1');
|
||||
checkAction('+1', '+1');
|
||||
checkAction(`-'1'`, `-"1"`);
|
||||
checkAction(`+'1'`, `+"1"`);
|
||||
});
|
||||
|
||||
it('should parse unary ! expressions', () => {
|
||||
|
@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {AbsoluteSourceSpan, AST, AstVisitor, ASTWithSource, Binary, BindingPipe, Chain, Conditional, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, NonNullAssert, ParseSpan, PrefixNot, PropertyRead, PropertyWrite, Quote, RecursiveAstVisitor, SafeMethodCall, SafePropertyRead} from '../../../src/expression_parser/ast';
|
||||
import {AST, AstVisitor, ASTWithSource, Binary, BindingPipe, Chain, Conditional, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, NonNullAssert, ParseSpan, PrefixNot, PropertyRead, PropertyWrite, Quote, RecursiveAstVisitor, SafeMethodCall, SafePropertyRead, Unary} from '../../../src/expression_parser/ast';
|
||||
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../../../src/ml_parser/interpolation_config';
|
||||
|
||||
class Unparser implements AstVisitor {
|
||||
@ -35,6 +35,11 @@ class Unparser implements AstVisitor {
|
||||
this._visit(ast.value);
|
||||
}
|
||||
|
||||
visitUnary(ast: Unary, context: any) {
|
||||
this._expression += ast.operator;
|
||||
this._visit(ast.expr);
|
||||
}
|
||||
|
||||
visitBinary(ast: Binary, context: any) {
|
||||
this._visit(ast.left);
|
||||
this._expression += ` ${ast.operation} `;
|
||||
|
@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {AST, Binary, BindingPipe, Chain, Conditional, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, ParseSpan, PrefixNot, PropertyRead, PropertyWrite, Quote, RecursiveAstVisitor, SafeMethodCall, SafePropertyRead} from '../../../src/expression_parser/ast';
|
||||
import {AST, Binary, BindingPipe, Chain, Conditional, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, ParseSpan, PrefixNot, PropertyRead, PropertyWrite, Quote, RecursiveAstVisitor, SafeMethodCall, SafePropertyRead, Unary} from '../../../src/expression_parser/ast';
|
||||
|
||||
import {unparse} from './unparser';
|
||||
|
||||
@ -34,6 +34,10 @@ class ASTValidator extends RecursiveAstVisitor {
|
||||
this.parentSpan = oldParent;
|
||||
}
|
||||
|
||||
visitUnary(ast: Unary, context: any): any {
|
||||
this.validate(ast, () => super.visitUnary(ast, context));
|
||||
}
|
||||
|
||||
visitBinary(ast: Binary, context: any): any {
|
||||
this.validate(ast, () => super.visitBinary(ast, context));
|
||||
}
|
||||
|
@ -138,6 +138,8 @@ const externalModuleIdentifier = new o.ExternalReference(anotherModuleUrl, 'some
|
||||
const lhs = o.variable('lhs');
|
||||
const rhs = o.variable('rhs');
|
||||
expect(emitStmt(o.not(someVar).toStmt())).toEqual('!someVar;');
|
||||
expect(emitStmt(o.unary(o.UnaryOperator.Minus, someVar).toStmt())).toEqual('(-someVar);');
|
||||
expect(emitStmt(o.unary(o.UnaryOperator.Plus, someVar).toStmt())).toEqual('(+someVar);');
|
||||
expect(emitStmt(o.assertNotNull(someVar).toStmt())).toEqual('someVar;');
|
||||
expect(
|
||||
emitStmt(someVar.conditional(o.variable('trueCase'), o.variable('falseCase')).toStmt()))
|
||||
|
@ -191,6 +191,8 @@ const externalModuleIdentifier = new o.ExternalReference(anotherModuleUrl, 'some
|
||||
const rhs = o.variable('rhs');
|
||||
expect(emitStmt(someVar.cast(o.INT_TYPE).toStmt())).toEqual('(<number>someVar);');
|
||||
expect(emitStmt(o.not(someVar).toStmt())).toEqual('!someVar;');
|
||||
expect(emitStmt(o.unary(o.UnaryOperator.Minus, someVar).toStmt())).toEqual('(-someVar);');
|
||||
expect(emitStmt(o.unary(o.UnaryOperator.Plus, someVar).toStmt())).toEqual('(+someVar);');
|
||||
expect(emitStmt(o.assertNotNull(someVar).toStmt())).toEqual('someVar!;');
|
||||
expect(
|
||||
emitStmt(someVar.conditional(o.variable('trueCase'), o.variable('falseCase')).toStmt()))
|
||||
|
@ -1656,7 +1656,7 @@ Reference "#a" is defined several times ("<div #a></div><div [ERROR ->]#a></div>
|
||||
expect(humanizeTplAst(parse('<div *ngIf="-1">', [ngIf]))).toEqual([
|
||||
[EmbeddedTemplateAst],
|
||||
[DirectiveAst, ngIf],
|
||||
[BoundDirectivePropertyAst, 'ngIf', '0 - 1'],
|
||||
[BoundDirectivePropertyAst, 'ngIf', '-1'],
|
||||
[ElementAst, 'div'],
|
||||
]);
|
||||
});
|
||||
|
@ -30,6 +30,10 @@ class ExpressionSourceHumanizer extends e.RecursiveAstVisitor implements t.Templ
|
||||
this.recordAst(ast);
|
||||
this.visitAll([ast.ast], null);
|
||||
}
|
||||
visitUnary(ast: e.Unary) {
|
||||
this.recordAst(ast);
|
||||
super.visitUnary(ast, null);
|
||||
}
|
||||
visitBinary(ast: e.Binary) {
|
||||
this.recordAst(ast);
|
||||
super.visitBinary(ast, null);
|
||||
|
@ -17,7 +17,7 @@ export function createHtmlSourceFile(filePath: string, content: string): ts.Sour
|
||||
|
||||
// Subtract two characters because the string literal quotes are only needed for parsing
|
||||
// and are not part of the actual source file.
|
||||
sourceFile.end = sourceFile.end - 2;
|
||||
(sourceFile.end as number) = sourceFile.end - 2;
|
||||
|
||||
// Note: This does not affect the way TSLint applies replacements for external resource files.
|
||||
// At the time of writing, TSLint loads files manually if the actual rule source file is not
|
||||
|
@ -26,7 +26,7 @@ export function isType(v: any): v is Type<any> {
|
||||
* @description
|
||||
*
|
||||
* Represents an abstract class `T`, if applied to a concrete class it would stop being
|
||||
* instantiatable.
|
||||
* instantiable.
|
||||
*
|
||||
* @publicApi
|
||||
*/
|
||||
|
@ -32,8 +32,9 @@ class DOMParserHelper implements InertBodyHelper {
|
||||
getInertBodyElement(html: string): HTMLElement|null {
|
||||
// We add these extra elements to ensure that the rest of the content is parsed as expected
|
||||
// e.g. leading whitespace is maintained and tags like `<meta>` do not get hoisted to the
|
||||
// `<head>` tag.
|
||||
html = '<body><remove></remove>' + html + '</body>';
|
||||
// `<head>` tag. Note that the `<body>` tag is closed implicitly to prevent unclosed tags
|
||||
// in `html` from consuming the otherwise explicit `</body>` tag.
|
||||
html = '<body><remove></remove>' + html;
|
||||
try {
|
||||
const body = new (window as any).DOMParser().parseFromString(html, 'text/html').body as
|
||||
HTMLBodyElement;
|
||||
|
@ -221,15 +221,6 @@
|
||||
{
|
||||
"name": "FormControlName"
|
||||
},
|
||||
{
|
||||
"name": "FormErrorExamples_formControlName"
|
||||
},
|
||||
{
|
||||
"name": "FormErrorExamples_formGroupName"
|
||||
},
|
||||
{
|
||||
"name": "FormErrorExamples_ngModelGroup"
|
||||
},
|
||||
{
|
||||
"name": "FormGroup"
|
||||
},
|
||||
@ -497,9 +488,6 @@
|
||||
{
|
||||
"name": "RangeValueAccessor"
|
||||
},
|
||||
{
|
||||
"name": "ReactiveErrors"
|
||||
},
|
||||
{
|
||||
"name": "ReactiveFormsComponent"
|
||||
},
|
||||
@ -608,9 +596,6 @@
|
||||
{
|
||||
"name": "TRANSITION_ID"
|
||||
},
|
||||
{
|
||||
"name": "TemplateDrivenErrors"
|
||||
},
|
||||
{
|
||||
"name": "TemplateFormsComponent"
|
||||
},
|
||||
@ -701,9 +686,6 @@
|
||||
{
|
||||
"name": "_keyMap"
|
||||
},
|
||||
{
|
||||
"name": "_noControlError"
|
||||
},
|
||||
{
|
||||
"name": "_randomChar"
|
||||
},
|
||||
@ -716,9 +698,6 @@
|
||||
{
|
||||
"name": "_testabilityGetter"
|
||||
},
|
||||
{
|
||||
"name": "_throwError"
|
||||
},
|
||||
{
|
||||
"name": "addComponentLogic"
|
||||
},
|
||||
@ -1628,9 +1607,6 @@
|
||||
{
|
||||
"name": "u"
|
||||
},
|
||||
{
|
||||
"name": "unimplemented"
|
||||
},
|
||||
{
|
||||
"name": "unwrapRNode"
|
||||
},
|
||||
|
@ -173,6 +173,27 @@ import {isDOMParserAvailable} from '../../src/sanitization/inert_body';
|
||||
expect(logMsgs.join('\n')).toMatch(/sanitizing HTML stripped some content/);
|
||||
});
|
||||
|
||||
it('should strip unclosed iframe tag', () => {
|
||||
expect(_sanitizeHtml(defaultDoc, '<iframe>')).toEqual('');
|
||||
expect([
|
||||
'<iframe>',
|
||||
// Double-escaped on IE
|
||||
'&lt;iframe&gt;'
|
||||
]).toContain(_sanitizeHtml(defaultDoc, '<iframe><iframe>'));
|
||||
expect([
|
||||
'<script>evil();</script>',
|
||||
// Double-escaped on IE
|
||||
'&lt;script&gt;evil();&lt;/script&gt;'
|
||||
]).toContain(_sanitizeHtml(defaultDoc, '<iframe><script>evil();</script>'));
|
||||
});
|
||||
|
||||
it('should ignore extraneous body tags', () => {
|
||||
expect(_sanitizeHtml(defaultDoc, '</body>')).toEqual('');
|
||||
expect(_sanitizeHtml(defaultDoc, 'foo</body>bar')).toEqual('foobar');
|
||||
expect(_sanitizeHtml(defaultDoc, 'foo<body>bar')).toEqual('foobar');
|
||||
expect(_sanitizeHtml(defaultDoc, 'fo<body>ob</body>ar')).toEqual('foobar');
|
||||
});
|
||||
|
||||
it('should not enter an infinite loop on clobbered elements', () => {
|
||||
// Some browsers are vulnerable to clobbered elements and will throw an expected exception
|
||||
// IE and EDGE does not seems to be affected by those cases
|
||||
|
@ -45,8 +45,7 @@ export abstract class NgElement extends HTMLElement {
|
||||
/**
|
||||
* The strategy that controls how a component is transformed in a custom element.
|
||||
*/
|
||||
// TODO(issue/24571): remove '!'.
|
||||
protected ngElementStrategy!: NgElementStrategy;
|
||||
protected abstract ngElementStrategy: NgElementStrategy;
|
||||
/**
|
||||
* A subscription to change, connect, and disconnect events in the custom element.
|
||||
*/
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user