Migrate @azure-tools/typespec-go from autorest.go into this repo#4628
Migrate @azure-tools/typespec-go from autorest.go into this repo#4628tadelesh wants to merge 28 commits into
Conversation
|
All changed packages have been documented.
Show changes
|
⚡ Benchmark Results
Full details – comparing
|
| Metric | Baseline | Current | Change |
|---|---|---|---|
| total | 🔴 599.1ms | 🔴 532.9ms | -11.0% 🟢 |
| loader | 🟢 148.9ms | 🟢 157.2ms | +5.6% 🔴 |
| resolver | 🟢 16.2ms | 🟢 19.6ms | +21.0% 🔴 |
| checker | 🟢 186.4ms | 🟢 185.8ms | -0.3% |
| validation | 🟢 41.0ms | 🟢 41.8ms | +2.1% |
| ↳ validation/@azure-tools/typespec-azure-core | 🟢 6.4ms | 🟢 6.1ms | -4.5% |
| ↳ validation/@typespec/http | 🟢 5.5ms | 🟢 5.1ms | -8.1% |
| ↳ validation/@typespec/rest | 🟢 0.5ms | 🟢 0.6ms | +20.8% |
| ↳ validation/@typespec/versioning | 🔴 27.1ms | 🔴 27.9ms | +2.9% |
| ↳ validation/compiler | 🟢 1.3ms | 🟢 1.7ms | +24.3% |
| linter | 🟢 132.1ms | 🟢 127.6ms | -3.4% |
| ↳ linter/@azure-tools/typespec-azure-core/auth-required | 🟢 0.0ms | 🟢 0.0ms | -1.7% |
| ↳ linter/@azure-tools/typespec-azure-core/bad-record-type | 🟢 0.2ms | 🟢 0.2ms | -12.0% |
| ↳ linter/@azure-tools/typespec-azure-core/byos | 🟢 5.6ms | 🟢 5.3ms | -5.0% |
| ↳ linter/@azure-tools/typespec-azure-core/casing-style | 🟢 0.6ms | 🟢 0.6ms | +3.4% |
| ↳ linter/@azure-tools/typespec-azure-core/composition-over-inheritance | 🟢 0.1ms | 🟢 0.1ms | +32.3% |
| ↳ linter/@azure-tools/typespec-azure-core/documentation-required | 🟢 0.8ms | 🟢 0.8ms | +10.4% |
| ↳ linter/@azure-tools/typespec-azure-core/friendly-name | 🟢 0.7ms | 🟢 0.6ms | -11.3% |
| ↳ linter/@azure-tools/typespec-azure-core/key-visibility-required | 🟢 0.2ms | 🟢 0.2ms | -1.8% |
| ↳ linter/@azure-tools/typespec-azure-core/known-encoding | 🟢 0.3ms | 🟢 0.3ms | -6.6% |
| ↳ linter/@azure-tools/typespec-azure-core/long-running-polling-operation-required | 🟢 0.3ms | 🟢 0.3ms | -1.4% |
| ↳ linter/@azure-tools/typespec-azure-core/no-case-mismatch | 🟢 0.2ms | 🟢 0.3ms | +8.8% |
| ↳ linter/@azure-tools/typespec-azure-core/no-closed-literal-union | 🟢 0.3ms | 🟢 0.3ms | +2.0% |
| ↳ linter/@azure-tools/typespec-azure-core/no-enum | 🟢 0.0ms | 🟢 0.0ms | +6.3% |
| ↳ linter/@azure-tools/typespec-azure-core/no-error-status-codes | 🟢 0.1ms | 🟢 0.1ms | -1.6% |
| ↳ linter/@azure-tools/typespec-azure-core/no-explicit-routes-resource-ops | 🟢 0.1ms | 🟢 0.1ms | -2.4% |
| ↳ linter/@azure-tools/typespec-azure-core/no-format | 🟢 0.6ms | 🟢 0.6ms | -13.7% |
| ↳ linter/@azure-tools/typespec-azure-core/no-generic-numeric | 🟢 0.4ms | 🟢 0.4ms | +21.3% |
| ↳ linter/@azure-tools/typespec-azure-core/no-header-explode | 🟡 18.0ms | 🟡 17.7ms | -1.9% |
| ↳ linter/@azure-tools/typespec-azure-core/no-legacy-usage | 🟢 1.1ms | 🟢 1.1ms | +0.0% |
| ↳ linter/@azure-tools/typespec-azure-core/no-multiple-discriminator | 🟢 0.1ms | 🟢 0.1ms | +2.6% |
| ↳ linter/@azure-tools/typespec-azure-core/no-nullable | 🟢 0.2ms | 🟢 0.2ms | -1.4% |
| ↳ linter/@azure-tools/typespec-azure-core/no-offsetdatetime | 🟢 1.2ms | 🟢 1.2ms | -5.1% |
| ↳ linter/@azure-tools/typespec-azure-core/no-openapi | 🟢 2.0ms | 🟢 2.0ms | -1.2% |
| ↳ linter/@azure-tools/typespec-azure-core/no-private-usage | 🟢 1.8ms | 🟢 1.9ms | +6.0% |
| ↳ linter/@azure-tools/typespec-azure-core/no-query-explode | 🟡 19.8ms | 🟡 18.6ms | -6.0% 🟢 |
| ↳ linter/@azure-tools/typespec-azure-core/no-response-body | 🔴 24.5ms | 🔴 22.3ms | -9.1% 🟢 |
| ↳ linter/@azure-tools/typespec-azure-core/no-rest-library-interfaces | 🟢 0.0ms | 🟢 0.0ms | +8.9% |
| ↳ linter/@azure-tools/typespec-azure-core/no-route-parameter-name-mismatch | 🟢 4.8ms | 🟢 4.6ms | -3.1% |
| ↳ linter/@azure-tools/typespec-azure-core/no-rpc-path-params | 🟢 0.2ms | 🟢 0.2ms | -1.7% |
| ↳ linter/@azure-tools/typespec-azure-core/no-string-discriminator | 🟢 0.0ms | 🟢 0.0ms | +12.4% |
| ↳ linter/@azure-tools/typespec-azure-core/no-unknown | 🟢 0.2ms | 🟢 0.2ms | -12.5% |
| ↳ linter/@azure-tools/typespec-azure-core/no-unnamed-union | 🟢 0.4ms | 🟢 0.3ms | -7.3% |
| ↳ linter/@azure-tools/typespec-azure-core/operation-missing-api-version | 🟢 0.2ms | 🟢 0.2ms | -5.5% |
| ↳ linter/@azure-tools/typespec-azure-core/request-body-problem | 🟢 0.3ms | 🟢 0.3ms | +4.0% |
| ↳ linter/@azure-tools/typespec-azure-core/require-versioned | 🟢 0.0ms | 🟢 0.0ms | +10.7% |
| ↳ linter/@azure-tools/typespec-azure-core/response-schema-problem | 🔴 22.5ms | 🔴 21.5ms | -4.2% |
| ↳ linter/@azure-tools/typespec-azure-core/rpc-operation-request-body | 🟢 0.3ms | 🟢 0.3ms | -9.4% |
| ↳ linter/@azure-tools/typespec-azure-core/spread-discriminated-model | 🟢 0.3ms | 🟢 0.3ms | -1.4% |
| ↳ linter/@azure-tools/typespec-azure-core/use-standard-names | 🟢 5.9ms | 🟢 5.1ms | -14.7% |
| ↳ linter/@azure-tools/typespec-azure-core/use-standard-operations | 🟢 0.1ms | 🟢 0.1ms | -4.0% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-agent-base-type-child-resources | 🟢 3.9ms | 🟢 3.7ms | -4.1% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-agent-base-type-lifecycle-operations | 🟢 0.0ms | 🟢 0.0ms | +22.2% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-common-types-version | 🟢 3.7ms | 🟢 3.6ms | -3.4% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-custom-resource-no-key | 🟢 0.1ms | 🟢 0.1ms | +1.6% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-custom-resource-usage-discourage | 🟢 0.1ms | 🟢 0.1ms | -11.6% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-delete-operation-response-codes | 🟢 1.0ms | 🟢 1.0ms | -0.0% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-no-path-casing-conflicts | 🟢 4.2ms | 🟢 4.1ms | -2.0% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-no-record | 🟢 0.4ms | 🟢 0.3ms | -7.5% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-post-operation-response-codes | 🟢 0.4ms | 🟢 0.5ms | +2.9% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-put-operation-response-codes | 🟢 0.0ms | 🟢 0.0ms | +7.8% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-resource-action-no-segment | 🟢 0.2ms | 🟢 0.2ms | -11.3% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-resource-duplicate-property | 🟢 0.1ms | 🟢 0.1ms | +0.4% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-resource-interface-requires-decorator | 🟢 0.0ms | 🟢 0.0ms | +4.4% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-resource-invalid-action-verb | 🟢 0.1ms | 🟢 0.1ms | -9.3% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-resource-invalid-envelope-property | 🟢 0.1ms | 🟢 0.1ms | +4.7% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-resource-invalid-version-format | 🟢 0.0ms | 🟢 0.0ms | +5.8% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-resource-key-invalid-chars | 🟢 0.2ms | 🟢 0.2ms | +5.2% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-resource-name-pattern | 🟢 0.0ms | 🟢 0.0ms | +11.5% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-resource-operation | 🟢 0.2ms | 🟢 0.2ms | -11.2% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-resource-operation-response | 🟢 4.4ms | 🟢 4.2ms | -2.9% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-resource-patch | 🟢 0.3ms | 🟢 0.3ms | -5.2% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-resource-path-segment-invalid-chars | 🟢 0.2ms | 🟢 0.2ms | -3.9% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-resource-provisioning-state | 🟢 0.1ms | 🟢 0.1ms | -0.1% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/beyond-nesting-levels | 🟢 0.1ms | 🟢 0.1ms | -17.7% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/empty-updateable-properties | 🟢 0.1ms | 🟢 0.1ms | +11.9% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/improper-subscription-list-operation | 🟢 0.0ms | 🟢 0.0ms | +17.4% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/lro-location-header | 🟡 13.0ms | 🟡 12.5ms | -3.3% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/missing-operations-endpoint | 🟢 0.0ms | 🟢 0.0ms | +0.9% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/missing-x-ms-identifiers | 🟢 0.3ms | 🟢 0.3ms | -9.5% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/no-empty-model | 🟢 0.1ms | 🟢 0.1ms | +0.8% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/no-override-props | 🟢 0.1ms | 🟢 0.1ms | -5.4% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/no-resource-delete-operation | 🟢 0.2ms | 🟢 0.2ms | -2.4% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/no-response-body | 🔴 20.0ms | 🟡 19.9ms | -1.0% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/patch-envelope | 🟢 0.1ms | 🟢 0.1ms | -1.2% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/resource-name | 🟢 0.1ms | 🟢 0.1ms | +3.5% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/secret-prop | 🟢 2.2ms | 🟢 2.5ms | +15.6% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/unsupported-type | 🟢 0.4ms | 🟢 0.4ms | +0.1% |
| ↳ linter/@azure-tools/typespec-azure-resource-manager/version-progression | 🟢 0.0ms | 🟢 0.0ms | +8.0% |
| ↳ linter/@azure-tools/typespec-client-generator-core/property-name-conflict | 🟢 1.1ms | 🟢 1.0ms | -2.8% |
| ↳ linter/@azure-tools/typespec-client-generator-core/require-client-suffix | 🟢 0.2ms | 🟢 0.2ms | +11.1% |
| emit | 🔴 5.62s | 🔴 5.74s | +2.2% |
| ↳ emit/@azure-tools/typespec-autorest | 🟢 158.1ms | 🟢 159.8ms | +1.0% |
| ↳ emit/@azure-tools/typespec-python | 🔴 4.12s | 🔴 4.16s | +1.1% |
| ↳ emit/@typespec/http-client-js | 🔴 1.12s | 🔴 1.15s | +3.0% |
| ↳ emit/@typespec/openapi3 | 🟢 143.3ms | 🟢 149.0ms | +4.0% |
| ↳ emit/@typespec/openapi3/compute | 🟢 124.7ms | 🟢 126.3ms | +1.3% |
| ↳ emit/@typespec/openapi3/write | 🟢 17.8ms | 🟢 20.6ms | +16.1% 🔴 |
Averaged across 3 specs (azure-arm-resource-manager, azure-core-dataplane, azure-full).
Threshold: changes > ±5% are highlighted.
🟢 Fast · 🟡 Moderate (stages >200ms, rules >10ms) · 🔴 Slow (stages >400ms, rules >20ms)
commit: |
|
You can try these changes here
|
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Fix 13 spelling typos carried over from upstream source (acceptible,
credendial, defef, deferenced, depracated, Disciminator, encounted,
enveloipe, furture, hepler, Iterface, specifiy, uhandled).
- Rename formatValue's defef parameter to deref (callers pass positionally).
- Rename trackDisciminator function to trackDiscriminator.
- Add cspell ignorePaths for packages/typespec-go/{test,temp,.scripts}/**.
- Add legitimate Go SDK identifiers (azfake, azidentity, GOPATH, Errorf,
Fatalf, polymorphics, Retriable, Unmarshaller, etc.) and Go stdlib
package names (cmplx, fcgi, flate, gofmt, pkix, etc.) to dictionary.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Ran pnpm sync-labels to regenerate triage policy files and CONTRIBUTING.md labels reference with the new emitter:go area label. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The filtered install (--filter @azure-tools/typespec-go...) doesn't pull in core's workspace-root devDependencies (prettier-plugin-organize-imports), which @typespec/xml's gen-extern-signature -> tspd -> prettier requires at build time. Match the build/test jobs and install everything. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- nooptionalbody/testdata/generate/transforms.go: hand-authored helper invoked by after_generate.go's //go:generate directive. Without it, the emit's post-step go generate fails, cleanGeneratedFiles wipes all zz_*.go except zz_version.go, and the orphan triggers a downstream golangci-lint 'unused const' error. - rawjson/go.mod and go.sum: rawjson uses containing-module=rawjson/v2 and emits into subpkg/. The module root (one level up) must exist for the emit's post-step go mod tidy to find go.mod. Also: surface emit failures in tspcompile.js by setting process.exitCode = 1. Previously a failed emit only logged 'exec error' and let the script exit 0, hiding the failure until a downstream lint job complained. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
verify-labels CI runs pnpm sync-labels --github --check which requires the banner emitted by core/eng/common/scripts/labels/definitions.ts:295 to match exactly. PR #4563 had hand-patched the banner to a different path that the script never reproduces, so any PR touching CONTRIBUTING.md or eng/config/labels.ts fails the check. Re-running pnpm sync-labels produces this content; committing verbatim. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…seline
After pnpm regenerate finishes, the locally regenerated test artifacts under packages/typespec-go/test/{local,http-specs,azure-http-specs}/ are automatically mirrored into a sparse checkout of Azure/azure-sdk-assets@typespec-go in packages/typespec-go/temp/baseline/. Developers can then inspect codegen deltas with a plain git -C temp/baseline status / git diff — no separate command needed.
- New .scripts/sync-baseline.js: idempotent shallow+blobless sparse clone (test/local, test/http-specs, test/azure-http-specs); mirror copies the local tree into the checkout after regen.
- tspcompile.js: kicks off the sync at startup and runs the mirror in beforeExit so it fires after every emit's async callback has drained. Skipped on regen failure (process.exitCode != 0) so partial output doesn't pollute the baseline.
- ci-go.yml: TYPESPEC_GO_SKIP_BASELINE=1 on the Regenerate step so CI doesn't need network access to azure-sdk-assets.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
doesn't need to be part of this PR if the emitter wasn't built to handle it nicely but did you try to add it to the playground as well?
There was a problem hiding this comment.
do you mean let playground could review go emitted code? that's not the goal for this pr. we just want to do the migration first before deprecate autorest.go.
There was a problem hiding this comment.
Do I need to be added as a code-owner for this directory, or how does that work?
There was a problem hiding this comment.
The script helps to sparse checkout the spec from the configured commit id and spec folder. And the regen script use it instead of what we maintain locally in the previous autorest.go.
There was a problem hiding this comment.
To clarify, my question was about the packages/typespec-go directory and not the file (sorry for the confusion).
There was a problem hiding this comment.
Currently, we do not have package level ownership for typespec-azure repo. Anyone have write access could approve the PR. @timotheeguerin Can we add that? Also, it seems the approval will not be steal if new commit is added after an approval.
|
Are there plans to get more granular WRT what runs in CI? At present it looks like quite a few things ran that's not germane to this emitter. |
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
The dependency was removed from package.json but the side-effect import was left behind, breaking the build with TS2882. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Are there plans to improve this (probably out of scope for this PR) so that emitter PRs don't have to be gated by unrelated tests? |
@timotheeguerin Do you think we could skip some of the CIs that not related to emitters? |
Port the autorest.go development doc to this package, updated for the typespec-azure monorepo layout, scripts, baseline-diff workflow, and Chronus changelog flow. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…specs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
go-test.js discovered any directory containing _test.go and ran go test there. Directories holding hand-written tests for specs disabled in tspcompile.js (addlpropsgroup, clientopgroup, multiclientgroup, renamedopgroup, twoopgroup) have no generated code or go.mod after a fresh regenerate, so go test failed with 'directory ... does not contain main module'. Discover Go modules (dirs with go.mod) that contain test files instead, matching lint-go.js, so disabled specs are skipped until they are re-enabled and regenerated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- lint-go.js now discovers go modules across test/local, test/http-specs and test/azure-http-specs (was test/local only), matching go-test.js. - go-test.js: drop the hasTestFiles filter; go test ./... on a module with no test files simply passes, so the filter added no value. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Port of Azure/autorest.go#1997 (commit 11f88629). The adapted type contains the proper name and correctly handles internal types, fixing adapting Access.internal operations that return a polymorphic type. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Migrates
@azure-tools/typespec-gofromAzure/autorest.gointo this monorepo. See Azure/autorest.go#1982 for full background and plan.What's in this PR
Source code (Phase 3 of the migration plan)
packages/typespec-go/containing the emitter and its three previously-private workspace dependencies inlined:codegen.go→src/codegen/codemodel.go→src/codemodel/naming.go→src/naming/README.md,LICENSE,CHANGELOG.md,vitest.config.ts,tsconfig.json(mirrorstypespec-python)..chronuschange entry.Test specs
test/tsp/(ClientOption, Internal.Pager, ModelsOnlyWithBaseTypes, NoOptionalBody, PageableLROs, Random.Management, RawJson, Regressions, Test.Management).Azure/azure-rest-api-specsat a pinned commit via a sparse, blobless, shallow clone:.scripts/azure-rest-api-specs.json— pinned commit + sparse paths.scripts/sync-azure-rest-api-specs.js— idempotent sync (skipped when cache marker matches).scripts/tspconfig.yaml— stub config that suppresses upstream tspconfig bleed (the per-language@azure-tools/typespec-gooptions that targetazure-sdk-for-go)ExactNametest removed — duplicated by an azure-http-specs scenario.Build / regenerate
.scripts/tspcompile.js— Always invoked with--config=.scripts/tspconfig.yamlso upstream tspconfigs don't bleed..scripts/semaphore.js— 8-way parallel compile primitive..scripts/spector.js— reused start/stop wrapper aroundnpx tsp-spector.CI
.github/workflows/ci-go.yml— jobs:build,test(Go matrix1.26.1/1.25.8with regenerate +lint:go+ spector-backedtest:go:e2e),unit(vitest),docs(regen-docs)..github/actions/setup-go/action.yml— composite wrappingactions/setup-go@v5.eng/config/labels.ts— addsemitter:goarea label + path mapping.eng/pipelines/jobs/build-for-publish.yml— adds Go regen + e2e +upload-spector-coverage.ymltemplate call (GeneratorName=@azure-tools/typespec-go) after the Python block.tsconfig.ws.jsonincludes the new package..gitignoreignores generatedzz_*.go/go.mod/go.sum/LICENSE.txt/testdataunderpackages/typespec-go/test/.Skipped per discussion
mise.toml— Go is installed viaactions/setup-goin GH Actions and viaGoTool@0in the ADO release pipeline.test/local/stays in this repo — not relocated toazure-sdk-for-go.Validation (local)
pnpm build✅pnpm test(vitest unit) ✅ 5/5pnpm regenerate✅ 115/115 tsp compilespnpm format:check✅ clean for allpackages/typespec-go/**filespnpm lint:go✅0 issues(golangci-lint v2.11.4 + shadow, every generated module)pnpm test:go:e2e✅ spector start → 3 hand-written test modules (azblob / azkeys / azregressions) PASS → spector stopFollow-ups (out of scope; tracked in autorest.go#1982)
codegen.go/codemodel.go/naming.goinautorest.goonce Phase 4 lands.autorest.go(after Phase 4).