Skip to content

Conversation

@joscha
Copy link
Contributor

@joscha joscha commented Oct 18, 2024

This pull request adds type checking to generated samples via .github/workflows/samples-typescript-typecheck.yaml.

There are various issues in the current generated samples, from simple missing overrides, over incorrect types up to missing code pieces like classes.

I will leave explanations on each fix needed via code comments on the change.

PR checklist

  • Read the contribution guidelines.
  • Pull Request title clearly describes the work in the pull request and Pull Request description provides details about how to validate the work. Missing information here may result in delayed response from the community.
  • Run the following to build the project and update samples:
    ./mvnw clean package 
    ./bin/generate-samples.sh ./bin/configs/*.yaml
    ./bin/utils/export_docs_generators.sh
    
    (For Windows users, please run the script in Git BASH)
    Commit all changed files.
    This is important, as CI jobs will verify all generator outputs of your HEAD commit as it would merge with master.
    These must match the expectations made by your contribution.
    You may regenerate an individual generator by passing the relevant config(s) as an argument to the script, for example ./bin/generate-samples.sh bin/configs/java*.
    IMPORTANT: Do NOT purge/delete any folders/files (e.g. tests) when regenerating the samples as manually written tests may be removed.
  • File the PR against the correct branch: master (upcoming 7.x.0 minor release - breaking changes with fallbacks), 8.0.x (breaking changes without fallbacks)
  • If your PR is targeting a particular programming language, @mention the technical committee members: @TiFu (2017/07) @taxpon (2017/07) @sebastianhaas (2017/07) @kenisteward (2017/07) @Vrolijkx (2017/09) @macjohnny (2018/01) @topce (2018/10) @akehir (2019/07) @petejohansonxo (2019/11) @amakhrov (2020/02) @davidgamero (2022/03) @mkusaka (2022/04) @joscha (2024/10)

@@ -0,0 +1,25 @@
name: TypeScript clients type checks
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This workflow runs the type check shell file

root_dir=$(git rev-parse --show-toplevel)
local dir

for dir in $(git ls-files samples | grep 'tsconfig.json$' | xargs -n1 dirname | sort -u); do
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We find all checked in samples that have a tsconfig.json

log "${dir}"
pushd "${root_dir}/${dir}" > /dev/null
npm_install \
|| npm_install --force # --force because we have some incompatible peer-dependencies that can't be fixed
Copy link
Contributor Author

Choose a reason for hiding this comment

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

you can see in the CI logs, but basically there are a couple really really outdated dependencies, which can't even be updated anymore, like https://github.com/angular/tsickle/ for example. Hence we need to sometimes override incompatible peer dependencies, etc.

pushd "${root_dir}/${dir}" > /dev/null
npm_install \
|| npm_install --force # --force because we have some incompatible peer-dependencies that can't be fixed
npm exec [email protected] --yes -- tsc --noEmit
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this does the actual type checking and fails the job if there are errors

typeMapping.put("Array", "Array");
typeMapping.put("array", "Array");
typeMapping.put("boolean", "boolean");
typeMapping.put("decimal", "string");
Copy link
Contributor Author

Choose a reason for hiding this comment

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

without this change,

for example would exopect a full-blown Decimal class, which is never generated. You can see code references to it that are not resolved as the file is not there. For example:

protected ensureParamIsSet<T>(context: string, params: T, paramName: keyof T): void {
if (null === params[paramName]) {
throw new Error(`Missing required parameter ${paramName} when calling ${context}`);
throw new Error(`Missing required parameter ${String(paramName)} when calling ${context}`);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixes:

Api.ts:73:53 - error TS2731: Implicit conversion of a 'symbol' to a 'string' will fail at runtime. Consider wrapping this expression in 'String(...)'.

73       throw new Error(`Missing required parameter ${paramName} when calling ${context}`);
                                                       ~~~~~~~~~


Found 1 error in Api.ts:73

return `${encodeURIComponent(fullKey)}=${encodeURIComponent(String(value))}`;
}

export function exists(json: any, key: string) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixes:

src/models/GetPetRegionsResponse.ts:15:10 - error TS2305: Module '"../runtime"' has no exported member 'exists'.

15 import { exists, mapValues } from '../runtime';
            ~~~~~~

return knownIndexedSetByKey.indexOf(key) !== -1 ? value.toSet() : value.toList();
} // we're reviving an array -> it's a List
const MatchingType = knownRecordFactories.get(value.get('recType')) as { new(input?: any): any }; // check if we know a Record with this type
const MatchingType = knownRecordFactories.get(value.get('recType') as string) as { new(input?: any): any }; // check if we know a Record with this type
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixes

src/runtimeSagasAndRecords.ts:16:55 - error TS2345: Argument of type 'unknown' is not assignable to parameter of type 'string'.

16         const MatchingType = knownRecordFactories.get(value.get('recType')) as { new(input?: any): any }; // check if we know a Record with this type
                                                         ~~~~~~~~~~~~~~~~~~~~

return undefined;
{{/returnType}}
} catch (error) {
} catch (error: any) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

no implicit any

}

export function *{{nickname}}SagaImp(_action_: Action<Payload{{#lambda.titlecase}}{{#lambda.camelcase}}{{nickname}}{{/lambda.camelcase}}{{/lambda.titlecase}}>) {
export function *{{nickname}}SagaImp(_action_: Action<Payload{{#lambda.titlecase}}{{#lambda.camelcase}}{{nickname}}{{/lambda.camelcase}}{{/lambda.titlecase}}>){{^returnType}}: any{{/returnType}} {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

no implicit any

@@ -1,4 +1,9 @@
{{#useAxiosHttpModule}}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@nestjs/axios is only in the package.json when useAxiosHttpModule is truthy. If not, the package is not a dependency, which then fails in the type checks, as it can't be resolved. Other code uses the HttpService from nestjs/common in that case, so we do that here as well now, too.

"@nestjs/testing": "~{{nestVersion}}",
"@types/express": "^4.16.0",
"@types/jest": "^24.0.15",
"@types/node": "^14.8.2",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

These types are so old that some definitions clash with the current typescript version.

}

const parameters = this.headers[headerName].split(";");
const parameters = this.headers[headerName]!.split(";");
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this is guarded further up, but some of the older TS versions we have can't determine this, yet.

const parameters = this.headers[headerName]!.split(";");
for (const parameter of parameters) {
let [key, value] = parameter.split("=", 2);
if (!key) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

undefined guard

*
* This helper transforms a string mime-type into an internal representation.
* This simplifies the implementation of predicates that in turn define common rules for parsing or stringifying
* the payload.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

undefined guards here too

*/
{{/description}}
'{{name}}'{{^required}}?{{/required}}: {{#isEnum}}{{{datatypeWithEnum}}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{#isNullable}} | null{{/isNullable}};
{{/vars}}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

noImplicitOverride fixes

class InjectableConfiguration implements AbstractConfiguration {
public httpApi: HttpLibrary = new DefaultHttpLibrary();
public middleware: Middleware[] = [];
public authMethods: AuthMethods = {};
Copy link
Contributor Author

Choose a reason for hiding this comment

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

services/index.ts:26:9 - error TS2322: Type 'ServerConfiguration<{}> | undefined' is not assignable to type 'AbstractServerConfiguration'.
  Type 'undefined' is not assignable to type 'AbstractServerConfiguration'.

26         @inject(AbstractServerConfiguration) @optional() public baseServer: AbstractServerConfiguration = servers[0],
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

services/index.ts:68:54 - error TS2345: Argument of type 'ServerConfiguration<{}> | undefined' is not assignable to parameter of type 'AbstractServerConfiguration'.
  Type 'undefined' is not assignable to type 'AbstractServerConfiguration'.

68         this.bindServerConfiguration.toConstantValue(servers[idx]);
                                                        ~~~~~~~~~~~~

{{/useRxJS}}
{{#useInversify}}
"inversify": "^5.0.1",
"inversify": "^6.0.1",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Inversify 5.x doesn't work properly with our current TS version. It's this issue here: https://stackoverflow.com/questions/75776252/inversify-typescript-5-ts1239-unable-to-resolve-signature-of-parameter-decor

runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
Copy link
Contributor Author

Choose a reason for hiding this comment

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

if we were to generate this file, we could used the samples axis to produce one for each project with tsconfig. This would run the checks in parallel and make failures easier to understand. It would be the first time we do a dynamic build step though. Let me know what you prefer. We can also improve this later.

export type HttpBasicConfiguration = { "username": string, "password": string };
export type HttpBearerConfiguration = { tokenProvider: TokenProvider };
export type OAuth2Configuration = { accessToken: string };
export type HttpSignatureConfiguration = unknown; // TODO: Implement
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am not actually sure what this has to look like. Currently it's not supported as we don't have it implemented. This fix is to produce valid syntactic code, the implementation is probably a bit more involved.
You can see the fix it produces below in samples/openapi3/client/petstore/typescript/builds/explode-query/auth/auth.ts.
And how it is currently broken (syntactically invalid TS), here:

"bearer_test"?: HttpBearerConfiguration,
"http_signature_test"?:
}

* return value;
*/
public bindServerConfigurationToPredefined(idx: number) {
if (!servers[idx]) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

servers[idx] can be undefined

{{/node}}
{{#browser}}
"lib": [ "es6", "dom" ],
,"dom"
Copy link
Contributor Author

Choose a reason for hiding this comment

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

unified this, browser only gets "dom" in addition to the rest.

Comment on lines +20 to +22
"noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
"noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
"noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
Copy link
Contributor Author

Choose a reason for hiding this comment

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

added these three

{{#platforms}}
"lib": [
"es6"
,"ES2017.Object"
Copy link
Contributor Author

Choose a reason for hiding this comment

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

brings Object.entries

"lib": [
"es6"
,"ES2017.Object"
,"ES2021.String"
Copy link
Contributor Author

Choose a reason for hiding this comment

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

String.replaceAll

Copy link
Member

@macjohnny macjohnny left a comment

Choose a reason for hiding this comment

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

thank you so much for this contribution, this is awesome!
it allows to ensure the generated samples compile, even without writing any unit tests!

@macjohnny macjohnny merged commit dc3718c into OpenAPITools:master Oct 18, 2024
25 checks passed
@joscha joscha deleted the joscha/typecheck-plus-fixes branch October 18, 2024 07:57
@joscha
Copy link
Contributor Author

joscha commented Oct 18, 2024

One thing to mention is, that currently none of the samples has a package-lock.json - this means that it is possible for the CI step to break due to incompatible dependency closures, if there were breaking changes in the patch and minor semver space.
I think the risk is quite small, but it could be worth adding lock files to the samples if it becomes a problem. Then we could also install with npm ci instead.

on:
pull_request:
paths:
- samples/**
Copy link
Member

@wing328 wing328 Oct 31, 2024

Choose a reason for hiding this comment

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

@joscha can we run this only if there's a change in the typescript-related samples?

#20000 (a recent PR) shows the workflow failed: https://github.com/OpenAPITools/openapi-generator/actions/runs/11606840555/job/32319917402?pr=20000

can you please take a look?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I can reduce it a bit more. In order to be super precise we'd need the full dependency graph, that is tricky as it includes any package.lock, node versions, etc. - it's not impossible but the repository is not set up for it currently, we'd want to also get the node dependency from nix for example. I can probably improve it a bit more, however I won't have access to a computer for a few more days. If the tests fail regularly with false positives, feel free to comment out the yaml and I'll fix and reenable when I get access to a computer.

@wing328 wing328 added this to the 7.10.0 milestone Nov 20, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants