Visitar URL original
chore(core): ratchet strictNullChecks errors against a per-file baseline by NathanWalker · Pull Request #11511 · NativeScript/NativeScript · GitHub
Skip to content
Open
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
33 changes: 33 additions & 0 deletions .github/workflows/core_typecheck.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Type-checks packages/core under strictNullChecks against the per-file error
# baseline in packages/core/strict-baseline.json, so the error count can only
# go down.
name: 'Core strict type-check'
on:
push:
branches:
- main
pull_request:

permissions:
contents: read

env:
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
typecheck-strict:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 23.5.0
cache: 'npm'
- name: 'Install dependencies'
run: npm ci
- name: 'Check strictNullChecks baseline'
run: npx nx run core:typecheck-strict
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ To add a skill: create `.agent/skills/<kebab-case-name>/SKILL.md` with `name` an
- Watch mode: `npx nx run core:test --watch`
- Single suite by describe name: `npx nx run core:test -t 'XmlParser'`
- Unit tests run in Node with NativeScript platform globals mocked in `packages/core/vitest.setup.ts` — they cannot exercise real native APIs. Behavior that touches iOS/Android at runtime is covered by the e2e suite: `npx nx run apps-automated:ios` or `npx nx run apps-automated:android` (requires a configured NativeScript environment with simulators/emulators).
- `npx nx run core:typecheck-strict` type-checks core under `strictNullChecks` against the per-file error counts in `packages/core/strict-baseline.json` (CI enforces it). Fixing strict errors? Run it with `--update` to lower the baseline in the same change; it refuses to record increases.
- Prefer adding a unit test for logic changes; add or extend an `apps/automated` test for native runtime behavior.

## Formatting & Commits
Expand Down
6 changes: 6 additions & 0 deletions packages/core/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@
"options": {
"reportsDirectory": "../../coverage/packages/core"
}
},
"typecheck-strict": {
"executor": "nx:run-commands",
"options": {
"command": "node tools/scripts/strict-baseline.mjs"
}
}
}
}
178 changes: 178 additions & 0 deletions packages/core/strict-baseline.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
{
"accessibility/accessibility-common.ts": 4,
"accessibility/accessibility-properties.ts": 3,
"application-settings/index.android.ts": 9,
"application-settings/index.ios.ts": 9,
"application/application-common.ts": 5,
"application/application.android.ts": 21,
"application/application.ios.ts": 33,
"application/helpers.android.ts": 2,
"color/color-common.ts": 2,
"color/color-utils.ts": 1,
"color/known-colors.ts": 1,
"connectivity/index.android.ts": 2,
"connectivity/index.ios.ts": 3,
"css-mediaquery/index.ts": 5,
"css/CSS3Parser.ts": 28,
"css/CSSNativeScript.ts": 5,
"css/css-tree-parser.ts": 2,
"css/lib/parse/index.ts": 4,
"css/parser.ts": 32,
"css/system-classes.ts": 3,
"data/observable-array/index.ts": 9,
"data/observable/index.ts": 2,
"data/virtual-array/index.ts": 1,
"debugger/dom-types.ts": 2,
"debugger/webinspector-css.ts": 1,
"debugger/webinspector-dom.ts": 3,
"debugger/webinspector-network.android.ts": 3,
"debugger/webinspector-network.ios.ts": 2,
"file-system/file-system-access.android.ts": 42,
"file-system/file-system-access.ios.ts": 11,
"file-system/index.ts": 12,
"globals/global-utils.ts": 2,
"globals/index.ts": 3,
"http/http-request-internal/index.android.ts": 8,
"http/http-request-internal/index.ios.ts": 3,
"http/http-request/index.android.ts": 2,
"http/http-request/index.ios.ts": 1,
"http/index.ts": 5,
"image-asset/index.ios.ts": 1,
"image-source/index.android.ts": 15,
"image-source/index.ios.ts": 31,
"inspector_modules.ts": 2,
"media-query-list/index.ts": 1,
"module-name-resolver/helpers.ts": 1,
"module-name-resolver/index.ts": 1,
"module-name-resolver/qualifier-matcher/index.ts": 6,
"native-window/native-window-common.ts": 6,
"native-window/native-window.android.ts": 3,
"native-window/native-window.ios.ts": 2,
"profiling/index.ts": 3,
"timer/index.android.ts": 1,
"timer/index.ios.ts": 1,
"trace/index.ts": 2,
"ui/action-bar/action-bar-common.ts": 5,
"ui/action-bar/index.android.ts": 10,
"ui/action-bar/index.ios.ts": 32,
"ui/animation/animation-common.ts": 6,
"ui/animation/index.android.ts": 3,
"ui/animation/index.ios.ts": 16,
"ui/animation/keyframe-animation.ts": 15,
"ui/builder/binding-builder.ts": 12,
"ui/builder/index.ts": 10,
"ui/button/index.ios.ts": 17,
"ui/content-view/index.ts": 1,
"ui/core/bindable/bindable-expressions.ts": 6,
"ui/core/bindable/index.ts": 21,
"ui/core/properties/index.ts": 21,
"ui/core/view-base/index.ts": 21,
"ui/core/view/index.android.ts": 26,
"ui/core/view/index.ios.ts": 31,
"ui/core/view/view-common.ts": 24,
"ui/core/view/view-helper/index.android.ts": 1,
"ui/core/view/view-helper/index.ios.ts": 28,
"ui/core/view/view-helper/view-helper-common.ts": 5,
"ui/core/weak-event-listener/index.ts": 1,
"ui/date-picker/index.ios.ts": 4,
"ui/dialogs/dialogs-common.ts": 5,
"ui/dialogs/index.android.ts": 13,
"ui/dialogs/index.ios.ts": 14,
"ui/editable-text-base/index.android.ts": 12,
"ui/editable-text-base/index.ios.ts": 3,
"ui/frame/fragment.android.ts": 1,
"ui/frame/fragment.transitions.android.ts": 60,
"ui/frame/frame-common.ts": 21,
"ui/frame/frame-helper-for-android.ts": 14,
"ui/frame/index.android.ts": 19,
"ui/frame/index.ios.ts": 28,
"ui/gestures/gestures-common.ts": 2,
"ui/gestures/index.android.ts": 17,
"ui/gestures/index.ios.ts": 23,
"ui/gestures/touch-manager.ts": 4,
"ui/html-view/index.ios.ts": 3,
"ui/image-cache/image-cache-common.ts": 5,
"ui/image-cache/index.ios.ts": 1,
"ui/image/image-common.ts": 5,
"ui/image/index.android.ts": 5,
"ui/image/index.ios.ts": 2,
"ui/label/index.ios.ts": 1,
"ui/layouts/flexbox-layout/flexbox-layout-common.ts": 1,
"ui/layouts/flexbox-layout/index.ios.ts": 1,
"ui/layouts/grid-layout/index.android.ts": 2,
"ui/layouts/layout-base-common.ts": 2,
"ui/layouts/root-layout/index.android.ts": 24,
"ui/layouts/root-layout/index.ios.ts": 10,
"ui/layouts/root-layout/root-layout-common.ts": 27,
"ui/layouts/root-layout/root-layout-stack.ts": 2,
"ui/list-picker/index.android.ts": 4,
"ui/list-picker/index.ios.ts": 3,
"ui/list-picker/list-picker-common.ts": 1,
"ui/list-view/index.android.ts": 23,
"ui/list-view/index.ios.ts": 17,
"ui/list-view/list-view-common.ts": 3,
"ui/page/index.android.ts": 1,
"ui/page/index.ios.ts": 24,
"ui/progress/index.android.ts": 3,
"ui/progress/index.ios.ts": 3,
"ui/proxy-view-container/index.ts": 3,
"ui/repeater/index.ts": 6,
"ui/scroll-view/index.android.ts": 1,
"ui/scroll-view/index.ios.ts": 1,
"ui/search-bar/index.android.ts": 5,
"ui/search-bar/index.ios.ts": 9,
"ui/segmented-bar/index.android.ts": 3,
"ui/segmented-bar/index.ios.ts": 6,
"ui/slider/index.android.ts": 2,
"ui/slider/index.ios.ts": 6,
"ui/split-view/index.ios.ts": 3,
"ui/styling/background-common.ts": 1,
"ui/styling/background.android.ts": 11,
"ui/styling/background.ios.ts": 44,
"ui/styling/converters.ts": 2,
"ui/styling/css-animation-parser.ts": 6,
"ui/styling/css-selector.ts": 25,
"ui/styling/css-shadow.ts": 2,
"ui/styling/css-stroke.ts": 2,
"ui/styling/css-transform.ts": 1,
"ui/styling/css-utils.ts": 1,
"ui/styling/font-common.ts": 4,
"ui/styling/font.android.ts": 10,
"ui/styling/font.ios.ts": 7,
"ui/styling/linear-gradient.ts": 1,
"ui/styling/style-properties.ts": 10,
"ui/styling/style-scope.ts": 38,
"ui/styling/style/index.ts": 3,
"ui/switch/index.ios.ts": 5,
"ui/tab-view/index.android.ts": 21,
"ui/tab-view/index.ios.ts": 27,
"ui/tab-view/tab-view-common.ts": 1,
"ui/text-base/index.android.ts": 14,
"ui/text-base/index.ios.ts": 22,
"ui/text-base/text-base-common.ts": 1,
"ui/text-field/index.ios.ts": 9,
"ui/text-view/index.ios.ts": 6,
"ui/time-picker/index.ios.ts": 2,
"ui/time-picker/time-picker-common.ts": 4,
"ui/transition/fade-transition.android.ts": 4,
"ui/transition/flip-transition.android.ts": 4,
"ui/transition/modal-transition.ios.ts": 12,
"ui/transition/page-transition.android.ts": 15,
"ui/transition/page-transition.ios.ts": 6,
"ui/transition/shared-transition-helper.ios.ts": 203,
"ui/transition/slide-transition.android.ts": 16,
"ui/transition/slide-transition.ios.ts": 1,
"ui/web-view/index.ios.ts": 9,
"ui/web-view/web-view-common.ts": 1,
"utils/common.ts": 5,
"utils/debug-source.ts": 2,
"utils/index.android.ts": 3,
"utils/index.ios.ts": 3,
"utils/native-helper-for-android.ts": 17,
"utils/native-helper.ios.ts": 11,
"utils/shared.ts": 1,
"utils/types.ts": 3,
"wgc/crypto/SubtleCrypto.ts": 46,
"xhr/index.ts": 10,
"xml/index.ts": 13
}
8 changes: 8 additions & 0 deletions packages/core/tsconfig.strict.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.lib.json",
"compilerOptions": {
"strictNullChecks": true,
"noEmit": true,
"diagnostics": false
}
}
9 changes: 9 additions & 0 deletions tools/notes/DevelopmentWorkflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,15 @@ Run a single test by it's describe name, for example to run just the `xml/index.
npx nx run core:test --watch -t 'XmlParser'
```

## Strict null checks

`packages/core` is not yet clean under `strictNullChecks`. CI type-checks it in strict mode and compares the error count of each file with `packages/core/strict-baseline.json`: a file may not gain errors, and a file that loses some must lower its baseline in the same change.

```bash
npx nx run core:typecheck-strict # check against the baseline
npx nx run core:typecheck-strict --update # record lowered counts
```

## Running the `e2e` Test Apps

There are a couple of application used for development and testing.
Expand Down
87 changes: 87 additions & 0 deletions tools/scripts/strict-baseline.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Ratchet for type-checking packages/core under strictNullChecks: per-file error
// counts must match packages/core/strict-baseline.json. Counts are kept per file
// so an error fixed in one file cannot hide one added in another.
//
// node tools/scripts/strict-baseline.mjs check against the baseline
// node tools/scripts/strict-baseline.mjs --update record lowered counts
import { spawnSync } from 'node:child_process';
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import { createRequire } from 'node:module';
import { dirname, join, relative } from 'node:path';
import { fileURLToPath } from 'node:url';

const root = join(dirname(fileURLToPath(import.meta.url)), '../..');
const coreDir = join(root, 'packages/core');
const baselinePath = join(coreDir, 'strict-baseline.json');
const baselineName = relative(root, baselinePath);
const update = process.argv.includes('--update');

const tsc = createRequire(import.meta.url).resolve('typescript/bin/tsc');
const run = spawnSync(process.execPath, [tsc, '-p', 'tsconfig.strict.json', '--pretty', 'false'], { cwd: coreDir, encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 });
if (run.error) throw run.error;

// A diagnostic starts at column 0; its continuation lines are indented.
const FILE_ERROR = /^(.+)\(\d+,\d+\): error TS\d+:/;
const diagnostics = [];
for (const line of run.stdout.split('\n')) {
if (/^\S/.test(line)) diagnostics.push(line);
else if (line && diagnostics.length) diagnostics[diagnostics.length - 1] += '\n' + line;
}

const current = {};
const errorsByFile = {};
for (const diagnostic of diagnostics) {
const match = FILE_ERROR.exec(diagnostic);
if (!match) {
// Errors without a file (bad config, missing lib) cannot be baselined.
console.error(run.stdout + run.stderr);
fail('tsc reported an error that is not tied to a file.');
}
const file = match[1];
current[file] = (current[file] ?? 0) + 1;
(errorsByFile[file] ??= []).push(diagnostic);
}
if (run.status !== 0 && diagnostics.length === 0) {
console.error(run.stdout + run.stderr);
fail(`tsc exited with ${run.status} without reporting errors.`);
}

const hasBaseline = existsSync(baselinePath);
if (!hasBaseline && !update) fail(`${baselineName} is missing; create it with --update.`);
const baseline = hasBaseline ? JSON.parse(readFileSync(baselinePath, 'utf8')) : {};
const files = [...new Set([...Object.keys(baseline), ...Object.keys(current)])].sort();
const increased = files.filter((file) => (current[file] ?? 0) > (baseline[file] ?? 0));
const decreased = files.filter((file) => (current[file] ?? 0) < (baseline[file] ?? 0));
const total = (counts) => Object.values(counts).reduce((sum, count) => sum + count, 0);
const describe = (file) => ` ${file}: ${baseline[file] ?? 0} -> ${current[file] ?? 0}`;

if (hasBaseline && increased.length) {
for (const file of increased) console.error(errorsByFile[file].join('\n'));
console.error(`\nstrictNullChecks errors increased in ${increased.length} file(s):`);
console.error(increased.map(describe).join('\n'));
fail(`Fix the new errors above; ${baselineName} only goes down${update ? ', so it was not updated' : ''}.`);
}

if (update) {
const next = Object.fromEntries(
Object.keys(current)
.sort()
.map((file) => [file, current[file]]),
);
writeFileSync(baselinePath, JSON.stringify(next, null, '\t') + '\n');
console.log(`Wrote ${baselineName}: ${total(next)} errors in ${Object.keys(next).length} files (was ${total(baseline)}).`);
process.exit(0);
}

if (decreased.length) {
console.error(`strictNullChecks errors decreased in ${decreased.length} file(s):`);
console.error(decreased.map(describe).join('\n'));
fail(`Lower the baseline in the same change: npx nx run core:typecheck-strict --update`);
}

console.log(`strictNullChecks: ${total(current)} errors in ${Object.keys(current).length} files, matching ${baselineName}.`);

function fail(message) {
console.error(message);
process.exit(1);
}
Loading