Skip to content
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

Distinguish a TopicName type from plain String #1266

Open
wants to merge 26 commits into
base: main
Choose a base branch
from

Conversation

gnprice
Copy link
Member

@gnprice gnprice commented Jan 8, 2025

Toward #1250.

This PR introduces a new type TopicName as a Dart "extension type":
https://dart.dev/language/extension-types
At runtime it's just a String — so there's no performance cost to it — but the type-checker tracks the distinction.

This lets us readily find all the spots in the code that need to adapt to show "general chat" for #1250: it's exactly the use sites of the new TopicName.displayName. (With one possible loophole involving string interpolation; see the last commit message below.)

This series is nearly NFC; each commit is NFC except the first two prep commits, which are nearly so.

Selected commit messages

api: Explicitly ignore topics on DMs

The topic/subject field on DMs carries no information in the current
API, and we already never actually look at it, as demonstrated by
the lack of changes in the rest of the tree in this commit. Clean
up the type definitions in the API a bit by making that explicit.

This is NFC except in the case where a server were to not send this
field at all for a DM (or to send a non-string value). In that case
the old code would reject the server's response as malformed, and
the new code after this commit would accept it because it doesn't
look for the field.


api [nfc]: Introduce TopicName extension type

We'll use this to make a type-level distinction between a string
that's being used to name a topic, and just any old string.
That will help us in particular with #1250 the "general chat"
feature, where the way we show a topic name to the user stops being
necessarily just the same string that it appears as in the API.

The next phase will be to migrate a bunch of places in our code
to refer to TopicName when that's what they mean, instead of just
String.

During this phase, a TopicName can be freely used as a String, but
not vice versa. So we'll do the migrations mostly in order of the
data flow, from upstream to downstream. That will allow us to do
them over a series of individually coherent commits, with a minimum
of occasions where we temporarily introduce a conversion that won't
be needed in the final result.

That means: first, test data; then topics returned from the API to
our code; then our internal models; then back to the API, this time
for topics passed to the API from our code.

After we have the type in place all over where it belongs, we'll
start making use of the distinction, and then enforcing it.


api [nfc]: Use TopicName type across request parameters

This completes the conversion to use the TopicName type in the
places where it belongs.

Next, we'll start making use of this new type distinction: instead
of using a TopicName implicitly as a String, we'll introduce members
corresponding to the different ways a topic name is used which are
going to start having different behavior in the future.

After that's complete, we can start enforcing the distinction in
order to catch accidental future implicit uses as a String.


api [nfc]: Introduce TopicName.apiName, and use for making API requests


api [nfc]: Use TopicName.apiName for constructing narrow links


notif [nfc]: Use TopicName.apiName for encoding NotificationOpenPayload

This isn't part of the Zulip server API -- it's entirely between the
buildUrl and parseUrl of this class, and we could make up whatever
encoding we like. But the API name of the topic is the natural thing
to use.


api [nfc]: Factor out TopicName.canonicalize


api [nfc]: Introduce TopicName.displayName, and use where needed

Each of these call sites will need to be updated for #1250. Those
sites can now be found by listing the references to this getter.

As a bonus the type-checker will point out most of the places that
need updates, because this getter will become nullable. (The
exceptions are where it's used in a string interpolation, because
null.toString() is valid and returns "null".)


api [nfc]: Seal the TopicName migration by making non-transparent

We're now explicitly using apiName, canonicalize, or displayName in
each of the places where we had been implicitly treating a topic name
as a String. Let the type-checker catch any future regressions on
that front, by removing "implements String".

More precisely, I think we've covered each such place. The
loophole is that Object methods can still be called, including
toString, so an interpolation like "${channelName} > ${topic}" won't
be flagged by the analyzer. I found some such interpolations with a
grep, but that's necessarily heuristic and there could be more.
So the analyzer won't give us quite as much help here as we'd like;
but it'll give quite a bit, and this is the most I see how to do.

Without the "implements String", a TopicName value can no longer be
implicitly converted to String or have String methods called on it.
Instead the type-checker will require any code that has such a value
to call one of the members we've declared on TopicName (or a member
of Object) in order to do anything with it. That way the code will
be explicit about whether it needs the API name, or the display
name, or the canonicalized form for making equality comparisons.

And that in turn will enable us to make "display name" behave
differently from "API name", for #1250 the "general chat" feature,
with a reliable way of tracking down which sites need which version.

gnprice added 25 commits January 7, 2025 20:16
The topic/subject field on DMs carries no information in the current
API, and we already never actually look at it, as demonstrated by
the lack of changes in the rest of the tree in this commit.  Clean
up the type definitions in the API a bit by making that explicit.

This is NFC except in the case where a server were to not send this
field at all for a DM (or to send a non-string value).  In that case
the old code would reject the server's response as malformed, and
the new code after this commit would accept it because it doesn't
look for the field.
The point in this autocomplete UI is to choose the entire new value
for the topic input.  So we can do that directly, without the more
complicated logic that parallels what we need in the more
complicated case of autocomplete in the content input.

This is NFC as far as the text value of the input is concerned.  I
believe it's probably NFC entirely, because the remaining aspects of
TextEditingValue -- the selection and the composing region -- are
probably clobbered anyway by the next line's giving focus to the
content input.  (The new test already passes before this change.)
We'll use this to make a type-level distinction between a string
that's being used to name a topic, and just any old string.
That will help us in particular with zulip#1250 the "general chat"
feature, where the way we show a topic name to the user stops being
necessarily just the same string that it appears as in the API.

The next phase will be to migrate a bunch of places in our code
to refer to TopicName when that's what they mean, instead of just
String.

During this phase, a TopicName can be freely used as a String, but
not vice versa.  So we'll do the migrations mostly in order of the
data flow, from upstream to downstream.  That will allow us to do
them over a series of individually coherent commits, with a minimum
of occasions where we temporarily introduce a conversion that won't
be needed in the final result.

That means: first, test data; then topics returned from the API to
our code; then our internal models; then back to the API, this time
for topics passed to the API from our code.

After we have the type in place all over where it belongs, we'll
start making use of the distinction, and then enforcing it.
These are being passed to test helpers which we'll leave as consuming
String instead of TopicName, for convenience in other test cases.

At these call sites the values were coming from fields that are or
will become TopicName, though; so when that happens and we make
TopicName no longer implicitly convertible to String, these will need
to be explicitly String instead.
We do this all together in one commit, to avoid trying to
disentangle the graph of how topics get passed around among the
various parts of our model.
This completes the conversion to use the TopicName type in the
places where it belongs.

Next, we'll start making use of this new type distinction: instead
of using a TopicName implicitly as a String, we'll introduce members
corresponding to the different ways a topic name is used which are
going to start having different behavior in the future.

After that's complete, we can start enforcing the distinction in
order to catch accidental future implicit uses as a String.
This isn't part of the Zulip server API -- it's entirely between the
`buildUrl` and `parseUrl` of this class, and we could make up whatever
encoding we like.  But the API name of the topic is the natural thing
to use.
Each of these call sites will need to be updated for zulip#1250.  Those
sites can now be found by listing the references to this getter.

As a bonus the type-checker will point out most of the places that
need updates, because this getter will become nullable.  (The
exceptions are where it's used in a string interpolation, because
`null.toString()` is valid and returns "null".)
@gnprice gnprice added the maintainer review PR ready for review by Zulip maintainers label Jan 8, 2025
We're now explicitly using apiName, canonicalize, or displayName in
each of the places where we had been implicitly treating a topic name
as a String.  Let the type-checker catch any future regressions on
that front, by removing "implements String".

More precisely, I *think* we've covered each such place.  The
loophole is that Object methods can still be called, including
toString, so an interpolation like "${channelName} > ${topic}" won't
be flagged by the analyzer.  I found some such interpolations with a
grep, but that's necessarily heuristic and there could be more.
So the analyzer won't give us quite as much help here as we'd like;
but it'll give quite a bit, and this is the most I see how to do.

Without the "implements String", a TopicName value can no longer be
implicitly converted to String or have String methods called on it.
Instead the type-checker will require any code that has such a value
to call one of the members we've declared on TopicName (or a member
of Object) in order to do anything with it.  That way the code will
be explicit about whether it needs the API name, or the display
name, or the canonicalized form for making equality comparisons.

And that in turn will enable us to make "display name" behave
differently from "API name", for zulip#1250 the "general chat" feature,
with a reliable way of tracking down which sites need which version.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
maintainer review PR ready for review by Zulip maintainers
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants