Skip to content

refactor(schematics): enable strict null checks #12983

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/lib/schematics/address-form/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export default function(options: Schema): Rule {
*/
function addFormModulesToModule(options: Schema) {
return (host: Tree) => {
const modulePath = findModuleFromOptions(host, options);
const modulePath = findModuleFromOptions(host, options)!;
Copy link
Member Author

@devversion devversion Sep 5, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is never undefined or null because the addXModulesToModule methods are only called if skipImport is set to false. And the findModuleFromOptions method only returns undefined if that option is enabled.

See: https://github.com/angular/angular-cli/blob/master/packages/schematics/angular/utility/find-module.ts#L26

@jelbourn Want me to add a comment for this? (afraid of repeating it for every schematic though)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could the function return an empty string in that case?

Copy link
Member Author

@devversion devversion Sep 5, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would be the benefit? This comes from an utility function from @schematics/angular. It should be really never undefined because we already check for skipImport before.

addModuleImportToModule(host, modulePath, 'MatInputModule', '@angular/material');
addModuleImportToModule(host, modulePath, 'MatButtonModule', '@angular/material');
addModuleImportToModule(host, modulePath, 'MatSelectModule', '@angular/material');
Expand Down
2 changes: 1 addition & 1 deletion src/lib/schematics/dashboard/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export default function(options: Schema): Rule {
*/
function addNavModulesToModule(options: Schema) {
return (host: Tree) => {
const modulePath = findModuleFromOptions(host, options);
const modulePath = findModuleFromOptions(host, options)!;
addModuleImportToModule(host, modulePath, 'MatGridListModule', '@angular/material');
addModuleImportToModule(host, modulePath, 'MatCardModule', '@angular/material');
addModuleImportToModule(host, modulePath, 'MatMenuModule', '@angular/material');
Expand Down
4 changes: 3 additions & 1 deletion src/lib/schematics/install/fonts/head-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ export function appendElementToHead(host: Tree, project: WorkspaceProject, eleme
throw `Could not find '<head>' element in HTML file: ${indexPath}`;
}

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

Expand Down
9 changes: 5 additions & 4 deletions src/lib/schematics/install/fonts/project-index-html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@

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

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

if (buildTarget.index && buildTarget.index.endsWith('index.html')) {
return buildTarget.index;
if (!buildOptions.index) {
throw new SchematicsException('No project "index.html" file could be found.');
}

throw new SchematicsException('No index.html file was found.');
return buildOptions.index;
}
2 changes: 1 addition & 1 deletion src/lib/schematics/install/gestures/hammerjs-import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {Rule, Tree} from '@angular-devkit/schematics';
import {getWorkspace} from '@schematics/angular/utility/config';
import {getProjectFromWorkspace} from '../../utils/get-project';
import {Schema} from '../schema';
import {getProjectMainFile} from './project-main-file';
import {getProjectMainFile} from '../../utils/project-main-file';

const hammerjsImportStatement = `import 'hammerjs';`;

Expand Down
10 changes: 5 additions & 5 deletions src/lib/schematics/install/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {Tree} from '@angular-devkit/schematics';
import {SchematicTestRunner} from '@angular-devkit/schematics/testing';
import {getProjectStyleFile} from '@angular/material/schematics/utils/project-style-file';
import {getProjectStyleFile} from '../utils/project-style-file';
import {getIndexHtmlPath} from './fonts/project-index-html';
import {getProjectFromWorkspace} from '../utils/get-project';
import {getFileContent} from '@schematics/angular/utility/test';
Expand Down Expand Up @@ -71,10 +71,10 @@ describe('material-install-schematic', () => {
const expectedStylesPath = normalize(`/${project.root}/src/styles.scss`);

const buffer = tree.read(expectedStylesPath);
const src = buffer!.toString();
const themeContent = buffer!.toString();

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

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

const defaultStylesPath = getProjectStyleFile(project);
const defaultStylesPath = getProjectStyleFile(project)!;
const htmlContent = tree.read(defaultStylesPath)!.toString();

expect(htmlContent).toContain('html, body { height: 100%; }');
Expand Down
4 changes: 2 additions & 2 deletions src/lib/schematics/install/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,9 @@ function addMaterialAppStyles(options: Schema) {
const workspace = getWorkspace(host);
const project = getProjectFromWorkspace(workspace, options.project);
const styleFilePath = getProjectStyleFile(project);
const buffer = host.read(styleFilePath);
const buffer = host.read(styleFilePath!);

if (!buffer) {
if (!styleFilePath || !buffer) {
return console.warn(`Could not find styles file: "${styleFilePath}". Skipping styles ` +
`generation. Please consider manually adding the "Roboto" font and resetting the ` +
`body margin.`);
Expand Down
7 changes: 7 additions & 0 deletions src/lib/schematics/install/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@
"title": "Material Install Options Schema",
"type": "object",
"properties": {
"project": {
"type": "string",
"description": "The name of the project.",
"$default": {
"$source": "projectName"
}
},
"skipPackageJson": {
"type": "boolean",
"default": false,
Expand Down
7 changes: 4 additions & 3 deletions src/lib/schematics/install/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
*/

export interface Schema {

/** Name of the project to target. */
project: string;

/** Whether to skip package.json install. */
skipPackageJson: boolean;

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

/** Name of pre-built theme to install. */
theme: 'indigo-pink' | 'deeppurple-amber' | 'pink-bluegrey' | 'purple-green' | 'custom';

/** Name of the project to target. */
project?: string;
}
25 changes: 15 additions & 10 deletions src/lib/schematics/install/theming/theming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export function addThemeToAppStyles(options: Schema): (host: Tree) => Tree {
if (themeName === 'custom') {
insertCustomTheme(project, options.project, host, workspace);
} else {
insertPrebuiltTheme(project, host, themeName, workspace, options.project);
insertPrebuiltTheme(project, host, themeName, workspace);
}

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

if (!stylesPath) {
if (!project.sourceRoot) {
throw new Error(`Could not find source root for project: "${projectName}". Please make ` +
`sure that the "sourceRoot" property is set in the workspace config.`);
}

// Normalize the path through the devkit utilities because we want to avoid having
// unnecessary path segments and windows backslash delimiters.
const customThemePath = normalize(join(project.sourceRoot, 'custom-theme.scss'));

host.create(customThemePath, themeContent);
addStyleToTarget(project.architect['build'], host, customThemePath, workspace);
return;

// Architect is always defined because we initially asserted if the default builder
// configuration is set up or not.
return addStyleToTarget(project.architect!['build'], host, customThemePath, workspace);
}

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

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

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

if (project.architect) {
addStyleToTarget(project.architect['build'], host, themePath, workspace);
addStyleToTarget(project.architect['test'], host, themePath, workspace);
} else {
throw new SchematicsException(`${projectName} does not have an architect configuration`);
}
// Architect is always defined because we initially asserted if the default builder
// configuration is set up or not.
addStyleToTarget(project.architect!['build'], host, themePath, workspace);
addStyleToTarget(project.architect!['test'], host, themePath, workspace);
}

/** Adds a style entry to the given target. */
Expand Down
2 changes: 1 addition & 1 deletion src/lib/schematics/nav/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export default function(options: Schema): Rule {
*/
function addNavModulesToModule(options: Schema) {
return (host: Tree) => {
const modulePath = findModuleFromOptions(host, options);
const modulePath = findModuleFromOptions(host, options)!;
addModuleImportToModule(host, modulePath, 'LayoutModule', '@angular/cdk/layout');
addModuleImportToModule(host, modulePath, 'MatToolbarModule', '@angular/material');
addModuleImportToModule(host, modulePath, 'MatButtonModule', '@angular/material');
Expand Down
2 changes: 1 addition & 1 deletion src/lib/schematics/table/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export default function(options: Schema): Rule {
*/
function addTableModulesToModule(options: Schema) {
return (host: Tree) => {
const modulePath = findModuleFromOptions(host, options);
const modulePath = findModuleFromOptions(host, options)!;
addModuleImportToModule(host, modulePath, 'MatTableModule', '@angular/material');
addModuleImportToModule(host, modulePath, 'MatPaginatorModule', '@angular/material');
addModuleImportToModule(host, modulePath, 'MatSortModule', '@angular/material');
Expand Down
2 changes: 1 addition & 1 deletion src/lib/schematics/tree/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export default function(options: Schema): Rule {
*/
function addTreeModulesToModule(options: Schema) {
return (host: Tree) => {
const modulePath = findModuleFromOptions(host, options);
const modulePath = findModuleFromOptions(host, options)!;
addModuleImportToModule(host, modulePath, 'MatTreeModule', '@angular/material');
addModuleImportToModule(host, modulePath, 'MatIconModule', '@angular/material');
addModuleImportToModule(host, modulePath, 'MatButtonModule', '@angular/material');
Expand Down
1 change: 1 addition & 0 deletions src/lib/schematics/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"moduleResolution": "node",
"outDir": "../../../dist/schematics",
"noEmitOnError": false,
"strictNullChecks": true,
"skipDefaultLibCheck": true,
"skipLibCheck": true,
"sourceMap": true,
Expand Down
8 changes: 1 addition & 7 deletions src/lib/schematics/update/material/data/input-names.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,11 @@ export interface MaterialInputNameData {
/** The new name for the @Input(). */
replaceWith: string;
/** Whitelist where this replacement is made. If omitted it is made in all HTML & CSS */
whitelist?: {
whitelist: {
/** Limit to elements with any of these element tags. */
elements?: string[],
/** Limit to elements with any of these attributes. */
attributes?: string[],
/**
* Whether inputs in stylesheets should be updated or not. Note that inputs inside of
* stylesheets usually don't make sense, but if developers use an input as a plain one-time
* attribute, it can be targeted through CSS selectors.
*/
stylesheet?: boolean,
};
}

Expand Down
2 changes: 1 addition & 1 deletion src/lib/schematics/update/material/data/output-names.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export interface MaterialOutputNameData {
/** The new name for the @Output(). */
replaceWith: string;
/** Whitelist where this replacement is made. If omitted it is made in all HTML & CSS */
whitelist?: {
whitelist: {
/** Limit to elements with any of these element tags. */
elements?: string[],
/** Limit to elements with any of these attributes. */
Expand Down
2 changes: 1 addition & 1 deletion src/lib/schematics/update/material/data/property-names.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export interface MaterialPropertyNameData {
/** Whitelist where this replacement is made. If omitted it is made for all Classes. */
whitelist: {
/** Replace the property only when its type is one of the given Classes. */
classes?: string[];
classes: string[];
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,5 @@ export function getChangesForTarget<T>(target: TargetVersion, data: VersionChang
return [];
}

return data[target].reduce((result, changes) => result.concat(changes.changes), []);
return data[target]!.reduce((result, prData) => result.concat(prData.changes), [] as T[]);
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export class Walker extends ProgramAwareRuleWalker {

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

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

if (data) {
this.addFailureAtNode(node, `Found class "${bold(node.name.text)}" which extends class ` +
this.addFailureAtNode(node, `Found class "${bold(className)}" which extends class ` +
`"${bold(typeName)}". Please note that the base class property ` +
`"${red(data.replace)}" has changed to "${green(data.replaceWith)}". ` +
`You may need to update your class as well`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,19 @@ export class Walker extends ProgramAwareRuleWalker {

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

if (!baseTypes) {
return;
}

if (baseTypes.includes('MatFormFieldControl')) {
const hasFloatLabelMember = node.members
.find(member => member.name && member.name.getText() === 'shouldFloatLabel');
.filter(member => member.name)
.find(member => member.name!.getText() === 'shouldFloatLabel');

if (!hasFloatLabelMember) {
this.addFailureAtNode(node, `Found class "${bold(node.name.text)}" which extends ` +
this.addFailureAtNode(node, `Found class "${bold(className)}" which extends ` +
`"${bold('MatFormFieldControl')}". This class must define ` +
`"${green('shouldLabelFloat')}" which is now a required property.`);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,6 @@ export class Walker extends ComponentWalker {
const replacements: {failureMessage: string, replacement: Replacement}[] = [];

this.data.forEach(name => {
if (name.whitelist && !name.whitelist.stylesheet) {
return;
}

const currentSelector = `[${name.replace}]`;
const updatedSelector = `[${name.replaceWith}]`;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,16 +55,16 @@ export class Walker extends ComponentWalker {

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

if (!whitelist || whitelist.attributes) {
if (whitelist.attributes) {
relativeOffsets.push(
...findInputsOnElementWithAttr(templateContent, name.replace, whitelist.attributes));
}

if (!whitelist || whitelist.elements) {
if (whitelist.elements) {
relativeOffsets.push(
...findInputsOnElementWithTag(templateContent, name.replace, whitelist.elements));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,16 +55,16 @@ export class Walker extends ComponentWalker {

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

if (!whitelist || whitelist.attributes) {
if (whitelist.attributes) {
relativeOffsets.push(
...findOutputsOnElementWithAttr(templateContent, name.replace, whitelist.attributes));
}

if (!whitelist || whitelist.elements) {
if (whitelist.elements) {
relativeOffsets.push(
...findOutputsOnElementWithTag(templateContent, name.replace, whitelist.elements));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ export class Walker extends ProgramAwareRuleWalker {
return signature.getParameters()
.map(param => param.declarations[0] as ts.ParameterDeclaration)
.map(node => node.type)
.map(node => this.getTypeChecker().getTypeFromTypeNode(node));
// TODO(devversion): handle non resolvable constructor types
.map(typeNode => this.getTypeChecker().getTypeFromTypeNode(typeNode!));
}

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

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

Expand All @@ -62,6 +63,8 @@ export class Walker extends ProgramAwareRuleWalker {
// TODO(devversion): we should check if the type is assignable to the signature
// TODO(devversion): blocked on https://github.com/Microsoft/TypeScript/issues/9879
const doesMatchSignature = classSignatures.some(signature => {
// TODO(devversion): better handling if signature item type is unresolved but assignable
// to everything.
return signature.every((type, index) => callExpressionSignature[index] === type) &&
signature.length === callExpressionSignature.length;
});
Expand Down
4 changes: 2 additions & 2 deletions src/lib/schematics/update/typescript/base-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ export function determineBaseTypes(node: ts.ClassDeclaration): string[] | null {
}

return node.heritageClauses
.reduce((types, clause) => types.concat(clause.types), [])
.reduce((types, clause) => types.concat(clause.types), [] as ts.ExpressionWithTypeArguments[])
.map(typeExpression => typeExpression.expression)
.filter(expression => expression && ts.isIdentifier(expression))
.map(identifier => identifier.text);
.map((identifier: ts.Identifier) => identifier.text);
}
Loading