Skip to content

Have IHtmlCollection<T> implement IReadOnlyList<T> #1227

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

Merged
merged 1 commit into from
May 23, 2025

Conversation

weitzhandler
Copy link
Contributor

@weitzhandler weitzhandler commented May 4, 2025

Types of Changes

Makes the IHTmlCollection<T> interface implement IReadOnlyList<T>, which is the conventional interface for types that expose a count property and enables index access, features that IHtmlCollection<T> already provides.

Closes #1226

Prerequisites

Please make sure you can check the following two boxes:

  • I have read the CONTRIBUTING document
  • My code follows the code style of this project

Contribution Type

What types of changes does your code introduce? Put an x in all the boxes that apply:

  • Bug fix (non-breaking change which fixes an issue, please reference the issue id)
  • New feature (non-breaking change which adds functionality, make sure to open an associated issue first)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • My change requires a change to the documentation
  • I have updated the documentation accordingly
  • I have added tests to cover my changes
  • All new and existing tests passed

Description

Upgrades IHtmlCollection<T> to implement IReadOnlyList<T> instead of just IEnumerable<T> since it already implements all its functionality.

@CLAassistant
Copy link

CLAassistant commented May 4, 2025

CLA assistant check
All committers have signed the CLA.

@FlorianRappl
Copy link
Contributor

Sorry this would be breaking and is therefore not acceptable.

Maybe we can have a non-breaking way (however, I doubt that - as I guess you explicitly want the interface to implement it).

@weitzhandler
Copy link
Contributor Author

Why do you consider this a breaking change? It's not affecting any existing functionality.

@FlorianRappl
Copy link
Contributor

It changes a public interface. So if you upgrade your code will not compile (assuming you implement that interface somewhere). If you just swap you'll end up with an exception.

So it requires changes which is by definition a breaking change.

@weitzhandler
Copy link
Contributor Author

We could maybe overcome this with default interface implementation can't we?

@FlorianRappl
Copy link
Contributor

In general default interface methods could help (up to some extend), but as they require runtime support (starting with .NET Core 3.0, i.e., .NET standard 2.1) it would be another breaking change (see: https://github.com/AngleSharp/AngleSharp/blob/devel/src/AngleSharp.nuspec - it's .NET Standard 2.0 that we support in the lib).

@weitzhandler
Copy link
Contributor Author

Would u consider target-specific implementation?

    public interface IHtmlCollection<T>
#if NET8_0_OR_GREATER
        : IReadOnlyList<T>
#endif

@weitzhandler
Copy link
Contributor Author

weitzhandler commented May 16, 2025

For example (all tests pass after changing only this file to the following):

namespace AngleSharp.Dom;

using AngleSharp.Attributes;
using System;
using System.Collections.Generic;

/// <summary>
/// This type represents a set of space-separated tokens.
/// </summary>
[DomName("DOMTokenList")]
public interface ITokenList :
#if NET8_0_OR_GREATER
    IReadOnlyList<String>
#else
    IEnumerable<String>
#endif
{
    /// <summary>
    /// Gets the number of contained tokens.
    /// </summary>
    [DomName("length")]
    Int32 Length { get; }

    /// <summary>
    /// Gets an item in the list by its index.
    /// </summary>
    /// <param name="index">The index of the item.</param>
    /// <returns>The item at the specified index.</returns>
    [DomName("item")]
    [DomAccessor(Accessors.Getter)]
#if NET8_0_OR_GREATER
    new
#endif
    String this[Int32 index] { get; }

    /// <summary>
    /// Returns true if the underlying string contains a token, otherwise
    /// false.
    /// </summary>
    /// <param name="token">The token to search for.</param>
    /// <returns>The result of the search.</returns>
    [DomName("contains")]
    Boolean Contains(String token);

    /// <summary>
    /// Adds some tokens to the underlying string.
    /// </summary>
    /// <param name="tokens">A list of tokens to add.</param>
    [DomName("add")]
    void Add(params String[] tokens);

    /// <summary>
    /// Remove some tokens from the underlying string.
    /// </summary>
    /// <param name="tokens">A list of tokens to remove.</param>
    [DomName("remove")]
    void Remove(params String[] tokens);

    /// <summary>
    /// Removes the specified token from string and returns false.
    /// If token doesn't exist it's added and the function returns true.
    /// </summary>
    /// <param name="token">The token to toggle.</param>
    /// <param name="force"></param>
    /// <returns>
    /// True if the token has been added, otherwise false.
    /// </returns>
    [DomName("toggle")]
    Boolean Toggle(String token, Boolean force = false);


#if NET8_0_OR_GREATER
    Int32 IReadOnlyCollection<String>.Count => Length;
#endif
}

@FlorianRappl
Copy link
Contributor

I think it might be an option; but it definitely needs further testing (for instance the swap test; can the lib just be changed or is a re-compilation necessary; which also indicates a breaking change - this time not API, but ABI).

@weitzhandler
Copy link
Contributor Author

I ran a manual test I created a new project with a project reference to AngleSharp.Core then replaced only the AngleSharp.Core.dll file with the output of the compilation containing the IReadOnlyList change

@FlorianRappl
Copy link
Contributor

I ran a manual test I created a new project with a project reference to AngleSharp.Core then replaced only the AngleSharp.Core.dll file with the output of the compilation containing the IReadOnlyList change

Did the project contain a reference / a class defined to, e.g., implement ITokenList? Because this is the scenario we are looking for.

@weitzhandler
Copy link
Contributor Author

Yes, it's a console app that adds a List<String> subclass that implements ITokenList and makes use of the implementation adding values and accessing it's Length and Count properties, all based on the current devel branch that only implements IEnumerable<String>.

After replacing AngleSharp.dll in the console folder with the new one compiled after introducing the interface implementation change it still works as intended.

@FlorianRappl
Copy link
Contributor

Then let's do it :)

@weitzhandler
Copy link
Contributor Author

FYI
There's one test failing, but it also fails on the devel branch.

Copy link
Contributor

@FlorianRappl FlorianRappl left a comment

Choose a reason for hiding this comment

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

In general I like it and I think we can go for it. However, I don't like that we need to hide the indexer using new. That feels fishy. I know its rather a .NET problem, but my question is: Wouldn't we be able to use IReadOnlyCollection instead of IReadOnlyList, i.e., not using the indexer? What's your take - do you need the list / or would a collection be sufficient?

@weitzhandler
Copy link
Contributor Author

The reason we're 'overriding' it is to allow for the documentation and attributes.

@FlorianRappl
Copy link
Contributor

The reason we're 'overriding' it is to allow for the documentation and attributes.

I know that. I have nothing against the reason; I have something against hiding and I'd like to avoid it (as written).

@weitzhandler
Copy link
Contributor Author

Fixed

@FlorianRappl
Copy link
Contributor

Both systems are failing; any reason why you still use the list and not a collection? I don't really get it. As it stands the previous iteration was better imho.

@weitzhandler
Copy link
Contributor Author

weitzhandler commented May 23, 2025

Bear with me just a bit more please flo 😁
Reason IReadOnlyList would be preferred is because IHtmlCollection exposes all features of it just in a different name. I mean, if it works why not include it.

BTW, I've learned a bit of the code in the CSS selector, a true masterpiece!

Copy link
Contributor

@FlorianRappl FlorianRappl left a comment

Choose a reason for hiding this comment

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

I like it with the abstract way. I think this is a great way to deal with it. Lets have this in the preview and let's recheck for breaking changes.

@FlorianRappl FlorianRappl merged commit a873d68 into AngleSharp:devel May 23, 2025
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Have IHtmlCollection implement IReadOnlyList
3 participants