Skip to content

Commit 098f25a

Browse files
devversionjelbourn
authored andcommitted
refactor(schematics): enable strict null checks (#12983)
* Enables strict null checks for the schematics and fixes a few potential unsafe accesses. * Supports specifying a project for `ng-add`.
1 parent e124418 commit 098f25a

31 files changed

+123
-78
lines changed

src/lib/schematics/address-form/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export default function(options: Schema): Rule {
3030
*/
3131
function addFormModulesToModule(options: Schema) {
3232
return (host: Tree) => {
33-
const modulePath = findModuleFromOptions(host, options);
33+
const modulePath = findModuleFromOptions(host, options)!;
3434
addModuleImportToModule(host, modulePath, 'MatInputModule', '@angular/material');
3535
addModuleImportToModule(host, modulePath, 'MatButtonModule', '@angular/material');
3636
addModuleImportToModule(host, modulePath, 'MatSelectModule', '@angular/material');

src/lib/schematics/dashboard/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export default function(options: Schema): Rule {
3030
*/
3131
function addNavModulesToModule(options: Schema) {
3232
return (host: Tree) => {
33-
const modulePath = findModuleFromOptions(host, options);
33+
const modulePath = findModuleFromOptions(host, options)!;
3434
addModuleImportToModule(host, modulePath, 'MatGridListModule', '@angular/material');
3535
addModuleImportToModule(host, modulePath, 'MatCardModule', '@angular/material');
3636
addModuleImportToModule(host, modulePath, 'MatMenuModule', '@angular/material');

src/lib/schematics/install/fonts/head-element.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ export function appendElementToHead(host: Tree, project: WorkspaceProject, eleme
3333
throw `Could not find '<head>' element in HTML file: ${indexPath}`;
3434
}
3535

36-
const endTagOffset = headTag.sourceCodeLocation.endTag.startOffset;
36+
// We always have access to the source code location here because the `getHeadTagElement`
37+
// function explicitly has the `sourceCodeLocationInfo` option enabled.
38+
const endTagOffset = headTag.sourceCodeLocation!.endTag.startOffset;
3739
const indentationOffset = getChildElementIndentation(headTag);
3840
const insertion = `${' '.repeat(indentationOffset)}${elementHtml}`;
3941

src/lib/schematics/install/fonts/project-index-html.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@
88

99
import {SchematicsException} from '@angular-devkit/schematics';
1010
import {WorkspaceProject} from '@schematics/angular/utility/config';
11+
import {getArchitectOptions} from '../../utils/architect-options';
1112

1213
/** Looks for the index HTML file in the given project and returns its path. */
1314
export function getIndexHtmlPath(project: WorkspaceProject): string {
14-
const buildTarget = project.architect.build.options;
15+
const buildOptions = getArchitectOptions(project, 'build');
1516

16-
if (buildTarget.index && buildTarget.index.endsWith('index.html')) {
17-
return buildTarget.index;
17+
if (!buildOptions.index) {
18+
throw new SchematicsException('No project "index.html" file could be found.');
1819
}
1920

20-
throw new SchematicsException('No index.html file was found.');
21+
return buildOptions.index;
2122
}

src/lib/schematics/install/gestures/hammerjs-import.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {Rule, Tree} from '@angular-devkit/schematics';
1010
import {getWorkspace} from '@schematics/angular/utility/config';
1111
import {getProjectFromWorkspace} from '../../utils/get-project';
1212
import {Schema} from '../schema';
13-
import {getProjectMainFile} from './project-main-file';
13+
import {getProjectMainFile} from '../../utils/project-main-file';
1414

1515
const hammerjsImportStatement = `import 'hammerjs';`;
1616

src/lib/schematics/install/index.spec.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {Tree} from '@angular-devkit/schematics';
22
import {SchematicTestRunner} from '@angular-devkit/schematics/testing';
3-
import {getProjectStyleFile} from '@angular/material/schematics/utils/project-style-file';
3+
import {getProjectStyleFile} from '../utils/project-style-file';
44
import {getIndexHtmlPath} from './fonts/project-index-html';
55
import {getProjectFromWorkspace} from '../utils/get-project';
66
import {getFileContent} from '@schematics/angular/utility/test';
@@ -71,10 +71,10 @@ describe('material-install-schematic', () => {
7171
const expectedStylesPath = normalize(`/${project.root}/src/styles.scss`);
7272

7373
const buffer = tree.read(expectedStylesPath);
74-
const src = buffer!.toString();
74+
const themeContent = buffer!.toString();
7575

76-
expect(src.indexOf(`@import '~@angular/material/theming';`)).toBeGreaterThan(-1);
77-
expect(src.indexOf(`$app-primary`)).toBeGreaterThan(-1);
76+
expect(themeContent).toContain(`@import '~@angular/material/theming';`);
77+
expect(themeContent).toContain(`$app-primary: mat-palette(`);
7878
});
7979

8080
it('should create a custom theme file if no SCSS file could be found', () => {
@@ -112,7 +112,7 @@ describe('material-install-schematic', () => {
112112
const workspace = getWorkspace(tree);
113113
const project = getProjectFromWorkspace(workspace);
114114

115-
const defaultStylesPath = getProjectStyleFile(project);
115+
const defaultStylesPath = getProjectStyleFile(project)!;
116116
const htmlContent = tree.read(defaultStylesPath)!.toString();
117117

118118
expect(htmlContent).toContain('html, body { height: 100%; }');

src/lib/schematics/install/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,9 @@ function addMaterialAppStyles(options: Schema) {
9494
const workspace = getWorkspace(host);
9595
const project = getProjectFromWorkspace(workspace, options.project);
9696
const styleFilePath = getProjectStyleFile(project);
97-
const buffer = host.read(styleFilePath);
97+
const buffer = host.read(styleFilePath!);
9898

99-
if (!buffer) {
99+
if (!styleFilePath || !buffer) {
100100
return console.warn(`Could not find styles file: "${styleFilePath}". Skipping styles ` +
101101
`generation. Please consider manually adding the "Roboto" font and resetting the ` +
102102
`body margin.`);

src/lib/schematics/install/schema.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@
44
"title": "Material Install Options Schema",
55
"type": "object",
66
"properties": {
7+
"project": {
8+
"type": "string",
9+
"description": "The name of the project.",
10+
"$default": {
11+
"$source": "projectName"
12+
}
13+
},
714
"skipPackageJson": {
815
"type": "boolean",
916
"default": false,

src/lib/schematics/install/schema.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
*/
88

99
export interface Schema {
10+
11+
/** Name of the project to target. */
12+
project: string;
13+
1014
/** Whether to skip package.json install. */
1115
skipPackageJson: boolean;
1216

@@ -15,7 +19,4 @@ export interface Schema {
1519

1620
/** Name of pre-built theme to install. */
1721
theme: 'indigo-pink' | 'deeppurple-amber' | 'pink-bluegrey' | 'purple-green' | 'custom';
18-
19-
/** Name of the project to target. */
20-
project?: string;
2122
}

src/lib/schematics/install/theming/theming.ts

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export function addThemeToAppStyles(options: Schema): (host: Tree) => Tree {
3131
if (themeName === 'custom') {
3232
insertCustomTheme(project, options.project, host, workspace);
3333
} else {
34-
insertPrebuiltTheme(project, host, themeName, workspace, options.project);
34+
insertPrebuiltTheme(project, host, themeName, workspace);
3535
}
3636

3737
return host;
@@ -49,13 +49,20 @@ function insertCustomTheme(project: WorkspaceProject, projectName: string, host:
4949
const themeContent = createCustomTheme(projectName);
5050

5151
if (!stylesPath) {
52+
if (!project.sourceRoot) {
53+
throw new Error(`Could not find source root for project: "${projectName}". Please make ` +
54+
`sure that the "sourceRoot" property is set in the workspace config.`);
55+
}
56+
5257
// Normalize the path through the devkit utilities because we want to avoid having
5358
// unnecessary path segments and windows backslash delimiters.
5459
const customThemePath = normalize(join(project.sourceRoot, 'custom-theme.scss'));
5560

5661
host.create(customThemePath, themeContent);
57-
addStyleToTarget(project.architect['build'], host, customThemePath, workspace);
58-
return;
62+
63+
// Architect is always defined because we initially asserted if the default builder
64+
// configuration is set up or not.
65+
return addStyleToTarget(project.architect!['build'], host, customThemePath, workspace);
5966
}
6067

6168
const insertion = new InsertChange(stylesPath, 0, themeContent);
@@ -67,17 +74,15 @@ function insertCustomTheme(project: WorkspaceProject, projectName: string, host:
6774

6875
/** Insert a pre-built theme into the angular.json file. */
6976
function insertPrebuiltTheme(project: WorkspaceProject, host: Tree, theme: string,
70-
workspace: WorkspaceSchema, projectName: string) {
77+
workspace: WorkspaceSchema) {
7178

7279
// Path needs to be always relative to the `package.json` or workspace root.
7380
const themePath = `./node_modules/@angular/material/prebuilt-themes/${theme}.css`;
7481

75-
if (project.architect) {
76-
addStyleToTarget(project.architect['build'], host, themePath, workspace);
77-
addStyleToTarget(project.architect['test'], host, themePath, workspace);
78-
} else {
79-
throw new SchematicsException(`${projectName} does not have an architect configuration`);
80-
}
82+
// Architect is always defined because we initially asserted if the default builder
83+
// configuration is set up or not.
84+
addStyleToTarget(project.architect!['build'], host, themePath, workspace);
85+
addStyleToTarget(project.architect!['test'], host, themePath, workspace);
8186
}
8287

8388
/** Adds a style entry to the given target. */

src/lib/schematics/nav/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export default function(options: Schema): Rule {
3030
*/
3131
function addNavModulesToModule(options: Schema) {
3232
return (host: Tree) => {
33-
const modulePath = findModuleFromOptions(host, options);
33+
const modulePath = findModuleFromOptions(host, options)!;
3434
addModuleImportToModule(host, modulePath, 'LayoutModule', '@angular/cdk/layout');
3535
addModuleImportToModule(host, modulePath, 'MatToolbarModule', '@angular/material');
3636
addModuleImportToModule(host, modulePath, 'MatButtonModule', '@angular/material');

src/lib/schematics/table/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export default function(options: Schema): Rule {
3030
*/
3131
function addTableModulesToModule(options: Schema) {
3232
return (host: Tree) => {
33-
const modulePath = findModuleFromOptions(host, options);
33+
const modulePath = findModuleFromOptions(host, options)!;
3434
addModuleImportToModule(host, modulePath, 'MatTableModule', '@angular/material');
3535
addModuleImportToModule(host, modulePath, 'MatPaginatorModule', '@angular/material');
3636
addModuleImportToModule(host, modulePath, 'MatSortModule', '@angular/material');

src/lib/schematics/tree/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export default function(options: Schema): Rule {
3030
*/
3131
function addTreeModulesToModule(options: Schema) {
3232
return (host: Tree) => {
33-
const modulePath = findModuleFromOptions(host, options);
33+
const modulePath = findModuleFromOptions(host, options)!;
3434
addModuleImportToModule(host, modulePath, 'MatTreeModule', '@angular/material');
3535
addModuleImportToModule(host, modulePath, 'MatIconModule', '@angular/material');
3636
addModuleImportToModule(host, modulePath, 'MatButtonModule', '@angular/material');

src/lib/schematics/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"moduleResolution": "node",
66
"outDir": "../../../dist/schematics",
77
"noEmitOnError": false,
8+
"strictNullChecks": true,
89
"skipDefaultLibCheck": true,
910
"skipLibCheck": true,
1011
"sourceMap": true,

src/lib/schematics/update/material/data/input-names.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,11 @@ export interface MaterialInputNameData {
1515
/** The new name for the @Input(). */
1616
replaceWith: string;
1717
/** Whitelist where this replacement is made. If omitted it is made in all HTML & CSS */
18-
whitelist?: {
18+
whitelist: {
1919
/** Limit to elements with any of these element tags. */
2020
elements?: string[],
2121
/** Limit to elements with any of these attributes. */
2222
attributes?: string[],
23-
/**
24-
* Whether inputs in stylesheets should be updated or not. Note that inputs inside of
25-
* stylesheets usually don't make sense, but if developers use an input as a plain one-time
26-
* attribute, it can be targeted through CSS selectors.
27-
*/
28-
stylesheet?: boolean,
2923
};
3024
}
3125

src/lib/schematics/update/material/data/output-names.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export interface MaterialOutputNameData {
1515
/** The new name for the @Output(). */
1616
replaceWith: string;
1717
/** Whitelist where this replacement is made. If omitted it is made in all HTML & CSS */
18-
whitelist?: {
18+
whitelist: {
1919
/** Limit to elements with any of these element tags. */
2020
elements?: string[],
2121
/** Limit to elements with any of these attributes. */

src/lib/schematics/update/material/data/property-names.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export interface MaterialPropertyNameData {
1717
/** Whitelist where this replacement is made. If omitted it is made for all Classes. */
1818
whitelist: {
1919
/** Replace the property only when its type is one of the given Classes. */
20-
classes?: string[];
20+
classes: string[];
2121
};
2222
}
2323

src/lib/schematics/update/material/transform-change-data.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,5 @@ export function getChangesForTarget<T>(target: TargetVersion, data: VersionChang
3333
return [];
3434
}
3535

36-
return data[target].reduce((result, changes) => result.concat(changes.changes), []);
36+
return data[target]!.reduce((result, prData) => result.concat(prData.changes), [] as T[]);
3737
}

src/lib/schematics/update/rules/class-inheritance/classInheritanceCheckRule.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export class Walker extends ProgramAwareRuleWalker {
4141

4242
visitClassDeclaration(node: ts.ClassDeclaration) {
4343
const baseTypes = determineBaseTypes(node);
44+
const className = node.name ? node.name.text : '{unknown-name}';
4445

4546
if (!baseTypes) {
4647
return;
@@ -50,7 +51,7 @@ export class Walker extends ProgramAwareRuleWalker {
5051
const data = this.propertyNames.get(typeName);
5152

5253
if (data) {
53-
this.addFailureAtNode(node, `Found class "${bold(node.name.text)}" which extends class ` +
54+
this.addFailureAtNode(node, `Found class "${bold(className)}" which extends class ` +
5455
`"${bold(typeName)}". Please note that the base class property ` +
5556
`"${red(data.replace)}" has changed to "${green(data.replaceWith)}". ` +
5657
`You may need to update your class as well`);

src/lib/schematics/update/rules/class-inheritance/classInheritanceMiscRule.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,19 @@ export class Walker extends ProgramAwareRuleWalker {
2525

2626
visitClassDeclaration(node: ts.ClassDeclaration) {
2727
const baseTypes = determineBaseTypes(node);
28+
const className = node.name ? node.name.text : '{unknown-name}';
2829

2930
if (!baseTypes) {
3031
return;
3132
}
3233

3334
if (baseTypes.includes('MatFormFieldControl')) {
3435
const hasFloatLabelMember = node.members
35-
.find(member => member.name && member.name.getText() === 'shouldFloatLabel');
36+
.filter(member => member.name)
37+
.find(member => member.name!.getText() === 'shouldFloatLabel');
3638

3739
if (!hasFloatLabelMember) {
38-
this.addFailureAtNode(node, `Found class "${bold(node.name.text)}" which extends ` +
40+
this.addFailureAtNode(node, `Found class "${bold(className)}" which extends ` +
3941
`"${bold('MatFormFieldControl')}". This class must define ` +
4042
`"${green('shouldLabelFloat')}" which is now a required property.`);
4143
}

src/lib/schematics/update/rules/input-names/inputNamesStylesheetRule.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,6 @@ export class Walker extends ComponentWalker {
6868
const replacements: {failureMessage: string, replacement: Replacement}[] = [];
6969

7070
this.data.forEach(name => {
71-
if (name.whitelist && !name.whitelist.stylesheet) {
72-
return;
73-
}
74-
7571
const currentSelector = `[${name.replace}]`;
7672
const updatedSelector = `[${name.replaceWith}]`;
7773

src/lib/schematics/update/rules/input-names/inputNamesTemplateRule.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,16 +55,16 @@ export class Walker extends ComponentWalker {
5555

5656
this.data.forEach(name => {
5757
const whitelist = name.whitelist;
58-
const relativeOffsets = [];
58+
const relativeOffsets: number[] = [];
5959
const failureMessage = `Found deprecated @Input() "${red(name.replace)}"` +
6060
` which has been renamed to "${green(name.replaceWith)}"`;
6161

62-
if (!whitelist || whitelist.attributes) {
62+
if (whitelist.attributes) {
6363
relativeOffsets.push(
6464
...findInputsOnElementWithAttr(templateContent, name.replace, whitelist.attributes));
6565
}
6666

67-
if (!whitelist || whitelist.elements) {
67+
if (whitelist.elements) {
6868
relativeOffsets.push(
6969
...findInputsOnElementWithTag(templateContent, name.replace, whitelist.elements));
7070
}

src/lib/schematics/update/rules/output-names/outputNamesTemplateRule.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,16 +55,16 @@ export class Walker extends ComponentWalker {
5555

5656
this.data.forEach(name => {
5757
const whitelist = name.whitelist;
58-
const relativeOffsets = [];
58+
const relativeOffsets: number[] = [];
5959
const failureMessage = `Found deprecated @Output() "${red(name.replace)}"` +
6060
` which has been renamed to "${green(name.replaceWith)}"`;
6161

62-
if (!whitelist || whitelist.attributes) {
62+
if (whitelist.attributes) {
6363
relativeOffsets.push(
6464
...findOutputsOnElementWithAttr(templateContent, name.replace, whitelist.attributes));
6565
}
6666

67-
if (!whitelist || whitelist.elements) {
67+
if (whitelist.elements) {
6868
relativeOffsets.push(
6969
...findOutputsOnElementWithTag(templateContent, name.replace, whitelist.elements));
7070
}

src/lib/schematics/update/rules/signature-check/constructorSignatureCheckRule.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ export class Walker extends ProgramAwareRuleWalker {
4040
return signature.getParameters()
4141
.map(param => param.declarations[0] as ts.ParameterDeclaration)
4242
.map(node => node.type)
43-
.map(node => this.getTypeChecker().getTypeFromTypeNode(node));
43+
// TODO(devversion): handle non resolvable constructor types
44+
.map(typeNode => this.getTypeChecker().getTypeFromTypeNode(typeNode!));
4445
}
4546

4647
private checkExpressionSignature(node: ts.CallExpression | ts.NewExpression) {
@@ -50,7 +51,7 @@ export class Walker extends ProgramAwareRuleWalker {
5051

5152
// TODO(devversion): Consider handling pass-through classes better.
5253
// TODO(devversion): e.g. `export class CustomCalendar extends MatCalendar {}`
53-
if (!classType || !constructorChecks.includes(className)) {
54+
if (!classType || !constructorChecks.includes(className) || !node.arguments) {
5455
return;
5556
}
5657

@@ -62,6 +63,8 @@ export class Walker extends ProgramAwareRuleWalker {
6263
// TODO(devversion): we should check if the type is assignable to the signature
6364
// TODO(devversion): blocked on https://github.com/Microsoft/TypeScript/issues/9879
6465
const doesMatchSignature = classSignatures.some(signature => {
66+
// TODO(devversion): better handling if signature item type is unresolved but assignable
67+
// to everything.
6568
return signature.every((type, index) => callExpressionSignature[index] === type) &&
6669
signature.length === callExpressionSignature.length;
6770
});

src/lib/schematics/update/typescript/base-types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ export function determineBaseTypes(node: ts.ClassDeclaration): string[] | null {
1515
}
1616

1717
return node.heritageClauses
18-
.reduce((types, clause) => types.concat(clause.types), [])
18+
.reduce((types, clause) => types.concat(clause.types), [] as ts.ExpressionWithTypeArguments[])
1919
.map(typeExpression => typeExpression.expression)
2020
.filter(expression => expression && ts.isIdentifier(expression))
21-
.map(identifier => identifier.text);
21+
.map((identifier: ts.Identifier) => identifier.text);
2222
}

0 commit comments

Comments
 (0)