Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

feat(eslint-plugin): [thenable-in-promise-aggregators] disallow non-Thenables to promise aggregators#8094

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Closed

Conversation

Tjstretchalot
Copy link

@TjstretchalotTjstretchalot commentedDec 18, 2023
edited
Loading

PR Checklist

Overview

Adds a new rule,thenable-in-promise-aggregators, which verifies that array arguments ofPromise.all,Promise.allSettled, andPromise.race are all Thenable. The purpose behind this PR is discussed in the issue; for me, I often wrap promises to make them cancelable, but it still makes sense to name the object e.g.fooPromise, and when mixed with regular promises (or even without), I will typePromise.race([stdPromise, fooPromise]), which will look correct and appear to behave correctly for a long time, before getting non-responsive if one of the promises is unexpectedly slow.

@typescript-eslint
Copy link
Contributor

Thanks for the PR,@Tjstretchalot!

typescript-eslint is a 100% community driven project, and we are incredibly grateful that you are contributing to that community.

The core maintainers work on this in their personal time, so please understand that it may not be possible for them to review your work immediately.

Thanks again!


🙏Please, if you or your company is finding typescript-eslint valuable, help us sustain the project by sponsoring it transparently onhttps://opencollective.com/typescript-eslint.

@netlifyNetlify
Copy link

netlifybot commentedDec 18, 2023
edited
Loading

Deploy Preview fortypescript-eslint ready!

NameLink
🔨 Latest commitea65212
🔍 Latest deploy loghttps://app.netlify.com/sites/typescript-eslint/deploys/65bd1424933eaf0007ea4345
😎 Deploy Previewhttps://deploy-preview-8094--typescript-eslint.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
1 paths audited
Performance: 99 (🟢 up 9 from production)
Accessibility: 100 (no change from production)
Best Practices: 92 (no change from production)
SEO: 98 (no change from production)
PWA: 80 (no change from production)
View the detailed breakdown and full score reports

To edit notification comments on pull requests, go to yourNetlify site configuration.

Copy link
Member

@auvredauvred left a comment

Choose a reason for hiding this comment

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

Nice start! Thanks for working on it!

Just requesting some changes:

@Tjstretchalot
Copy link
Author

Tjstretchalot commentedDec 20, 2023
edited
Loading

I believe I covered all the comments. I left conversations open when there may be ambiguity, but since it's easy to miss (I almost missed 5 due to the hidden comments), I'll summarize still pending thoughts here that I'm seeking advice on the best way to resolve:

{code:`class Foo extends Promise<number> {}await Foo.all([0]);      `,errors:[{line:3,messageId:'inArray',},],}
  • I use two different styles for valid/invalid cases because I usedawait-thenable as a reference and it does that. Can/should I do something differently (now or if I make future PRs)? RESOLVED -> using shortened version

Comment on lines +178 to +188
const elementType = services.getTypeAtLocation(element);
if (isTypeAnyType(elementType) || isTypeUnknownType(elementType)) {
continue;
}

const originalNode = services.esTreeNodeToTSNodeMap.get(element);
if (tsutils.isThenableType(checker, originalNode, elementType)) {
continue;
}
Copy link
Member

Choose a reason for hiding this comment

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

this logic (ifisTypeAnyType(sometype) || isTypeUnknownType(sometype) ortsutils.isThenableType(checker, originalNode, sometype)) is used three times in this rule. Maybe we should move it to the function to avoid duplicated code?

Choose a reason for hiding this comment

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

Hmm; I refactored around a bit, though I still have 3 calls like this. However, I don't think something like this would add anything besides some mental overhead:

functionisTypeAnyOrUnknownType(type:ts.Type):boolean{returnisTypeAnyType(type)||isTypeUnknownType(type);}

For this to be useful from my current perspective there would need to be a more salient name for the method that is a more useful concept thanchecking for any or unknown and makes the implementation obvious (since it's so short). I looked elsewhere around the codebase and any types are not universally treated the same as unknown types, e.g., I could imagine a flag on this check later, perhaps two flags (one for any and one for unknown), and depending on exactly how those flags work it might make sense to break it out then, but more likely even if you made those flags it would still not be helpful to have a function like this given you would want different errors each which explanations of the flag to use to disable that feature, ect.

Copy link
Member

@Josh-CenaJosh-Cena left a comment

Choose a reason for hiding this comment

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

Was the decision to create a new rule discussed anywhere? I thought it would be a new option forawait-thenable, likepromiseAggregators: "all" | "any" | "ignore".

kirkwaiblinger reacted with thumbs up emoji
@Tjstretchalot
Copy link
Author

Happy new year!

Rebased onto the current state of the main branch

@Josh-Cena There was no discussion AFAIK, I initially made this as a custom eslint rule for myself and so it was easier to translate it over as its own ESLint rule. I'm impartial to including it directly in await-thenable. The only main question I have is how to handle the defaults for the flags?

Right now it's in what I think is the ideal case: included when enablingstrict-typed, not included inrecommended, and it can be included inrecommended on the next major version.

If we add it as configuration forawait-thenable, since that is inrecommended, the flags presumably need to default to off. But that would mean it's not included instrict-typed and doesn't have an obvious upgrade path torecommended. I suppose it might be possible to have the flags default change depending on whether you havestrict-typed included or not, but that seems... weird?

I imagine this came up any other time typescript-eslint wanted to add flags, so I just need to be pointed in the right direction for how to handle them. It would be a little annoying for my use-case, though certainly not the end of the world, if I had to remember to specifically enable these flags on every project to make use of the rule, especially since I would likely only notice it's disabled after the issue it attempts to resolve bit me!

@Josh-Cena
Copy link
Member

Yeah, good point about different options. I think this fits in#7318.

meta: {
docs: {
description:
'Disallow passing non-Thenable values to promise aggregators',

Choose a reason for hiding this comment

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

[Docs] Interesting, we don't have a standard for whether we refer to Promises with an upper-case P or lower-case p... Outside the scope of this PR, maybe it'd be a good followup to file an issue about standardizing this in docs?

Not a blocker or request for changes 😄 just ruminating.

Tjstretchalot reacted with thumbs up emoji
Comment on lines 102 to 109
if (
callee.property.type !== AST_NODE_TYPES.Literal ||
typeof callee.property.value !== 'string' ||
!aggregateFunctionNames.includes(callee.property.value)
) {
return;
}
} else if (!aggregateFunctionNames.includes(callee.property.name)) {

Choose a reason for hiding this comment

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

[Refactor] This"get the name one way if it's computed or another if it's static" need comes up a lot in rules. We should have a shared utility for it.getStaticStringValue should do the trick, orgetStaticValue. One of those 😄.

Choose a reason for hiding this comment

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

There is already a functiongetStaticStringValue ineslint-plugin/src/util but it's about finding what the source representation of a value would be, e.g., turning a regex into/this/ or null into the stringnull, etc.

To avoid confusion with that I've broken out agetMemberName function you can look at and see if it might be worth breaking out into a shared utility

Comment on lines 133 to 131
`
async function test() {
const intersectionArr: (Promise<number> & number)[];
await Promise.all(intersectionArr);
}
`,

Choose a reason for hiding this comment

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

Yeah I think unions should be allowed. It's a nice convenience and intentionally part of the Promise aggregator design to allow this.

Copy link
Member

@JoshuaKGoldbergJoshuaKGoldberg left a comment

Choose a reason for hiding this comment

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

This is looking great, thanks for working so much on this rule! I think@auvred reviewing earlier has made my job a lot easier 😄 thanks for that too!

Ithink I responded to all the pending conversations but I'm not sure. There was a lot to go through 😅. If there's anything I missed, please do start a new comment thread in a file and I'll take a look.

Requesting changes on some more testing and on reusing a shared utility for a static key value. The other points aren't blocking IMO as we can file them as followup issues.

Blue cartoonish character with a gray triangle hat dancing awkwardly on top of a yellow blanket saying "you're doing great!" on top of what looks like a potato with a smiley face.

requiresTypeChecking: true,
},
messages: {
inArray:

Choose a reason for hiding this comment

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

[Question] Is there a use case for adding an option to allow users to provide an array/tuple where only some of the elements are thenables? LikePromise.all([callAsyncThing(), "other", callAsyncThing()])?

I can't think of one 🤔 so maybe this is a non-actionable comment?

Btw, sorry if I missed this in previous discussion - I looked through and couldn't find anything.

Copy link
Author

@TjstretchalotTjstretchalotJan 12, 2024
edited
Loading

Choose a reason for hiding this comment

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

This is the main case I'm specifically looking to lint, and is the most common case where mistakes are made. There is no advantage to doing this from the perspective of control flow (i.e., how long the returned promise takes to fulfill), however, it does change the return value. However, whenever you really do want to provide a non-Thenable to change the control flow, you can always wrap it inPromise.resolve to get exactly the same behavior while making it way more clear how the aggregator will behave. This is still true even when considering if the returned promise is fulfilled synchronously vs asynchronously: these promise aggregators only fulfill synchronously when passed an empty array.

I would argue especially for all of these promise aggregators, the most important thing is making the control flow clear. Any non-Thenable value passed to a promise aggregator will be treated asPromise.resolve(value) according to the spec; see e.g.,PerformPromiseAll. Thus given that a value in the list is known to be non-Thenable, there is a static conversion available that will make the control flow easier to understand, or you can be explicit and wrap the value in Promise.resolve, which jumps out at the reader and thus also makes the control flow easier to understand.

Given:

declareconstaPromise:Promise<number>;declareconstbPromise:Promise<number;declareconstcNumber:number;
// allPromise.all([aPromise,bPromise,cNumber])// equivalent toPromise.all([aPromise,bPromise,Promise.resolve(cNumber)])// which resolves in the same amount of time asPromise.all([aPromise,bPromise])// previously working, now causes lint errorconst[a,b,c]=awaitPromise.all([aPromise,bPromise,cNumber]);// suggested changeconst[a,b]=awaitPromise.all([aPromise,bPromise]);constc=cNumber;
// allSettledPromise.allSettled([aPromise,bPromise,cNumber]);// equivalent toPromise.allSettled([aPromise,bPromise,Promise.resolve(cNumber)]);// resolves in the same time asPromise.allSettled([aPromise,bPromise]);// previously working, now causes lint errorconst[aPromise2,bPromise2,cPromise]=awaitPromise.allSettled([aPromise,bPromise,cNumber]);// suggested changeconst[aPromise2,bPromise2]=awaitPromise.allSettled([aPromise,bPromise]);constcPromise=Promise.resolve(cNumber);
// raceawaitPromise.race([aPromise,bPromise,cNumber]);// equivalent toawaitPromise.race([aPromise,bPromise,Promise.resolve(cNumber)]);// always resolves immediately (but never synchronously, as Promise.race is never synchronous)// previously working, now causes lint error// this is a if a is immediately resolved, b if b is immediately resolved, otherwise cconstaOrBOrC=awaitPromise.race([aPromise,bPromise,cNumber]);// suggested change if you really did want the above behavior (most of the time you do NOT)constaOrBOrC=awaitPromise.race([aPromise,bPromise,Promise.resolve(cNumber)]);// suggested change if you actually intended to wait for something to happenconstaOrB=awaitPromise.race([aPromise,bPromise])// note that most of the time this comes up, this is whats actually happening:declareconstaPromise:Promise<number>;declareconstbPromise:Promise<number>;declareconstcWrappedPromise:{promise:Promise<number>};// lint error that catches a real subtle bugconstaOrBOrC=awaitPromise.race([aPromise,bPromise,cWrappedPromise]);// correct codeconstaOrBOrC=awaitPromise.race([aPromise,bPromise,cWrappedPromise.promise]);
// any is the same as race for the above purposes, the difference is how rejected values are handled

Copy link
Member

@JoshuaKGoldbergJoshuaKGoldbergJan 24, 2024
edited
Loading

Choose a reason for hiding this comment

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

[Non-Actionable] I realized a situation where this option would be helpful! [playground link]

declarefunctionfetch(url:string):Promise<object>;functionexample(){returnPromise.all([fetch("/"),"in the middle",fetch("/")]);}

If I was the developer here usingPromise.all directly to avoid having to make & manage a[object, string, object] tuple, I'd be annoyed at the rule asking me to explicitlyPromise.resolve("in the middle"). There's no benefit in this situation beyond consistency - at the cost of simplicity.

But, I get what you're saying that this is really the main point of the rule. I think it's fine for now to leave as-is. Since it's only enabled in the strict config, it should just show up for users who have opted into the more strict opinions.

Tjstretchalot reacted with thumbs up emoji
Copy link
Member

Choose a reason for hiding this comment

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

Please do add an option that allows mixed arrays. I often parallel a bunch of tasks and sometimes tasks shift between being sync and async (for example, because it no longer dynamically imports a library, or it no longer reads a config file). I really don't want to break the symmetry in this case.

JoshuaKGoldberg reacted with thumbs up emoji

Choose a reason for hiding this comment

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

In that case, +1ing Josh's request 🙂

@JoshuaKGoldbergJoshuaKGoldberg added the awaiting responseIssues waiting for a reply from the OP or another party labelJan 11, 2024
@Tjstretchalot
Copy link
Author

(the force-pushes are just rebasing with main to keep it in sync)

@github-actionsgithub-actionsbot removed the awaiting responseIssues waiting for a reply from the OP or another party labelJan 12, 2024
Copy link
Member

@JoshuaKGoldbergJoshuaKGoldberg left a comment

Choose a reason for hiding this comment

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

This is looking great, thanks! 🚀

Just requesting changes on the use cases I commented in threads.

@@ -105,7 +117,7 @@ export function isBuiltinTypeAliasLike(
export function isBuiltinSymbolLike(
program: ts.Program,
type: ts.Type,
symbolName: string,
predicate: (symbolName: string) => boolean,

Choose a reason for hiding this comment

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

[Refactor] Every one of the uses for this function does >=1=== check. How about using aSet instead to make it more clear what's happening?

Suggested change
predicate:(symbolName:string)=>boolean,
symbolNames:Set<string>

Copy link
Member

Choose a reason for hiding this comment

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

By the way, these functions are already included in several public releases. Shouldn't we keep the already published signatures to avoid breaking changes? Perhaps we can add this new functionality as overload/union? Although these packages are not documented yet, not sure, just asking 😄

Choose a reason for hiding this comment

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

Predicates are more composable than sets and offer easier ergonomics; for sets;(v) => symbolNameSet.has(v) is just as readable assymbolNameSet, and for looking up a single value(symbolName) => symbolName === 'asdf' is in my opinion much easier to interpret thannew Set(['foo']) which offers no context for what the set is for, so you end up needing either a comment or breaking it out into a constant. Finally, predicates can be more consistently applied; e.g., we already have predicates forisBuiltinSymbolLikeRecurser, so switching between predicates and sets for arguments requires constantly deciding which ones "tend to be used like sets". Furthermore, predicates portray the intent of the argument more clearly: a set has many functions besides just contains-checking, including iteration. Providing a predicate makes it clear the only part the library function needs is the yes/no answer on each element.

For@auvred 's comment; happy to add the overload/union; adds a little clutter though, so will wait for more discussion since I'm not familiar enough with the project to have any opinion for backwards compatibility vs tidiness for those helpers

Copy link
Member

@JoshuaKGoldbergJoshuaKGoldbergFeb 3, 2024
edited
Loading

Choose a reason for hiding this comment

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

backwards compatibility

Yup, this'll need to retain backwards compatibility. I haven't heard of anybody telling us they use it, buta Sourcegraph query for TypeScript files referring to it shows a few public repos that do.

(v) => symbolNameSet.has(v) is just as readable assymbolNameSet

I think this is subjective 😄 I find the latter more intuitive. In my mind I'm kind of implicitly applying the context from"is built-in symbol like" to the last parameter. But that's just me.

offers no context for what the set is for, so you end up needing either a comment or breaking it out into a constant

This is equally applicable to both a predicate and a set. For both, a descriptive parameter name likeallowedSymbolNames orisAllowedSymbolName fixes the context issue.

predicates portray the intent of the argument more clearly
a set has many functions besides ...

I supposePick<Set<String>, 'has'> would be the most precise thing to take in. All we really want in any of this is to know whether a name is what we're looking for.

One flaw of functions is it leaks the implementation detail of"this gets called up to once per symbol name", so folks can do weird things like:

isBuiltinSymbolLike(program,type,(name)=>{someRegistry.add(name);if(someAttribute(name)){context.report({data:{ name},messageId:"someMessage",node:someNode});}});

Just pointing this out as a counterpoint to the idea that a predicate adds more practical or semantic safety. I don't think we have to worry about anybody doing such an odd thing.


Anyway, the issue of whether we use a set or predicate is not particularly important to me. It's a small implementation detail and if you feel strongly about it I don't mind just shrugging and seeing what happens.

+1 in general to pushing back on my stylistic suggestions 😄 I like this!

requiresTypeChecking: true,
},
messages: {
inArray:
Copy link
Member

@JoshuaKGoldbergJoshuaKGoldbergJan 24, 2024
edited
Loading

Choose a reason for hiding this comment

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

[Non-Actionable] I realized a situation where this option would be helpful! [playground link]

declarefunctionfetch(url:string):Promise<object>;functionexample(){returnPromise.all([fetch("/"),"in the middle",fetch("/")]);}

If I was the developer here usingPromise.all directly to avoid having to make & manage a[object, string, object] tuple, I'd be annoyed at the rule asking me to explicitlyPromise.resolve("in the middle"). There's no benefit in this situation beyond consistency - at the cost of simplicity.

But, I get what you're saying that this is really the main point of the rule. I think it's fine for now to leave as-is. Since it's only enabled in the strict config, it should just show up for users who have opted into the more strict opinions.

Tjstretchalot reacted with thumbs up emoji
node:
| TSESTree.MemberExpressionComputedName
| TSESTree.MemberExpressionNonComputedName,
predicate: (name: string) => boolean,

Choose a reason for hiding this comment

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

[Refactor] Similar to theisBuiltinSymbolLike comment, this is only ever used with aSet<string>. I think expanding the use case to predicates is just a bit more complexity than we need.

Suggested change
predicate:(name:string)=>boolean,
memberNames:Set<string>

@JoshuaKGoldbergJoshuaKGoldberg added the awaiting responseIssues waiting for a reply from the OP or another party labelJan 24, 2024
@github-actionsgithub-actionsbot removed the awaiting responseIssues waiting for a reply from the OP or another party labelJan 24, 2024
@Tjstretchalot
Copy link
Author

For some reason I can't respond to some these individually, at least as far as I can tell, only emote to them. Breaking out here instead

#8094 (comment)

Sure, will add default blocking of unknown with an option to unblock. I agree that wouldn't use unknown without a type guard, but the use-case is logically consistent, as it is a type refinement, though one too subtle to be picked up by current typescript (i.e.,Awaited<unknown> is just unknown now and for the foreseeable future, but there are some things that can be known about it).

However, I do think this is a bit inconsistent sinceawait-thenable allows unknown (first time trying to use the playground, might have made a mistake, but this is definitely clear in the source code)

#8094 (comment)

Responded in#8094 (comment) - if my argument for predicates isn't convincing will switch it over with an unambigious blocking remark

@codecovCodecov
Copy link

codecovbot commentedFeb 2, 2024
edited
Loading

Codecov Report

Attention: Patch coverage is77.77778% with20 lines in your changes are missing coverage. Please review.

Project coverage is 87.68%. Comparing base(69bd501) to head(ea65212).
Report is 169 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@##             main    #8094      +/-   ##==========================================- Coverage   87.72%   87.68%   -0.04%==========================================  Files         397      398       +1       Lines       13971    14057      +86       Branches     4065     4089      +24     ==========================================+ Hits        12256    12326      +70- Misses       1403     1418      +15- Partials      312      313       +1
FlagCoverage Δ
unittest87.68% <77.77%> (-0.04%)⬇️

Flags with carried forward coverage won't be shown.Click here to find out more.

FilesCoverage Δ
packages/type-utils/src/builtinSymbolLikes.ts0.00% <0.00%> (ø)
...lugin/src/rules/thenable-in-promise-aggregators.ts86.41% <86.41%> (ø)

@Tjstretchalot
Copy link
Author

Updated to latest main branch and ensured this doesn't change the library functions in a backwards incompatible way (cc:@auvred#8094 (comment)). Initially I did it via overloads so I could mark the string one deprecated, but that fails linting withThese overloads can be combined into one signature, so this leaves no deprecation path for the string variant.

As#8301 appears to be resolved with wontfix I'll leave unknown behavior as is.

cc:@JoshuaKGoldberg for updating labels, deciding if predicate->set change is blocking

JoshuaKGoldberg reacted with thumbs up emoji

@JoshuaKGoldbergJoshuaKGoldberg removed the blocked by another issueIssues which are not ready because another issue needs to be resolved first labelFeb 2, 2024
@JoshuaKGoldberg
Copy link
Member

Super, thanks! Putting back on the queue to review :)

if (!checker.isArrayType(type) && !checker.isTupleType(type)) {
const indexType = checker.getIndexTypeOfType(type, ts.IndexKind.Number);
if (indexType === undefined) {
return false;

Choose a reason for hiding this comment

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

[Testing] Missing unit test coverage for this and a few other lines. If it's a case where weknow the value is definitely not nullish (e.g. an array type having a numeric index) then a! is reasonable.

Choose a reason for hiding this comment

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

[Non-Actionable] Every time I come back to this PR I get confused as to how such an important-looking file has such major gaps in unit test coverage... it's#6701, but for this package.

Filed#8358 to track adding in unit tests separately.

Copy link
Member

@JoshuaKGoldbergJoshuaKGoldberg left a comment
edited
Loading

Choose a reason for hiding this comment

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

Just a unit test coverage request that's blocking from me right now 🙂 great stuff!

Edit: oh, and the request for an option around mixed arrays.

(I'll re-review more deeply once the comments are all resolved)

@JoshuaKGoldbergJoshuaKGoldberg added the awaiting responseIssues waiting for a reply from the OP or another party labelFeb 3, 2024
@JoshuaKGoldberg
Copy link
Member

👋 Hey@Tjstretchalot! Just checking in, is this still something you have time for? No worries if not - I just don't want to leave it hanging.

@JoshuaKGoldbergJoshuaKGoldberg added the stalePRs or Issues that are at risk of being or have been closed due to inactivity for a prolonged period labelMar 17, 2024
@Tjstretchalot
Copy link
Author

Hey@JoshuaKGoldberg - apologies for this; work got very busy rather quickly and had to reduce some other stuff to make space. It'll hopefully get better in April, but if someone else wants to pick it up or you want to close until then let me know

JoshuaKGoldberg reacted with heart emoji

@JoshuaKGoldberg
Copy link
Member

Thanks, good to know. I'll removestale for now but if by the middle of the month it's still pending I'd say we should free it up then. Good luck with the work!

@JoshuaKGoldbergJoshuaKGoldberg removed the stalePRs or Issues that are at risk of being or have been closed due to inactivity for a prolonged period labelMar 26, 2024
@JoshuaKGoldberg
Copy link
Member

Closing as it's been a bit - thanks for getting started on it! If you do end up having time to continue@Tjstretchalot feel free to reopen this PR if it's unlocked, or send a new one otherwise.

In the meantime, if anybody else wants to work on the linked issue, go right ahead. Just be sure to add a co-author attribution if you use any code from this PR. Cheers!

@github-actionsgithub-actionsbot locked asresolvedand limited conversation to collaboratorsMay 1, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account?Sign in.
Reviewers

@JoshuaKGoldbergJoshuaKGoldbergJoshuaKGoldberg requested changes

@Josh-CenaJosh-CenaAwaiting requested review from Josh-Cena

@auvredauvredAwaiting requested review from auvred

Assignees
No one assigned
Labels
awaiting responseIssues waiting for a reply from the OP or another party
Projects
None yet
Milestone
No milestone
Development

Successfully merging this pull request may close these issues.

4 participants
@Tjstretchalot@Josh-Cena@JoshuaKGoldberg@auvred

[8]ページ先頭

©2009-2025 Movatter.jp