Skip to content

Commit e2b9b57

Browse files
committed
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 zulip#1250 the "general chat" feature, with a reliable way of tracking down which sites need which version.
1 parent bcb1b5b commit e2b9b57

File tree

2 files changed

+39
-3
lines changed

2 files changed

+39
-3
lines changed

lib/api/model/model.dart

+8-2
Original file line numberDiff line numberDiff line change
@@ -656,8 +656,14 @@ enum MessageFlag {
656656
}
657657

658658
/// The name of a Zulip topic.
659-
// TODO(#1250): Migrate all implicit uses as String; remove "implements String".
660-
extension type const TopicName(String _value) implements String {
659+
// TODO(dart): Can we forbid calling Object members on this extension type?
660+
// (The lack of "implements Object" ought to do that, but doesn't.)
661+
// In particular an interpolation "foo > $topic" is a bug we'd like to catch.
662+
// TODO(dart): Can we forbid using this extension type as a key in a Map?
663+
// (The lack of "implements Object" arguably should do that, but doesn't.)
664+
// Using as a Map key is almost certainly a bug because it won't case-fold;
665+
// see for example #739, #980, #1205.
666+
extension type const TopicName(String _value) {
661667
/// The string this topic is identified by in the Zulip API.
662668
///
663669
/// This should be used in constructing HTTP requests to the server,

lib/api/model/narrow.dart

+31-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,37 @@ ApiNarrow resolveDmElements(ApiNarrow narrow, int zulipFeatureLevel) {
2828
/// please add more as needed.
2929
sealed class ApiNarrowElement {
3030
String get operator;
31-
Object get operand;
31+
32+
/// The operand of this narrow filter.
33+
///
34+
/// The base-class getter [ApiNarrowElement.operand] returns `dynamic`,
35+
/// and its value should only be used for encoding as JSON, for use in a
36+
/// request to the Zulip server.
37+
///
38+
/// For any operations that depend more specifically on the operand's type,
39+
/// do not use run-time type checks on the value of [operand]; instead, make
40+
/// a run-time type check (e.g. with `switch`) on the [ApiNarrowElement]
41+
/// itself, and use the [operand] getter of the specific subtype.
42+
///
43+
/// That makes a difference because [ApiNarrowTopic.operand] has type
44+
/// [TopicName]; at runtime a [TopicName] is indistinguishable from [String],
45+
/// but an [ApiNarrowTopic] can still be distinguished from other subclasses.
46+
//
47+
// We can't just write [Object] here; if we do, the compiler rejects the
48+
// override in ApiNarrowTopic because TopicName can't be assigned to Object.
49+
// The reason that could be bad is that a caller of [ApiNarrowElement.operand]
50+
// could take the result and call Object members on it, like toString, even
51+
// though TopicName doesn't declare those members.
52+
//
53+
// In this case that's fine because the only plausible thing to do with
54+
// a generic [ApiNarrowElement.operand] is to encode it as JSON anyway,
55+
// which behaves just fine on TopicName.
56+
//
57+
// ... Even if it weren't fine, in the case of Object this protection is
58+
// thoroughly undermined already: code that has a TopicName can call Object
59+
// members on it directly. See comments at [TopicName].
60+
dynamic get operand; // see justification for `dynamic` above
61+
3262
final bool negated;
3363

3464
ApiNarrowElement({this.negated = false});

0 commit comments

Comments
 (0)